Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d449513861 | ||
|
|
6709e229b3 | ||
|
|
00a8f16262 | ||
|
|
00d16f8615 |
109
Dockerfile.alpine-musl
Normal file
109
Dockerfile.alpine-musl
Normal file
@@ -0,0 +1,109 @@
|
||||
# Alpine-based MUSL static binary builder for C-Relay
|
||||
# Produces truly portable binaries with zero runtime dependencies
|
||||
|
||||
FROM alpine:3.19 AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
musl-dev \
|
||||
git \
|
||||
cmake \
|
||||
pkgconfig \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
zlib-dev \
|
||||
zlib-static \
|
||||
curl-dev \
|
||||
curl-static \
|
||||
sqlite-dev \
|
||||
sqlite-static \
|
||||
linux-headers \
|
||||
wget \
|
||||
bash
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
|
||||
# Build libsecp256k1 static
|
||||
RUN cd /tmp && \
|
||||
git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||
cd secp256k1 && \
|
||||
./autogen.sh && \
|
||||
./configure --enable-static --disable-shared --prefix=/usr \
|
||||
CFLAGS="-fPIC" && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
rm -rf /tmp/secp256k1
|
||||
|
||||
# Build libwebsockets static with minimal features
|
||||
RUN cd /tmp && \
|
||||
git clone --depth 1 --branch v4.3.3 https://github.com/warmcat/libwebsockets.git && \
|
||||
cd libwebsockets && \
|
||||
mkdir build && cd build && \
|
||||
cmake .. \
|
||||
-DLWS_WITH_STATIC=ON \
|
||||
-DLWS_WITH_SHARED=OFF \
|
||||
-DLWS_WITH_SSL=ON \
|
||||
-DLWS_WITHOUT_TESTAPPS=ON \
|
||||
-DLWS_WITHOUT_TEST_SERVER=ON \
|
||||
-DLWS_WITHOUT_TEST_CLIENT=ON \
|
||||
-DLWS_WITHOUT_TEST_PING=ON \
|
||||
-DLWS_WITH_HTTP2=OFF \
|
||||
-DLWS_WITH_LIBUV=OFF \
|
||||
-DLWS_WITH_LIBEVENT=OFF \
|
||||
-DLWS_IPV6=ON \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_C_FLAGS="-fPIC" && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
rm -rf /tmp/libwebsockets
|
||||
|
||||
# Copy c-relay source
|
||||
COPY . /build/
|
||||
|
||||
# Clean up any stale submodule references (nips directory is not a submodule)
|
||||
RUN git rm --cached nips 2>/dev/null || true
|
||||
|
||||
# Initialize submodules and build nostr_core_lib with required NIPs
|
||||
# Disable fortification in build.sh to prevent __*_chk symbol issues
|
||||
# NIPs: 001(Basic), 006(Keys), 013(PoW), 017(DMs), 019(Bech32), 044(Encryption), 059(Gift Wrap - required by NIP-17)
|
||||
RUN git submodule update --init --recursive && \
|
||||
cd nostr_core_lib && \
|
||||
chmod +x build.sh && \
|
||||
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
|
||||
rm -f *.o *.a 2>/dev/null || true && \
|
||||
./build.sh --nips=1,6,13,17,19,44,59
|
||||
|
||||
# Build c-relay with full static linking
|
||||
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
|
||||
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||
-I. -Inostr_core_lib -Inostr_core_lib/nostr_core \
|
||||
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
|
||||
src/main.c src/config.c src/dm_admin.c src/request_validator.c \
|
||||
src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c \
|
||||
src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c \
|
||||
-o /build/c_relay_static_musl \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
|
||||
-lcurl -lz -lpthread -lm -ldl
|
||||
|
||||
# Strip binary to reduce size
|
||||
RUN strip /build/c_relay_static_musl
|
||||
|
||||
# Verify it's truly static
|
||||
RUN echo "=== Binary Information ===" && \
|
||||
file /build/c_relay_static_musl && \
|
||||
ls -lh /build/c_relay_static_musl && \
|
||||
echo "=== Checking for dynamic dependencies ===" && \
|
||||
(ldd /build/c_relay_static_musl 2>&1 || echo "Binary is static") && \
|
||||
echo "=== Build complete ==="
|
||||
|
||||
# Output stage - just the binary
|
||||
FROM scratch AS output
|
||||
COPY --from=builder /build/c_relay_static_musl /c_relay_static_musl
|
||||
305
build_static.sh
305
build_static.sh
@@ -1,144 +1,197 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build fully static MUSL binaries for C-Relay
|
||||
# Produces portable binaries with zero runtime dependencies
|
||||
# Build fully static MUSL binaries for C-Relay using Alpine Docker
|
||||
# Produces truly portable binaries with zero runtime dependencies
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
|
||||
|
||||
echo "Building fully static MUSL binaries for C-Relay..."
|
||||
echo "=========================================="
|
||||
echo "C-Relay MUSL Static Binary Builder"
|
||||
echo "=========================================="
|
||||
echo "Project directory: $SCRIPT_DIR"
|
||||
echo "Build directory: $BUILD_DIR"
|
||||
echo ""
|
||||
|
||||
# Create build directory
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# Check if Docker is available first
|
||||
if command -v docker &> /dev/null && sudo docker buildx version &> /dev/null 2>&1; then
|
||||
echo "Docker available but Alpine repositories are having issues - using native build"
|
||||
USE_DOCKER=false
|
||||
# Check if Docker is available
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "ERROR: Docker is not installed or not in PATH"
|
||||
echo ""
|
||||
echo "Docker is required to build MUSL static binaries."
|
||||
echo "Please install Docker:"
|
||||
echo " - Ubuntu/Debian: sudo apt install docker.io"
|
||||
echo " - Or visit: https://docs.docker.com/engine/install/"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker daemon is running (try with and without sudo)
|
||||
if docker info &> /dev/null; then
|
||||
DOCKER_CMD="docker"
|
||||
elif sudo docker info &> /dev/null; then
|
||||
echo "Note: Using sudo for Docker commands (user not in docker group)"
|
||||
echo "To avoid sudo, run: sudo usermod -aG docker $USER && newgrp docker"
|
||||
echo ""
|
||||
DOCKER_CMD="sudo docker"
|
||||
else
|
||||
echo "Docker not available - attempting native MUSL build"
|
||||
USE_DOCKER=false
|
||||
echo "ERROR: Docker daemon is not running"
|
||||
echo ""
|
||||
echo "Please start Docker:"
|
||||
echo " - sudo systemctl start docker"
|
||||
echo " - Or start Docker Desktop"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if musl-gcc is available for native build
|
||||
if [ "$USE_DOCKER" = false ]; then
|
||||
if ! command -v musl-gcc &> /dev/null; then
|
||||
echo "Installing musl development tools..."
|
||||
sudo apt update && sudo apt install -y musl-dev musl-tools
|
||||
|
||||
if ! command -v musl-gcc &> /dev/null; then
|
||||
echo "ERROR: Failed to install musl-gcc"
|
||||
echo "Please install musl-dev package manually: sudo apt install musl-dev musl-tools"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$USE_DOCKER" = true ]; then
|
||||
# Docker-based build
|
||||
echo "Building x86_64 static binary with Docker..."
|
||||
sudo docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
-f "$SCRIPT_DIR/examples/deployment/static-builder.Dockerfile" \
|
||||
-t c-relay-static-builder-x86_64 \
|
||||
--load \
|
||||
"$SCRIPT_DIR"
|
||||
|
||||
# Extract x86_64 binary
|
||||
sudo docker run --rm -v "$BUILD_DIR:/output" c-relay-static-builder-x86_64 \
|
||||
sh -c "cp /c_relay_static_musl_x86_64 /output/c_relay_static_x86_64"
|
||||
|
||||
echo "x86_64 static binary created: $BUILD_DIR/c_relay_static_x86_64"
|
||||
|
||||
# Build ARM64 static binary
|
||||
echo "Building ARM64 static binary with Docker..."
|
||||
sudo docker buildx build \
|
||||
--platform linux/arm64 \
|
||||
-f "$SCRIPT_DIR/examples/deployment/static-builder.Dockerfile" \
|
||||
-t c-relay-static-builder-arm64 \
|
||||
--load \
|
||||
"$SCRIPT_DIR"
|
||||
|
||||
# Extract ARM64 binary
|
||||
sudo docker run --rm -v "$BUILD_DIR:/output" c-relay-static-builder-arm64 \
|
||||
sh -c "cp /c_relay_static_musl_arm64 /output/c_relay_static_arm64"
|
||||
|
||||
echo "ARM64 static binary created: $BUILD_DIR/c_relay_static_arm64"
|
||||
else
|
||||
# Native static build with regular gcc
|
||||
echo "Building static binary with gcc..."
|
||||
|
||||
# Check for required static libraries
|
||||
echo "Checking for static libraries..."
|
||||
MISSING_LIBS=""
|
||||
|
||||
for lib in libsqlite3.a libssl.a libcrypto.a libz.a; do
|
||||
if ! find /usr/lib* /usr/local/lib* -name "$lib" 2>/dev/null | head -1 | grep -q .; then
|
||||
MISSING_LIBS="$MISSING_LIBS $lib"
|
||||
fi
|
||||
done
|
||||
|
||||
# libsecp256k1 might not be available as static lib, so we'll try without it first
|
||||
|
||||
# Initialize submodules if needed
|
||||
if [ ! -f "nostr_core_lib/libnostr_core_x64.a" ]; then
|
||||
echo "Building nostr_core_lib..."
|
||||
git submodule update --init --recursive
|
||||
cd nostr_core_lib && ./build.sh && cd ..
|
||||
fi
|
||||
|
||||
# Install additional static libraries needed for libwebsockets
|
||||
echo "Installing additional static libraries..."
|
||||
sudo apt install -y libcap-dev libuv1-dev libev-dev
|
||||
|
||||
# Try building with regular gcc and static linking
|
||||
echo "Compiling with gcc -static..."
|
||||
|
||||
# Use the same approach as the regular Makefile but with static linking
|
||||
gcc -static -O2 -Wall -Wextra -std=c99 -g \
|
||||
-I. -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
|
||||
src/main.c src/config.c src/dm_admin.c src/request_validator.c src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c \
|
||||
-o "$BUILD_DIR/c_relay_static_x86_64" \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
-lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl -lcap -luv_a -lev
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "x86_64 static binary created: $BUILD_DIR/c_relay_static_x86_64"
|
||||
else
|
||||
echo "ERROR: Static build failed"
|
||||
echo "This may be due to missing static libraries or incompatible library versions"
|
||||
echo "Consider using Docker-based build instead"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify binaries
|
||||
echo "Verifying static binaries..."
|
||||
for binary in "$BUILD_DIR"/c_relay_static_*; do
|
||||
if [ -f "$binary" ]; then
|
||||
echo "Binary: $(basename "$binary")"
|
||||
file "$binary"
|
||||
ls -lh "$binary"
|
||||
|
||||
# Test if binary is truly static (no dynamic dependencies)
|
||||
if ldd "$binary" 2>/dev/null | grep -q "not a dynamic executable"; then
|
||||
echo "✓ Binary is fully static"
|
||||
elif ldd "$binary" 2>/dev/null | grep -q "statically linked"; then
|
||||
echo "✓ Binary is statically linked"
|
||||
else
|
||||
echo "⚠ Binary may have dynamic dependencies:"
|
||||
ldd "$binary" 2>/dev/null || echo " (ldd check failed)"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Static build complete!"
|
||||
echo "Binaries available in: $BUILD_DIR/"
|
||||
ls -la "$BUILD_DIR"/c_relay_static_* 2>/dev/null || echo "No static binaries found"
|
||||
echo "✓ Docker is available and running"
|
||||
echo ""
|
||||
echo "These binaries should have minimal runtime dependencies and work across Linux distributions."
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="c_relay_static_musl_x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
PLATFORM="linux/arm64"
|
||||
OUTPUT_NAME="c_relay_static_musl_arm64"
|
||||
;;
|
||||
*)
|
||||
echo "WARNING: Unknown architecture: $ARCH"
|
||||
echo "Defaulting to linux/amd64"
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="c_relay_static_musl_${ARCH}"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Building for platform: $PLATFORM"
|
||||
echo "Output binary: $OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
# Build the Docker image
|
||||
echo "=========================================="
|
||||
echo "Step 1: Building Alpine Docker image"
|
||||
echo "=========================================="
|
||||
echo "This will:"
|
||||
echo " - Use Alpine Linux (native MUSL)"
|
||||
echo " - Build all dependencies statically"
|
||||
echo " - Compile c-relay with full static linking"
|
||||
echo ""
|
||||
|
||||
$DOCKER_CMD build \
|
||||
--platform "$PLATFORM" \
|
||||
-f "$DOCKERFILE" \
|
||||
-t c-relay-musl-builder:latest \
|
||||
--progress=plain \
|
||||
. || {
|
||||
echo ""
|
||||
echo "ERROR: Docker build failed"
|
||||
echo "Check the output above for details"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "✓ Docker image built successfully"
|
||||
echo ""
|
||||
|
||||
# Extract the binary from the container
|
||||
echo "=========================================="
|
||||
echo "Step 2: Extracting static binary"
|
||||
echo "=========================================="
|
||||
|
||||
# Build the builder stage to extract the binary
|
||||
$DOCKER_CMD build \
|
||||
--platform "$PLATFORM" \
|
||||
--target builder \
|
||||
-f "$DOCKERFILE" \
|
||||
-t c-relay-musl-builder-stage:latest \
|
||||
. > /dev/null 2>&1
|
||||
|
||||
# Create a temporary container to copy the binary
|
||||
CONTAINER_ID=$($DOCKER_CMD create c-relay-musl-builder-stage:latest)
|
||||
|
||||
# Copy binary from container
|
||||
$DOCKER_CMD cp "$CONTAINER_ID:/build/c_relay_static_musl" "$BUILD_DIR/$OUTPUT_NAME" || {
|
||||
echo "ERROR: Failed to extract binary from container"
|
||||
$DOCKER_CMD rm "$CONTAINER_ID" 2>/dev/null
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Clean up container
|
||||
$DOCKER_CMD rm "$CONTAINER_ID" > /dev/null
|
||||
|
||||
echo "✓ Binary extracted to: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
# Make binary executable
|
||||
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
|
||||
|
||||
# Verify the binary
|
||||
echo "=========================================="
|
||||
echo "Step 3: Verifying static binary"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
echo "File information:"
|
||||
file "$BUILD_DIR/$OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
echo "File size: $(ls -lh "$BUILD_DIR/$OUTPUT_NAME" | awk '{print $5}')"
|
||||
echo ""
|
||||
|
||||
echo "Checking for dynamic dependencies:"
|
||||
LDD_OUTPUT=$(ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1)
|
||||
if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable"; then
|
||||
echo "✓ Binary is fully static (no dynamic dependencies)"
|
||||
TRULY_STATIC=true
|
||||
elif echo "$LDD_OUTPUT" | grep -q "statically linked"; then
|
||||
echo "✓ Binary is statically linked"
|
||||
TRULY_STATIC=true
|
||||
else
|
||||
echo "⚠ WARNING: Binary may have dynamic dependencies:"
|
||||
echo "$LDD_OUTPUT"
|
||||
TRULY_STATIC=false
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test if binary runs
|
||||
echo "Testing binary execution:"
|
||||
if "$BUILD_DIR/$OUTPUT_NAME" --version 2>&1 | head -5; then
|
||||
echo "✓ Binary executes successfully"
|
||||
else
|
||||
echo "⚠ Binary execution test failed (this may be normal if --version is not supported)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "=========================================="
|
||||
echo "Build Summary"
|
||||
echo "=========================================="
|
||||
echo "Binary: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||
echo "Platform: $PLATFORM"
|
||||
if [ "$TRULY_STATIC" = true ]; then
|
||||
echo "Type: Fully static MUSL binary"
|
||||
echo "Portability: Works on ANY Linux distribution"
|
||||
else
|
||||
echo "Type: Static binary (may have minimal dependencies)"
|
||||
fi
|
||||
echo ""
|
||||
echo "✓ Build complete!"
|
||||
echo ""
|
||||
echo "To use the binary:"
|
||||
echo " $BUILD_DIR/$OUTPUT_NAME --port 8888"
|
||||
echo ""
|
||||
echo "To verify portability, test on different Linux distributions:"
|
||||
echo " - Alpine Linux"
|
||||
echo " - Ubuntu/Debian"
|
||||
echo " - CentOS/RHEL"
|
||||
echo " - Arch Linux"
|
||||
echo ""
|
||||
27
deploy_static.sh
Executable file
27
deploy_static.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# C-Relay Static Binary Deployment Script
|
||||
# Deploys build/c_relay_static_x86_64 to server via sshlt
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
LOCAL_BINARY="build/c_relay_static_x86_64"
|
||||
REMOTE_BINARY_PATH="/usr/local/bin/c_relay/c_relay"
|
||||
SERVICE_NAME="c-relay"
|
||||
|
||||
# Create backup
|
||||
ssh ubuntu@laantungir.com "sudo cp '$REMOTE_BINARY_PATH' '${REMOTE_BINARY_PATH}.backup.$(date +%Y%m%d_%H%M%S)'" 2>/dev/null || true
|
||||
|
||||
# Upload binary to temp location
|
||||
scp "$LOCAL_BINARY" "ubuntu@laantungir.com:/tmp/c_relay.tmp"
|
||||
|
||||
# Install binary
|
||||
ssh ubuntu@laantungir.com "sudo mv '/tmp/c_relay.tmp' '$REMOTE_BINARY_PATH'"
|
||||
ssh ubuntu@laantungir.com "sudo chown c-relay:c-relay '$REMOTE_BINARY_PATH'"
|
||||
ssh ubuntu@laantungir.com "sudo chmod +x '$REMOTE_BINARY_PATH'"
|
||||
|
||||
# Restart service
|
||||
ssh ubuntu@laantungir.com "sudo systemctl restart '$SERVICE_NAME'"
|
||||
|
||||
echo "Deployment complete!"
|
||||
275
docs/musl_static_build.md
Normal file
275
docs/musl_static_build.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# MUSL Static Binary Build Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to build truly portable MUSL-based static binaries of c-relay using Alpine Linux Docker containers. These binaries have **zero runtime dependencies** and work on any Linux distribution.
|
||||
|
||||
## Why MUSL?
|
||||
|
||||
### MUSL vs glibc Static Binaries
|
||||
|
||||
**MUSL Advantages:**
|
||||
- **Truly Static**: No hidden dependencies on system libraries
|
||||
- **Smaller Size**: ~7.6MB vs ~12MB+ for glibc static builds
|
||||
- **Better Portability**: Works on ANY Linux distribution without modification
|
||||
- **Cleaner Linking**: No glibc-specific extensions or fortified functions
|
||||
- **Simpler Deployment**: Single binary, no library compatibility issues
|
||||
|
||||
**glibc Limitations:**
|
||||
- Static builds still require dynamic loading for NSS (Name Service Switch)
|
||||
- Fortified functions (`__*_chk`) don't exist in MUSL
|
||||
- Larger binary size due to glibc's complexity
|
||||
- May have compatibility issues across different glibc versions
|
||||
|
||||
## Build Process
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker installed and running
|
||||
- Sufficient disk space (~2GB for Docker layers)
|
||||
- Internet connection (for downloading dependencies)
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Build MUSL static binary
|
||||
./build_static.sh
|
||||
|
||||
# The binary will be created at:
|
||||
# build/c_relay_static_musl_x86_64 (on x86_64)
|
||||
# build/c_relay_static_musl_arm64 (on ARM64)
|
||||
```
|
||||
|
||||
### What Happens During Build
|
||||
|
||||
1. **Alpine Linux Base**: Uses Alpine 3.19 with native MUSL support
|
||||
2. **Static Dependencies**: Builds all dependencies with static linking:
|
||||
- libsecp256k1 (Bitcoin cryptography)
|
||||
- libwebsockets (WebSocket server)
|
||||
- OpenSSL (TLS/crypto)
|
||||
- SQLite (database)
|
||||
- curl (HTTP client)
|
||||
- zlib (compression)
|
||||
|
||||
3. **nostr_core_lib**: Builds with MUSL-compatible flags:
|
||||
- Disables glibc fortification (`-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0`)
|
||||
- Includes required NIPs: 001, 006, 013, 017, 019, 044, 059
|
||||
- Produces static library (~316KB)
|
||||
|
||||
4. **c-relay Compilation**: Links everything statically:
|
||||
- All source files compiled with `-static` flag
|
||||
- Fortification disabled to avoid `__*_chk` symbols
|
||||
- Results in ~7.6MB stripped binary
|
||||
|
||||
5. **Verification**: Confirms binary is truly static:
|
||||
- `ldd` shows "not a dynamic executable"
|
||||
- `file` shows "statically linked"
|
||||
- Binary executes successfully
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Dockerfile Structure
|
||||
|
||||
The build uses a multi-stage Dockerfile (`Dockerfile.alpine-musl`):
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Builder (Alpine Linux)
|
||||
FROM alpine:3.19 AS builder
|
||||
- Install build tools and static libraries
|
||||
- Build dependencies from source
|
||||
- Compile nostr_core_lib with MUSL flags
|
||||
- Compile c-relay with full static linking
|
||||
- Strip binary to reduce size
|
||||
|
||||
# Stage 2: Output (scratch)
|
||||
FROM scratch AS output
|
||||
- Contains only the final binary
|
||||
```
|
||||
|
||||
### Key Compilation Flags
|
||||
|
||||
**For nostr_core_lib:**
|
||||
```bash
|
||||
CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"
|
||||
```
|
||||
|
||||
**For c-relay:**
|
||||
```bash
|
||||
gcc -static -O2 -Wall -Wextra -std=c99 \
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||
[source files] \
|
||||
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
|
||||
-lcurl -lz -lpthread -lm -ldl
|
||||
```
|
||||
|
||||
### Fortification Issue
|
||||
|
||||
**Problem**: GCC's `-O2` optimization enables fortification by default, replacing standard functions with `__*_chk` variants (e.g., `__snprintf_chk`, `__fprintf_chk`). These are glibc-specific and don't exist in MUSL.
|
||||
|
||||
**Solution**: Explicitly disable fortification with:
|
||||
- `-U_FORTIFY_SOURCE` (undefine any existing definition)
|
||||
- `-D_FORTIFY_SOURCE=0` (set to 0)
|
||||
|
||||
This must be applied to **both** nostr_core_lib and c-relay compilation.
|
||||
|
||||
### NIP Dependencies
|
||||
|
||||
The build includes these NIPs in nostr_core_lib:
|
||||
- **NIP-001**: Basic protocol (event creation, signing)
|
||||
- **NIP-006**: Key derivation from mnemonic
|
||||
- **NIP-013**: Proof of Work validation
|
||||
- **NIP-017**: Private Direct Messages
|
||||
- **NIP-019**: Bech32 encoding (nsec/npub)
|
||||
- **NIP-044**: Modern encryption
|
||||
- **NIP-059**: Gift Wrap (required by NIP-017)
|
||||
|
||||
## Verification
|
||||
|
||||
### Check Binary Type
|
||||
|
||||
```bash
|
||||
# Should show "statically linked"
|
||||
file build/c_relay_static_musl_x86_64
|
||||
|
||||
# Should show "not a dynamic executable"
|
||||
ldd build/c_relay_static_musl_x86_64
|
||||
|
||||
# Check size (should be ~7.6MB)
|
||||
ls -lh build/c_relay_static_musl_x86_64
|
||||
```
|
||||
|
||||
### Test Execution
|
||||
|
||||
```bash
|
||||
# Show help
|
||||
./build/c_relay_static_musl_x86_64 --help
|
||||
|
||||
# Show version
|
||||
./build/c_relay_static_musl_x86_64 --version
|
||||
|
||||
# Run relay
|
||||
./build/c_relay_static_musl_x86_64 --port 8888
|
||||
```
|
||||
|
||||
### Cross-Distribution Testing
|
||||
|
||||
Test the binary on different distributions to verify portability:
|
||||
|
||||
```bash
|
||||
# Alpine Linux
|
||||
docker run --rm -v $(pwd)/build:/app alpine:latest /app/c_relay_static_musl_x86_64 --version
|
||||
|
||||
# Ubuntu
|
||||
docker run --rm -v $(pwd)/build:/app ubuntu:latest /app/c_relay_static_musl_x86_64 --version
|
||||
|
||||
# Debian
|
||||
docker run --rm -v $(pwd)/build:/app debian:latest /app/c_relay_static_musl_x86_64 --version
|
||||
|
||||
# CentOS
|
||||
docker run --rm -v $(pwd)/build:/app centos:latest /app/c_relay_static_musl_x86_64 --version
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker Permission Denied
|
||||
|
||||
**Problem**: `permission denied while trying to connect to the Docker daemon socket`
|
||||
|
||||
**Solution**: Add user to docker group:
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
newgrp docker # Or logout and login again
|
||||
```
|
||||
|
||||
### Build Fails with Fortification Errors
|
||||
|
||||
**Problem**: `undefined reference to '__snprintf_chk'` or `'__fprintf_chk'`
|
||||
|
||||
**Solution**: Ensure fortification is disabled in both:
|
||||
1. nostr_core_lib build.sh (line 534)
|
||||
2. c-relay compilation flags in Dockerfile
|
||||
|
||||
### Binary Won't Execute
|
||||
|
||||
**Problem**: Binary fails to run on target system
|
||||
|
||||
**Checks**:
|
||||
1. Verify it's truly static: `ldd binary` should show "not a dynamic executable"
|
||||
2. Check architecture matches: `file binary` should show correct arch
|
||||
3. Ensure execute permissions: `chmod +x binary`
|
||||
|
||||
### Missing NIP Functions
|
||||
|
||||
**Problem**: `undefined reference to 'nostr_nip*'` during linking
|
||||
|
||||
**Solution**: Add missing NIPs to the build command:
|
||||
```bash
|
||||
./build.sh --nips=1,6,13,17,19,44,59
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Single Binary Deployment
|
||||
|
||||
```bash
|
||||
# Copy binary to server
|
||||
scp build/c_relay_static_musl_x86_64 user@server:/opt/c-relay/
|
||||
|
||||
# Run on server (no dependencies needed!)
|
||||
ssh user@server
|
||||
cd /opt/c-relay
|
||||
./c_relay_static_musl_x86_64 --port 8888
|
||||
```
|
||||
|
||||
### SystemD Service
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=C-Relay Nostr Relay (MUSL Static)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=c-relay
|
||||
WorkingDirectory=/opt/c-relay
|
||||
ExecStart=/opt/c-relay/c_relay_static_musl_x86_64
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Metric | MUSL Static | glibc Static | glibc Dynamic |
|
||||
|--------|-------------|--------------|---------------|
|
||||
| Binary Size | 7.6 MB | 12+ MB | 2-3 MB |
|
||||
| Startup Time | ~50ms | ~60ms | ~40ms |
|
||||
| Memory Usage | Similar | Similar | Similar |
|
||||
| Portability | ✓ Any Linux | ⚠ glibc only | ✗ Requires libs |
|
||||
| Dependencies | None | NSS libs | Many libs |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always verify** the binary is truly static before deployment
|
||||
2. **Test on multiple distributions** to ensure portability
|
||||
3. **Keep Docker images updated** for security patches
|
||||
4. **Document the build date** and commit hash for reproducibility
|
||||
5. **Store binaries** with architecture in filename (e.g., `_x86_64`, `_arm64`)
|
||||
|
||||
## References
|
||||
|
||||
- [MUSL libc](https://musl.libc.org/)
|
||||
- [Alpine Linux](https://alpinelinux.org/)
|
||||
- [Static Linking Best Practices](https://www.musl-libc.org/faq.html)
|
||||
- [GCC Fortification](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html)
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2025-10-11
|
||||
- Initial MUSL build system implementation
|
||||
- Alpine Docker-based build process
|
||||
- Fortification fix for nostr_core_lib
|
||||
- Complete NIP dependency resolution
|
||||
- Documentation created
|
||||
@@ -69,6 +69,20 @@ RUN cd /tmp && \
|
||||
./Configure linux-x86_64 no-shared --prefix=/usr && \
|
||||
make && make install_sw
|
||||
|
||||
# Build SQLite with JSON1 extension enabled
|
||||
RUN cd /tmp && \
|
||||
wget https://www.sqlite.org/2024/sqlite-autoconf-3460000.tar.gz && \
|
||||
tar xzf sqlite-autoconf-3460000.tar.gz && \
|
||||
cd sqlite-autoconf-3460000 && \
|
||||
./configure \
|
||||
--enable-static \
|
||||
--disable-shared \
|
||||
--enable-json1 \
|
||||
--enable-fts5 \
|
||||
--prefix=/usr \
|
||||
CFLAGS="-DSQLITE_ENABLE_JSON1=1 -DSQLITE_ENABLE_FTS5=1" && \
|
||||
make && make install
|
||||
|
||||
# Build libsecp256k1 static
|
||||
RUN cd /tmp && \
|
||||
git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||
|
||||
@@ -163,9 +163,15 @@ rm -f db/c_nostr_relay.db* 2>/dev/null
|
||||
echo "Embedding web files..."
|
||||
./embed_web_files.sh
|
||||
|
||||
# Build the project first
|
||||
echo "Building project..."
|
||||
make clean all
|
||||
# Build the project first - use static build by default
|
||||
echo "Building project (static binary with SQLite JSON1 extension)..."
|
||||
./build_static.sh
|
||||
|
||||
# Fallback to regular build if static build fails
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Static build failed, falling back to regular build..."
|
||||
make clean all
|
||||
fi
|
||||
|
||||
# Restore database files if preserving
|
||||
if [ "$PRESERVE_DATABASE" = true ] && [ -d "/tmp/relay_backup_$$" ]; then
|
||||
@@ -181,22 +187,34 @@ if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if relay binary exists after build - detect architecture
|
||||
# Check if relay binary exists after build - prefer static binary, fallback to regular
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
BINARY_PATH="./build/c_relay_x86"
|
||||
STATIC_BINARY="./build/c_relay_static_x86_64"
|
||||
REGULAR_BINARY="./build/c_relay_x86"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
BINARY_PATH="./build/c_relay_arm64"
|
||||
STATIC_BINARY="./build/c_relay_static_arm64"
|
||||
REGULAR_BINARY="./build/c_relay_arm64"
|
||||
;;
|
||||
*)
|
||||
BINARY_PATH="./build/c_relay_$ARCH"
|
||||
STATIC_BINARY="./build/c_relay_static_$ARCH"
|
||||
REGULAR_BINARY="./build/c_relay_$ARCH"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -f "$BINARY_PATH" ]; then
|
||||
echo "ERROR: Relay binary not found at $BINARY_PATH after build. Build may have failed."
|
||||
# Prefer static binary if available
|
||||
if [ -f "$STATIC_BINARY" ]; then
|
||||
BINARY_PATH="$STATIC_BINARY"
|
||||
echo "Using static binary: $BINARY_PATH"
|
||||
elif [ -f "$REGULAR_BINARY" ]; then
|
||||
BINARY_PATH="$REGULAR_BINARY"
|
||||
echo "Using regular binary: $BINARY_PATH"
|
||||
else
|
||||
echo "ERROR: No relay binary found. Checked:"
|
||||
echo " - $STATIC_BINARY"
|
||||
echo " - $REGULAR_BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Submodule nostr_core_lib updated: c0784fc890...5066ba8dd0
133
src/config.c
133
src/config.c
@@ -11,6 +11,7 @@
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <libwebsockets.h>
|
||||
|
||||
// External database connection (from main.c)
|
||||
@@ -132,7 +133,6 @@ void force_config_cache_refresh(void) {
|
||||
g_unified_cache.cache_valid = 0;
|
||||
g_unified_cache.cache_expires = 0;
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
log_info("Configuration cache forcibly invalidated");
|
||||
}
|
||||
|
||||
// Update specific cache value without full refresh
|
||||
@@ -211,7 +211,6 @@ int update_cache_value(const char* key, const char* value) {
|
||||
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
|
||||
log_info("Updated specific cache value");
|
||||
printf(" Key: %s\n", key);
|
||||
return 0;
|
||||
}
|
||||
@@ -223,8 +222,6 @@ static int refresh_unified_cache_from_table(void) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Refreshing unified configuration cache from database");
|
||||
|
||||
// Lock the cache for update (don't memset entire cache to avoid wiping relay_info)
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
|
||||
@@ -361,7 +358,6 @@ static int refresh_unified_cache_from_table(void) {
|
||||
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
|
||||
log_info("Unified configuration cache refreshed from database");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -502,7 +498,6 @@ int create_database_with_relay_pubkey(const char* relay_pubkey) {
|
||||
strncpy(g_database_path, db_name, sizeof(g_database_path) - 1);
|
||||
g_database_path[sizeof(g_database_path) - 1] = '\0';
|
||||
|
||||
log_info("Creating database with relay pubkey");
|
||||
printf(" Database: %s\n", db_name);
|
||||
|
||||
free(db_name);
|
||||
@@ -562,7 +557,6 @@ int store_config_event_in_database(const cJSON* event) {
|
||||
free(tags_str);
|
||||
|
||||
if (rc == SQLITE_DONE) {
|
||||
log_success("Configuration event stored in database");
|
||||
return 0;
|
||||
} else {
|
||||
log_error("Failed to store configuration event");
|
||||
@@ -576,7 +570,6 @@ cJSON* load_config_event_from_database(const char* relay_pubkey) {
|
||||
}
|
||||
|
||||
// Configuration is now managed through config table, not events
|
||||
log_info("Configuration events are no longer stored in events table");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -793,8 +786,6 @@ int init_configuration_system(const char* config_dir_override, const char* confi
|
||||
(void)config_dir_override;
|
||||
(void)config_file_override;
|
||||
|
||||
log_info("Initializing event-based configuration system...");
|
||||
|
||||
// Initialize unified cache with proper structure initialization
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
|
||||
@@ -839,14 +830,11 @@ int init_configuration_system(const char* config_dir_override, const char* confi
|
||||
g_unified_cache.nip70_protected_events_enabled = 0;
|
||||
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
|
||||
log_success("Event-based configuration system initialized with unified cache structures");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cleanup_configuration_system(void) {
|
||||
log_info("Cleaning up configuration system...");
|
||||
|
||||
if (g_current_config) {
|
||||
cJSON_Delete(g_current_config);
|
||||
g_current_config = NULL;
|
||||
@@ -907,7 +895,6 @@ void cleanup_configuration_system(void) {
|
||||
memset(&g_unified_cache.expiration_config, 0, sizeof(g_unified_cache.expiration_config));
|
||||
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
log_success("Configuration system cleaned up with proper JSON cleanup");
|
||||
}
|
||||
|
||||
int set_database_config(const char* key, const char* value, const char* changed_by) {
|
||||
@@ -1001,7 +988,6 @@ int store_relay_private_key(const char* relay_privkey_hex) {
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc == SQLITE_DONE) {
|
||||
log_success("Relay private key stored securely in database");
|
||||
return 0;
|
||||
} else {
|
||||
log_error("Failed to store relay private key in database");
|
||||
@@ -1064,8 +1050,6 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
log_info("Creating default configuration event...");
|
||||
|
||||
// Create tags array with default configuration values
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) {
|
||||
@@ -1101,7 +1085,6 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||
char port_str[16];
|
||||
snprintf(port_str, sizeof(port_str), "%d", cli_options->port_override);
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(port_str));
|
||||
log_info("Using command line port override in configuration event");
|
||||
printf(" Port: %d (overriding default %s)\n", cli_options->port_override, DEFAULT_CONFIG_VALUES[i].value);
|
||||
} else {
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
|
||||
@@ -1134,7 +1117,6 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
|
||||
if (id_obj && pubkey_obj) {
|
||||
log_success("Default configuration event created successfully");
|
||||
printf(" Event ID: %s\n", cJSON_GetStringValue(id_obj));
|
||||
printf(" Admin Public Key: %s\n", cJSON_GetStringValue(pubkey_obj));
|
||||
}
|
||||
@@ -1147,7 +1129,6 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||
// ================================
|
||||
|
||||
int first_time_startup_sequence(const cli_options_t* cli_options) {
|
||||
log_info("Starting first-time startup sequence...");
|
||||
|
||||
// 1. Generate or use provided admin keypair
|
||||
unsigned char admin_privkey_bytes[32];
|
||||
@@ -1156,7 +1137,6 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
||||
|
||||
if (cli_options && strlen(cli_options->admin_pubkey_override) == 64) {
|
||||
// Use provided admin public key directly - skip private key generation entirely
|
||||
log_info("Using provided admin public key override - skipping private key generation");
|
||||
strncpy(admin_pubkey, cli_options->admin_pubkey_override, sizeof(admin_pubkey) - 1);
|
||||
admin_pubkey[sizeof(admin_pubkey) - 1] = '\0';
|
||||
|
||||
@@ -1178,7 +1158,6 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
||||
generated_admin_key = 0; // Did not generate a new key
|
||||
} else {
|
||||
// Generate random admin keypair using /dev/urandom + nostr_core_lib
|
||||
log_info("Generating random admin keypair");
|
||||
if (generate_random_private_key_bytes(admin_privkey_bytes) != 0) {
|
||||
log_error("Failed to generate admin private key");
|
||||
return -1;
|
||||
@@ -1201,7 +1180,6 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
||||
|
||||
if (cli_options && strlen(cli_options->relay_privkey_override) == 64) {
|
||||
// Use provided relay private key
|
||||
log_info("Using provided relay private key override");
|
||||
strncpy(relay_privkey, cli_options->relay_privkey_override, sizeof(relay_privkey) - 1);
|
||||
relay_privkey[sizeof(relay_privkey) - 1] = '\0';
|
||||
|
||||
@@ -1249,10 +1227,8 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
||||
// 5. Store relay private key in temporary storage for later secure storage
|
||||
strncpy(g_temp_relay_privkey, relay_privkey, sizeof(g_temp_relay_privkey) - 1);
|
||||
g_temp_relay_privkey[sizeof(g_temp_relay_privkey) - 1] = '\0';
|
||||
log_info("Relay private key cached for secure storage after database initialization");
|
||||
|
||||
|
||||
// 6. Handle configuration setup - defaults will be populated after database initialization
|
||||
log_info("Configuration setup prepared - defaults will be populated after database initialization");
|
||||
|
||||
|
||||
// CLI overrides will be applied after database initialization in main.c
|
||||
@@ -1285,7 +1261,6 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
log_success("First-time startup sequence completed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1295,7 +1270,6 @@ int startup_existing_relay(const char* relay_pubkey) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Starting existing relay...");
|
||||
printf(" Relay pubkey: %s\n", relay_pubkey);
|
||||
|
||||
// Store relay pubkey in unified cache
|
||||
@@ -1321,12 +1295,9 @@ int startup_existing_relay(const char* relay_pubkey) {
|
||||
}
|
||||
|
||||
// Configuration will be migrated from events to table after database initialization
|
||||
log_info("Configuration migration will be performed after database is available");
|
||||
|
||||
// Load configuration event from database (after database is initialized)
|
||||
// This will be done in apply_configuration_from_database()
|
||||
|
||||
log_success("Existing relay startup prepared");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1724,7 +1695,6 @@ static int validate_configuration_event_fields(const cJSON* event, char* error_m
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Validating configuration event fields...");
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
@@ -1779,10 +1749,7 @@ static int validate_configuration_event_fields(const cJSON* event, char* error_m
|
||||
log_error(summary);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char success_msg[256];
|
||||
snprintf(success_msg, sizeof(success_msg), "%d configuration fields validated successfully", validated_fields);
|
||||
log_success(success_msg);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1791,9 +1758,7 @@ int process_configuration_event(const cJSON* event) {
|
||||
log_error("Invalid configuration event");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Processing configuration event...");
|
||||
|
||||
|
||||
// Validate event structure
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
@@ -1819,22 +1784,19 @@ int process_configuration_event(const cJSON* event) {
|
||||
}
|
||||
|
||||
// Comprehensive event validation using nostr_core_lib
|
||||
log_info("Validating configuration event structure and signature...");
|
||||
|
||||
|
||||
// First validate the event structure (fields, format, etc.)
|
||||
if (nostr_validate_event_structure((cJSON*)event) != NOSTR_SUCCESS) {
|
||||
log_error("Configuration event has invalid structure");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// Then validate the cryptographic signature
|
||||
if (nostr_verify_event_signature((cJSON*)event) != NOSTR_SUCCESS) {
|
||||
log_error("Configuration event has invalid signature");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_success("Configuration event structure and signature validated successfully");
|
||||
|
||||
|
||||
// NEW: Validate configuration field values
|
||||
char validation_error[512];
|
||||
if (validate_configuration_event_fields(event, validation_error, sizeof(validation_error)) != 0) {
|
||||
@@ -2262,22 +2224,10 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro
|
||||
}
|
||||
|
||||
int kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||
printf(" Event kind: %d\n", kind);
|
||||
|
||||
// Extract and log event details for debugging
|
||||
// Get event pubkey for authorization logging
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
|
||||
const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown";
|
||||
const char* event_content = content_obj ? cJSON_GetStringValue(content_obj) : "unknown";
|
||||
|
||||
printf(" Pubkey: %.16s...\n", event_pubkey ? event_pubkey : "null");
|
||||
printf(" Content length: %zu\n", event_content ? strlen(event_content) : 0);
|
||||
printf(" Has tags: %s\n", tags_obj ? "yes" : "no");
|
||||
if (tags_obj && cJSON_IsArray(tags_obj)) {
|
||||
printf(" Tags count: %d\n", cJSON_GetArraySize(tags_obj));
|
||||
}
|
||||
const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
|
||||
|
||||
// DEFENSE-IN-DEPTH: Use comprehensive admin authorization validation
|
||||
if (!is_authorized_admin_event(event)) {
|
||||
@@ -2306,12 +2256,7 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro
|
||||
|
||||
// Handle legacy Kind 33334 configuration management events
|
||||
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) {
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0;
|
||||
|
||||
log_info("Processing admin configuration event");
|
||||
printf(" Kind: %d\n", kind);
|
||||
|
||||
|
||||
// Parse tags to find query commands according to API specification
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
if (tags_obj && cJSON_IsArray(tags_obj)) {
|
||||
@@ -2407,8 +2352,6 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0;
|
||||
|
||||
printf(" Kind: %d\n", kind);
|
||||
|
||||
// Extract and log additional event details for debugging
|
||||
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
@@ -2570,7 +2513,6 @@ cJSON* create_admin_response_event(const char* encrypted_content, const char* re
|
||||
return NULL;
|
||||
}
|
||||
|
||||
log_info("Creating signed kind 23457 admin response event");
|
||||
printf(" Recipient pubkey: %.16s...\n", recipient_pubkey);
|
||||
printf(" Encrypted content length: %zu\n", strlen(encrypted_content));
|
||||
|
||||
@@ -2629,7 +2571,6 @@ cJSON* create_admin_response_event(const char* encrypted_content, const char* re
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(response_event, "pubkey");
|
||||
|
||||
if (id_obj && pubkey_obj) {
|
||||
log_success("Kind 23457 admin response event created and signed successfully");
|
||||
printf(" Event ID: %s\n", cJSON_GetStringValue(id_obj));
|
||||
printf(" Relay pubkey: %.16s...\n", cJSON_GetStringValue(pubkey_obj));
|
||||
}
|
||||
@@ -2644,17 +2585,15 @@ char* encrypt_admin_response_content(const cJSON* response_data, const char* rec
|
||||
return NULL;
|
||||
}
|
||||
|
||||
log_info("Encrypting admin response content with NIP-44");
|
||||
printf(" Recipient pubkey: %.16s...\n", recipient_pubkey);
|
||||
|
||||
|
||||
// Convert response data to JSON string
|
||||
char* response_json = cJSON_Print(response_data);
|
||||
if (!response_json) {
|
||||
log_error("Failed to serialize response data for encryption");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
log_info("Response data serialized for encryption");
|
||||
|
||||
printf(" JSON length: %zu\n", strlen(response_json));
|
||||
printf(" JSON preview: %.100s%s\n", response_json,
|
||||
strlen(response_json) > 100 ? "..." : "");
|
||||
@@ -2702,8 +2641,7 @@ char* encrypt_admin_response_content(const cJSON* response_data, const char* rec
|
||||
printf(" Encryption result code: %d\n", encrypt_result);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
log_success("Admin response content encrypted successfully with NIP-44");
|
||||
|
||||
printf(" Encrypted content length: %zu\n", strlen(encrypted_content));
|
||||
printf(" Encrypted preview: %.50s...\n", encrypted_content);
|
||||
|
||||
@@ -2720,7 +2658,6 @@ int send_admin_response_event(const cJSON* response_data, const char* recipient_
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Sending admin response as signed kind 23457 event through relay distribution system");
|
||||
printf(" Recipient pubkey: %.16s...\n", recipient_pubkey);
|
||||
|
||||
// Step 1: Encrypt response data using NIP-44
|
||||
@@ -2739,18 +2676,15 @@ int send_admin_response_event(const cJSON* response_data, const char* recipient_
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Admin response event created successfully");
|
||||
cJSON* id_obj = cJSON_GetObjectItem(response_event, "id");
|
||||
if (id_obj) {
|
||||
printf(" Event ID: %s\n", cJSON_GetStringValue(id_obj));
|
||||
}
|
||||
|
||||
|
||||
// Step 3: Store event in database for persistence
|
||||
extern int store_event(cJSON* event);
|
||||
if (store_event(response_event) != 0) {
|
||||
log_warning("Failed to store admin response event in database (continuing with broadcast)");
|
||||
} else {
|
||||
log_info("Admin response event stored in database successfully");
|
||||
}
|
||||
|
||||
// Step 4: Broadcast event to all matching subscriptions using relay's standard system
|
||||
@@ -2758,10 +2692,9 @@ int send_admin_response_event(const cJSON* response_data, const char* recipient_
|
||||
int broadcast_count = broadcast_event_to_subscriptions(response_event);
|
||||
|
||||
if (broadcast_count >= 0) {
|
||||
log_success("Admin response event distributed through relay subscription system");
|
||||
printf(" Event kind: 23457 (admin response)\n");
|
||||
printf(" Subscriptions notified: %d\n", broadcast_count);
|
||||
|
||||
|
||||
// Clean up and return success - event creation succeeded regardless of broadcast count
|
||||
cJSON_Delete(response_event);
|
||||
return 0;
|
||||
@@ -3072,9 +3005,6 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Processing unified auth query");
|
||||
printf(" Query type: %s\n", query_type);
|
||||
|
||||
const char* sql = NULL;
|
||||
int use_pattern_param = 0;
|
||||
char* pattern_value = NULL;
|
||||
@@ -3170,7 +3100,6 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
printf("Total results: %d\n", rule_count);
|
||||
log_success("Auth query completed successfully with signed response");
|
||||
printf(" Response query_type: %s (mapped from %s)\n", mapped_query_type, query_type);
|
||||
cJSON_Delete(response);
|
||||
cJSON_Delete(results_array);
|
||||
@@ -3193,9 +3122,6 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Processing unified config query");
|
||||
printf(" Query type: %s\n", query_type);
|
||||
|
||||
const char* sql = NULL;
|
||||
int use_pattern_param = 0;
|
||||
char* pattern_value = NULL;
|
||||
@@ -3297,7 +3223,6 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
printf("Total results: %d\n", config_count);
|
||||
log_success("Config query completed successfully with signed response");
|
||||
printf(" Response query_type: %s (mapped from %s)\n", mapped_query_type, query_type);
|
||||
cJSON_Delete(response);
|
||||
cJSON_Delete(results_array);
|
||||
@@ -3320,10 +3245,6 @@ int handle_config_set_unified(cJSON* event, const char* config_key, const char*
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Processing unified config set command");
|
||||
printf(" Key: %s\n", config_key);
|
||||
printf(" Value: %s\n", config_value);
|
||||
|
||||
// Validate the configuration field before updating
|
||||
char validation_error[512];
|
||||
if (validate_config_field(config_key, config_value, validation_error, sizeof(validation_error)) != 0) {
|
||||
@@ -3387,7 +3308,6 @@ int handle_config_set_unified(cJSON* event, const char* config_key, const char*
|
||||
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("Config set command completed successfully with signed response");
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
@@ -3406,9 +3326,6 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Processing unified system command");
|
||||
printf(" Command: %s\n", command);
|
||||
|
||||
if (strcmp(command, "clear_all_auth_rules") == 0) {
|
||||
// Count existing rules first
|
||||
const char* count_sql = "SELECT COUNT(*) FROM auth_rules";
|
||||
@@ -3456,7 +3373,6 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("Clear auth rules command completed successfully with signed response");
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
@@ -3535,7 +3451,6 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("Delete auth rule command completed successfully with signed response");
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
@@ -3597,7 +3512,6 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("System status query completed successfully with signed response");
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
@@ -3630,11 +3544,8 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
|
||||
// Send acknowledgment response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("Restart acknowledgment sent successfully - initiating shutdown");
|
||||
|
||||
// Trigger graceful shutdown by setting the global shutdown flag
|
||||
g_shutdown_flag = 1;
|
||||
log_info("Shutdown flag set - relay will restart gracefully");
|
||||
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
@@ -3748,7 +3659,6 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz
|
||||
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("Auth rule modification completed successfully with signed response");
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
@@ -3772,9 +3682,6 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Processing unified stats query");
|
||||
printf(" Query type: stats_query\n");
|
||||
|
||||
// Build response with database statistics
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "query_type", "stats_query");
|
||||
@@ -3886,7 +3793,6 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
|
||||
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("Stats query completed successfully with signed response");
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
@@ -4009,11 +3915,6 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error
|
||||
const char* category = category_obj && cJSON_IsString(category_obj) ?
|
||||
cJSON_GetStringValue(category_obj) : "general";
|
||||
|
||||
log_info("Processing config object");
|
||||
printf(" Key: %s\n", key);
|
||||
printf(" Value: %s\n", value);
|
||||
printf(" Data type: %s\n", data_type);
|
||||
printf(" Category: %s\n", category);
|
||||
|
||||
// Validate the configuration field before updating
|
||||
char validation_error[512];
|
||||
@@ -4269,7 +4170,6 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error
|
||||
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("Config update command completed successfully with signed response");
|
||||
printf(" Response query_type: config_update\n");
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
@@ -4571,7 +4471,6 @@ int process_startup_config_event(const cJSON* event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Processing startup configuration event through admin API...");
|
||||
|
||||
// Validate event structure first
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <cjson/cJSON.h>
|
||||
#include <libwebsockets.h>
|
||||
|
||||
@@ -257,9 +258,7 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
|
||||
snprintf(error_message, error_size, "invalid: unknown DM command type '%s'", command_type);
|
||||
}
|
||||
|
||||
if (result == 0) {
|
||||
log_success("DM Admin: Command processed successfully");
|
||||
} else {
|
||||
if (result != 0) {
|
||||
log_error("DM Admin: Command processing failed");
|
||||
}
|
||||
|
||||
@@ -579,7 +578,6 @@ void cleanup_expired_pending_changes(void) {
|
||||
while (current) {
|
||||
pending_config_change_t* next = current->next;
|
||||
if (now - current->timestamp > CONFIG_CHANGE_TIMEOUT) {
|
||||
log_info("Cleaning up expired config change request");
|
||||
remove_pending_change(current);
|
||||
}
|
||||
current = next;
|
||||
@@ -650,9 +648,6 @@ int apply_config_change(const char* key, const char* value) {
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
char log_msg[512];
|
||||
snprintf(log_msg, sizeof(log_msg), "Configuration updated: %s = %s", key, normalized_value);
|
||||
log_success(log_msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -912,8 +907,6 @@ char* generate_stats_json(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
log_info("Generating stats JSON from database");
|
||||
|
||||
// Build response with database statistics
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "query_type", "stats_query");
|
||||
@@ -1013,9 +1006,7 @@ char* generate_stats_json(void) {
|
||||
char* json_string = cJSON_Print(response);
|
||||
cJSON_Delete(response);
|
||||
|
||||
if (json_string) {
|
||||
log_success("Stats JSON generated successfully");
|
||||
} else {
|
||||
if (!json_string) {
|
||||
log_error("Failed to generate stats JSON");
|
||||
}
|
||||
|
||||
@@ -1096,7 +1087,6 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content,
|
||||
strcmp(cJSON_GetStringValue(tag_name), "p") == 0) {
|
||||
// Replace the p tag value with the correct user pubkey
|
||||
cJSON_ReplaceItemInArray(tag, 1, cJSON_CreateString(sender_pubkey));
|
||||
log_info("NIP-17: Fixed p tag in response gift wrap");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1113,11 +1103,7 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content,
|
||||
}
|
||||
|
||||
// Broadcast the response event to active subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(gift_wraps[0]);
|
||||
char debug_broadcast_msg[128];
|
||||
snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
|
||||
"NIP-17: Response broadcast to %d subscriptions", broadcast_count);
|
||||
log_info(debug_broadcast_msg);
|
||||
broadcast_event_to_subscriptions(gift_wraps[0]);
|
||||
|
||||
cJSON_Delete(gift_wraps[0]);
|
||||
return 0;
|
||||
@@ -1367,7 +1353,6 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
|
||||
free(relay_privkey_hex);
|
||||
|
||||
// Step 3: Decrypt and parse inner event using library function
|
||||
log_info("NIP-17: Attempting to decrypt gift wrap with nostr_nip17_receive_dm");
|
||||
cJSON* inner_dm = nostr_nip17_receive_dm(gift_wrap_event, relay_privkey);
|
||||
if (!inner_dm) {
|
||||
log_error("NIP-17: nostr_nip17_receive_dm returned NULL");
|
||||
@@ -1385,14 +1370,10 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
|
||||
sprintf(privkey_hex + (i * 2), "%02x", relay_privkey[i]);
|
||||
}
|
||||
privkey_hex[64] = '\0';
|
||||
char privkey_msg[128];
|
||||
snprintf(privkey_msg, sizeof(privkey_msg), "NIP-17: Using relay private key: %.16s...", privkey_hex);
|
||||
log_info(privkey_msg);
|
||||
|
||||
strncpy(error_message, "NIP-17: Failed to decrypt and parse inner DM event", error_size - 1);
|
||||
return NULL;
|
||||
}
|
||||
log_info("NIP-17: Successfully decrypted gift wrap");
|
||||
|
||||
// Step 4: Process admin command
|
||||
int result = process_nip17_admin_command(inner_dm, error_message, error_size, wsi);
|
||||
@@ -1422,7 +1403,6 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
|
||||
// If it's a plain text stats or config command, don't create additional response
|
||||
if (strstr(content_lower, "stats") != NULL || strstr(content_lower, "statistics") != NULL ||
|
||||
strstr(content_lower, "config") != NULL || strstr(content_lower, "configuration") != NULL) {
|
||||
log_info("NIP-17: Plain text command already handled response, skipping generic response");
|
||||
cJSON_Delete(inner_dm);
|
||||
return NULL; // No additional response needed
|
||||
}
|
||||
@@ -1432,7 +1412,6 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
|
||||
if (command_array && cJSON_IsArray(command_array) && cJSON_GetArraySize(command_array) > 0) {
|
||||
cJSON* first_item = cJSON_GetArrayItem(command_array, 0);
|
||||
if (cJSON_IsString(first_item) && strcmp(cJSON_GetStringValue(first_item), "stats") == 0) {
|
||||
log_info("NIP-17: JSON stats command already handled response, skipping generic response");
|
||||
cJSON_Delete(command_array);
|
||||
cJSON_Delete(inner_dm);
|
||||
return NULL; // No additional response needed
|
||||
@@ -1442,7 +1421,6 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
|
||||
}
|
||||
} else if (result > 0) {
|
||||
// Command was handled and response was sent, don't create generic response
|
||||
log_info("NIP-17: Command handled with custom response, skipping generic response");
|
||||
cJSON_Delete(inner_dm);
|
||||
return NULL;
|
||||
|
||||
@@ -1588,7 +1566,6 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
// Check if sender is admin before processing any commands
|
||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey");
|
||||
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
|
||||
log_info("NIP-17: DM missing sender pubkey, treating as user DM");
|
||||
return 0; // Not an error, just treat as user DM
|
||||
}
|
||||
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
||||
@@ -1597,11 +1574,6 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
const char* admin_pubkey = get_admin_pubkey_cached();
|
||||
int is_admin = admin_pubkey && strlen(admin_pubkey) > 0 && strcmp(sender_pubkey, admin_pubkey) == 0;
|
||||
|
||||
log_info("NIP-17: Processing admin command from DM content");
|
||||
char log_msg[256];
|
||||
snprintf(log_msg, sizeof(log_msg), "NIP-17: Received DM content: '%.50s'%s", dm_content, strlen(dm_content) > 50 ? "..." : "");
|
||||
log_info(log_msg);
|
||||
|
||||
// Parse DM content as JSON array of commands
|
||||
cJSON* command_array = cJSON_Parse(dm_content);
|
||||
if (!command_array || !cJSON_IsArray(command_array)) {
|
||||
@@ -1623,9 +1595,6 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
|
||||
// Check for stats commands
|
||||
if (strstr(content_lower, "stats") != NULL || strstr(content_lower, "statistics") != NULL) {
|
||||
log_info("NIP-17: Recognized plain text 'stats' command from admin");
|
||||
log_info("NIP-17: Action: Generate and send relay statistics");
|
||||
|
||||
char* stats_text = generate_stats_text();
|
||||
if (!stats_text) {
|
||||
return -1;
|
||||
@@ -1639,15 +1608,11 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
log_error(error_msg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_success("NIP-17: Stats command processed successfully, response sent");
|
||||
|
||||
return 0;
|
||||
}
|
||||
// Check for config commands
|
||||
else if (strstr(content_lower, "config") != NULL || strstr(content_lower, "configuration") != NULL) {
|
||||
log_info("NIP-17: Recognized plain text 'config' command from admin");
|
||||
log_info("NIP-17: Action: Generate and send relay configuration");
|
||||
|
||||
char* config_text = generate_config_text();
|
||||
if (!config_text) {
|
||||
return -1;
|
||||
@@ -1661,8 +1626,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
log_error(error_msg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_success("NIP-17: Config command processed successfully, response sent");
|
||||
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
@@ -1670,7 +1634,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
int confirmation_result = handle_config_confirmation(sender_pubkey, dm_content);
|
||||
if (confirmation_result != 0) {
|
||||
if (confirmation_result > 0) {
|
||||
log_success("NIP-17: Configuration confirmation processed successfully");
|
||||
// Configuration confirmation processed successfully
|
||||
} else if (confirmation_result == -2) {
|
||||
// No pending changes
|
||||
char no_pending_msg[256];
|
||||
@@ -1691,7 +1655,6 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
int config_result = process_config_change_request(sender_pubkey, dm_content);
|
||||
if (config_result != 0) {
|
||||
if (config_result > 0) {
|
||||
log_success("NIP-17: Configuration change request processed successfully");
|
||||
return 1; // Return positive value to indicate response was handled
|
||||
} else {
|
||||
log_error("NIP-17: Configuration change request failed");
|
||||
@@ -1699,12 +1662,10 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
}
|
||||
}
|
||||
|
||||
log_info("NIP-17: Plain text content from admin not recognized as command, treating as user DM");
|
||||
return 0; // Admin sent unrecognized plain text, treat as user DM
|
||||
}
|
||||
} else {
|
||||
// Not admin, treat as user DM
|
||||
log_info("NIP-17: Content is not JSON array and sender is not admin, treating as user DM");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1713,8 +1674,6 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
if (cJSON_GetArraySize(command_array) > 0) {
|
||||
cJSON* first_item = cJSON_GetArrayItem(command_array, 0);
|
||||
if (cJSON_IsString(first_item) && strcmp(cJSON_GetStringValue(first_item), "stats") == 0) {
|
||||
log_info("NIP-17: Processing 'stats' command directly");
|
||||
|
||||
// Get sender pubkey for response
|
||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey");
|
||||
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
|
||||
@@ -1742,8 +1701,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
strncpy(error_message, error_msg, error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_success("NIP-17: Stats command processed successfully");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
325
src/main.c
325
src/main.c
@@ -126,6 +126,22 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro
|
||||
// Forward declaration for NIP-45 COUNT message handling
|
||||
int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
|
||||
|
||||
// Parameter binding helpers for SQL queries
|
||||
static void add_bind_param(char*** params, int* count, int* capacity, const char* value) {
|
||||
if (*count >= *capacity) {
|
||||
*capacity = *capacity == 0 ? 16 : *capacity * 2;
|
||||
*params = realloc(*params, *capacity * sizeof(char*));
|
||||
}
|
||||
(*params)[(*count)++] = strdup(value);
|
||||
}
|
||||
|
||||
static void free_bind_params(char** params, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(params[i]);
|
||||
}
|
||||
free(params);
|
||||
}
|
||||
|
||||
// Forward declaration for enhanced admin event authorization
|
||||
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
@@ -219,13 +235,11 @@ void update_subscription_manager_config(void) {
|
||||
"Subscription limits: max_per_client=%d, max_total=%d",
|
||||
g_subscription_manager.max_subscriptions_per_client,
|
||||
g_subscription_manager.max_total_subscriptions);
|
||||
log_info(config_msg);
|
||||
}
|
||||
|
||||
// Signal handler for graceful shutdown
|
||||
void signal_handler(int sig) {
|
||||
if (sig == SIGINT || sig == SIGTERM) {
|
||||
log_info("Received shutdown signal");
|
||||
g_server_running = 0;
|
||||
}
|
||||
}
|
||||
@@ -287,10 +301,6 @@ int init_database(const char* database_path_override) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char success_msg[256];
|
||||
snprintf(success_msg, sizeof(success_msg), "Database connection established: %s", db_path);
|
||||
log_success(success_msg);
|
||||
|
||||
// Check if database is already initialized by looking for the events table
|
||||
const char* check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='events'";
|
||||
sqlite3_stmt* check_stmt;
|
||||
@@ -367,7 +377,6 @@ int init_database(const char* database_path_override) {
|
||||
if (error_msg) sqlite3_free(error_msg);
|
||||
return -1;
|
||||
}
|
||||
log_success("Created auth_rules table");
|
||||
|
||||
// Add indexes for auth_rules table
|
||||
const char* create_auth_rules_indexes_sql =
|
||||
@@ -385,7 +394,6 @@ int init_database(const char* database_path_override) {
|
||||
if (index_error_msg) sqlite3_free(index_error_msg);
|
||||
return -1;
|
||||
}
|
||||
log_success("Created auth_rules indexes");
|
||||
} else {
|
||||
// auth_rules table already exists, skipping creation
|
||||
}
|
||||
@@ -406,7 +414,6 @@ int init_database(const char* database_path_override) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_success("Database migration to v6 completed successfully");
|
||||
}
|
||||
} else {
|
||||
// Initialize database schema using embedded SQL
|
||||
@@ -425,7 +432,6 @@ int init_database(const char* database_path_override) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_success("Database schema initialized successfully");
|
||||
}
|
||||
} else {
|
||||
log_error("Failed to check existing database schema");
|
||||
@@ -440,7 +446,6 @@ void close_database() {
|
||||
if (g_db) {
|
||||
sqlite3_close(g_db);
|
||||
g_db = NULL;
|
||||
log_info("Database connection closed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,7 +676,6 @@ int store_event(cJSON* event) {
|
||||
}
|
||||
|
||||
free(tags_json);
|
||||
log_success("Event stored in database");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -738,7 +742,95 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
log_error("REQ filters is not an array");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// EARLY SUBSCRIPTION LIMIT CHECK - Check limits BEFORE any processing
|
||||
if (pss) {
|
||||
time_t current_time = time(NULL);
|
||||
|
||||
// Check if client is currently rate limited due to excessive failed attempts
|
||||
if (pss->rate_limit_until > current_time) {
|
||||
char rate_limit_msg[256];
|
||||
int remaining_seconds = (int)(pss->rate_limit_until - current_time);
|
||||
snprintf(rate_limit_msg, sizeof(rate_limit_msg),
|
||||
"Rate limited due to excessive failed subscription attempts. Try again in %d seconds.", remaining_seconds);
|
||||
|
||||
// Send CLOSED notice for rate limiting
|
||||
cJSON* closed_msg = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED"));
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id));
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: rate limited"));
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(rate_limit_msg));
|
||||
|
||||
char* closed_str = cJSON_Print(closed_msg);
|
||||
if (closed_str) {
|
||||
size_t closed_len = strlen(closed_str);
|
||||
unsigned char* buf = malloc(LWS_PRE + closed_len);
|
||||
if (buf) {
|
||||
memcpy(buf + LWS_PRE, closed_str, closed_len);
|
||||
lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT);
|
||||
free(buf);
|
||||
}
|
||||
free(closed_str);
|
||||
}
|
||||
cJSON_Delete(closed_msg);
|
||||
|
||||
// Update rate limiting counters
|
||||
pss->failed_subscription_attempts++;
|
||||
pss->last_failed_attempt = current_time;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check session subscription limits
|
||||
if (pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) {
|
||||
log_error("Maximum subscriptions per client exceeded");
|
||||
|
||||
// Update rate limiting counters for failed attempt
|
||||
pss->failed_subscription_attempts++;
|
||||
pss->last_failed_attempt = current_time;
|
||||
pss->consecutive_failures++;
|
||||
|
||||
// Implement progressive backoff: 1s, 5s, 30s, 300s (5min) based on consecutive failures
|
||||
int backoff_seconds = 1;
|
||||
if (pss->consecutive_failures >= 10) backoff_seconds = 300; // 5 minutes
|
||||
else if (pss->consecutive_failures >= 5) backoff_seconds = 30; // 30 seconds
|
||||
else if (pss->consecutive_failures >= 3) backoff_seconds = 5; // 5 seconds
|
||||
|
||||
pss->rate_limit_until = current_time + backoff_seconds;
|
||||
|
||||
// Send CLOSED notice with backoff information
|
||||
cJSON* closed_msg = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED"));
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id));
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: too many subscriptions"));
|
||||
|
||||
char backoff_msg[256];
|
||||
snprintf(backoff_msg, sizeof(backoff_msg),
|
||||
"Maximum subscriptions per client exceeded. Backoff for %d seconds.", backoff_seconds);
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(backoff_msg));
|
||||
|
||||
char* closed_str = cJSON_Print(closed_msg);
|
||||
if (closed_str) {
|
||||
size_t closed_len = strlen(closed_str);
|
||||
unsigned char* buf = malloc(LWS_PRE + closed_len);
|
||||
if (buf) {
|
||||
memcpy(buf + LWS_PRE, closed_str, closed_len);
|
||||
lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT);
|
||||
free(buf);
|
||||
}
|
||||
free(closed_str);
|
||||
}
|
||||
cJSON_Delete(closed_msg);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Parameter binding helpers
|
||||
char** bind_params = NULL;
|
||||
int bind_param_count = 0;
|
||||
int bind_param_capacity = 0;
|
||||
|
||||
// Check for kind 33334 configuration event requests BEFORE creating subscription
|
||||
int config_events_sent = 0;
|
||||
int has_config_request = 0;
|
||||
@@ -782,32 +874,6 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
// If only config events were requested, we can return early after sending EOSE
|
||||
// But still create the subscription for future config updates
|
||||
|
||||
// Check session subscription limits
|
||||
if (pss && pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) {
|
||||
log_error("Maximum subscriptions per client exceeded");
|
||||
|
||||
// Send CLOSED notice
|
||||
cJSON* closed_msg = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED"));
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id));
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: too many subscriptions"));
|
||||
|
||||
char* closed_str = cJSON_Print(closed_msg);
|
||||
if (closed_str) {
|
||||
size_t closed_len = strlen(closed_str);
|
||||
unsigned char* buf = malloc(LWS_PRE + closed_len);
|
||||
if (buf) {
|
||||
memcpy(buf + LWS_PRE, closed_str, closed_len);
|
||||
lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT);
|
||||
free(buf);
|
||||
}
|
||||
free(closed_str);
|
||||
}
|
||||
cJSON_Delete(closed_msg);
|
||||
|
||||
return has_config_request ? config_events_sent : 0;
|
||||
}
|
||||
|
||||
// Create persistent subscription
|
||||
subscription_t* subscription = create_subscription(sub_id, wsi, filters, pss ? pss->client_ip : "unknown");
|
||||
if (!subscription) {
|
||||
@@ -819,13 +885,13 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
if (add_subscription_to_manager(subscription) != 0) {
|
||||
log_error("Failed to add subscription to global manager");
|
||||
free_subscription(subscription);
|
||||
|
||||
|
||||
// Send CLOSED notice
|
||||
cJSON* closed_msg = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED"));
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id));
|
||||
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: subscription limit reached"));
|
||||
|
||||
|
||||
char* closed_str = cJSON_Print(closed_msg);
|
||||
if (closed_str) {
|
||||
size_t closed_len = strlen(closed_str);
|
||||
@@ -838,7 +904,15 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
free(closed_str);
|
||||
}
|
||||
cJSON_Delete(closed_msg);
|
||||
|
||||
|
||||
// Update rate limiting counters for failed attempt (global limit reached)
|
||||
if (pss) {
|
||||
time_t current_time = time(NULL);
|
||||
pss->failed_subscription_attempts++;
|
||||
pss->last_failed_attempt = current_time;
|
||||
pss->consecutive_failures++;
|
||||
}
|
||||
|
||||
return has_config_request ? config_events_sent : 0;
|
||||
}
|
||||
|
||||
@@ -860,7 +934,13 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
log_warning("Invalid filter object");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Reset bind params for this filter
|
||||
free_bind_params(bind_params, bind_param_count);
|
||||
bind_params = NULL;
|
||||
bind_param_count = 0;
|
||||
bind_param_capacity = 0;
|
||||
|
||||
// Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries
|
||||
char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
|
||||
char* sql_ptr = sql + strlen(sql);
|
||||
@@ -900,56 +980,80 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
// Handle authors filter
|
||||
cJSON* authors = cJSON_GetObjectItem(filter, "authors");
|
||||
if (authors && cJSON_IsArray(authors)) {
|
||||
int author_count = cJSON_GetArraySize(authors);
|
||||
int author_count = 0;
|
||||
// Count valid authors
|
||||
for (int a = 0; a < cJSON_GetArraySize(authors); a++) {
|
||||
cJSON* author = cJSON_GetArrayItem(authors, a);
|
||||
if (cJSON_IsString(author)) {
|
||||
author_count++;
|
||||
}
|
||||
}
|
||||
if (author_count > 0) {
|
||||
snprintf(sql_ptr, remaining, " AND pubkey IN (");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
|
||||
for (int a = 0; a < author_count; a++) {
|
||||
cJSON* author = cJSON_GetArrayItem(authors, a);
|
||||
if (cJSON_IsString(author)) {
|
||||
if (a > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(author));
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
if (a > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "?");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
}
|
||||
snprintf(sql_ptr, remaining, ")");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
|
||||
// Add author values to bind params
|
||||
for (int a = 0; a < cJSON_GetArraySize(authors); a++) {
|
||||
cJSON* author = cJSON_GetArrayItem(authors, a);
|
||||
if (cJSON_IsString(author)) {
|
||||
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(author));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ids filter
|
||||
cJSON* ids = cJSON_GetObjectItem(filter, "ids");
|
||||
if (ids && cJSON_IsArray(ids)) {
|
||||
int id_count = cJSON_GetArraySize(ids);
|
||||
int id_count = 0;
|
||||
// Count valid ids
|
||||
for (int i = 0; i < cJSON_GetArraySize(ids); i++) {
|
||||
cJSON* id = cJSON_GetArrayItem(ids, i);
|
||||
if (cJSON_IsString(id)) {
|
||||
id_count++;
|
||||
}
|
||||
}
|
||||
if (id_count > 0) {
|
||||
snprintf(sql_ptr, remaining, " AND id IN (");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
|
||||
for (int i = 0; i < id_count; i++) {
|
||||
cJSON* id = cJSON_GetArrayItem(ids, i);
|
||||
if (cJSON_IsString(id)) {
|
||||
if (i > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(id));
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
if (i > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "?");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
}
|
||||
snprintf(sql_ptr, remaining, ")");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
|
||||
// Add id values to bind params
|
||||
for (int i = 0; i < cJSON_GetArraySize(ids); i++) {
|
||||
cJSON* id = cJSON_GetArrayItem(ids, i);
|
||||
if (cJSON_IsString(id)) {
|
||||
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,29 +1066,42 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.)
|
||||
|
||||
if (cJSON_IsArray(filter_item)) {
|
||||
int tag_value_count = cJSON_GetArraySize(filter_item);
|
||||
int tag_value_count = 0;
|
||||
// Count valid tag values
|
||||
for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) {
|
||||
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
|
||||
if (cJSON_IsString(tag_value)) {
|
||||
tag_value_count++;
|
||||
}
|
||||
}
|
||||
if (tag_value_count > 0) {
|
||||
// Use EXISTS with LIKE to check for matching tags
|
||||
snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = '%s' AND json_extract(value, '$[1]') IN (", tag_name);
|
||||
// Use EXISTS with parameterized query
|
||||
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);
|
||||
|
||||
for (int i = 0; i < tag_value_count; i++) {
|
||||
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
|
||||
if (cJSON_IsString(tag_value)) {
|
||||
if (i > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(tag_value));
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
if (i > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "?");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "))");
|
||||
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 i = 0; i < cJSON_GetArraySize(filter_item); i++) {
|
||||
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
|
||||
if (cJSON_IsString(tag_value)) {
|
||||
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(tag_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1060,6 +1177,11 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
log_error(error_msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bind parameters
|
||||
for (int i = 0; i < bind_param_count; i++) {
|
||||
sqlite3_bind_text(stmt, i + 1, bind_params[i], -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
int row_count = 0;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
@@ -1124,7 +1246,10 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
|
||||
// Cleanup bind params
|
||||
free_bind_params(bind_params, bind_param_count);
|
||||
|
||||
return events_sent;
|
||||
}
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -1159,7 +1284,6 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf
|
||||
cJSON *tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
// No tags array - treat as regular event for different relay
|
||||
log_info("Admin event has no tags array - treating as event for different relay");
|
||||
snprintf(error_buffer, error_buffer_size, "Admin event not targeting this relay (no tags)");
|
||||
return -1;
|
||||
}
|
||||
@@ -1226,7 +1350,6 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf
|
||||
}
|
||||
|
||||
// All checks passed - authorized admin event
|
||||
log_info("Admin event authorization successful");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1323,7 +1446,6 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
char port_msg[128];
|
||||
snprintf(port_msg, sizeof(port_msg), "Port override specified: %d", cli_options.port_override);
|
||||
log_info(port_msg);
|
||||
} else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--admin-pubkey") == 0) {
|
||||
// Admin public key override option
|
||||
if (i + 1 >= argc) {
|
||||
@@ -1354,7 +1476,6 @@ int main(int argc, char* argv[]) {
|
||||
cli_options.admin_pubkey_override[sizeof(cli_options.admin_pubkey_override) - 1] = '\0';
|
||||
i++; // Skip the key argument
|
||||
|
||||
log_info("Admin public key override specified");
|
||||
} else if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--relay-privkey") == 0) {
|
||||
// Relay private key override option
|
||||
if (i + 1 >= argc) {
|
||||
@@ -1385,11 +1506,9 @@ int main(int argc, char* argv[]) {
|
||||
cli_options.relay_privkey_override[sizeof(cli_options.relay_privkey_override) - 1] = '\0';
|
||||
i++; // Skip the key argument
|
||||
|
||||
log_info("Relay private key override specified");
|
||||
} else if (strcmp(argv[i], "--strict-port") == 0) {
|
||||
// Strict port mode option
|
||||
cli_options.strict_port = 1;
|
||||
log_info("Strict port mode enabled - will fail if exact port is unavailable");
|
||||
} else {
|
||||
log_error("Unknown argument. Use --help for usage information.");
|
||||
print_usage(argv[0]);
|
||||
@@ -1412,7 +1531,6 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
// Check if this is first-time startup or existing relay
|
||||
if (is_first_time_startup()) {
|
||||
log_info("First-time startup detected");
|
||||
|
||||
// Initialize event-based configuration system
|
||||
if (init_configuration_system(NULL, NULL) != 0) {
|
||||
@@ -1446,7 +1564,6 @@ int main(int argc, char* argv[]) {
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
log_success("Relay private key stored securely in database");
|
||||
} else {
|
||||
log_error("Relay private key not available from first-time startup");
|
||||
cleanup_configuration_system();
|
||||
@@ -1477,7 +1594,6 @@ int main(int argc, char* argv[]) {
|
||||
close_database();
|
||||
return 1;
|
||||
}
|
||||
log_info("Applied port override from command line");
|
||||
printf(" Port: %d (overriding default)\n", cli_options.port_override);
|
||||
}
|
||||
|
||||
@@ -1490,18 +1606,15 @@ int main(int argc, char* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
log_success("Configuration populated directly in config table after database initialization");
|
||||
|
||||
// Now store the pubkeys in config table since database is available
|
||||
const char* admin_pubkey = get_admin_pubkey_cached();
|
||||
const char* relay_pubkey_from_cache = get_relay_pubkey_cached();
|
||||
if (admin_pubkey && strlen(admin_pubkey) == 64) {
|
||||
set_config_value_in_table("admin_pubkey", admin_pubkey, "string", "Administrator public key", "authentication", 0);
|
||||
log_success("Admin pubkey stored in config table for first-time startup");
|
||||
}
|
||||
if (relay_pubkey_from_cache && strlen(relay_pubkey_from_cache) == 64) {
|
||||
set_config_value_in_table("relay_pubkey", relay_pubkey_from_cache, "string", "Relay public key", "relay", 0);
|
||||
log_success("Relay pubkey stored in config table for first-time startup");
|
||||
}
|
||||
} else {
|
||||
// Find existing database file
|
||||
@@ -1569,7 +1682,6 @@ int main(int argc, char* argv[]) {
|
||||
if (apply_configuration_from_event(config_event) != 0) {
|
||||
log_warning("Failed to apply configuration from database");
|
||||
} else {
|
||||
log_success("Configuration loaded from database");
|
||||
|
||||
// Extract admin pubkey from the config event and store in config table for unified cache access
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(config_event, "pubkey");
|
||||
@@ -1600,7 +1712,6 @@ int main(int argc, char* argv[]) {
|
||||
close_database();
|
||||
return 1;
|
||||
}
|
||||
log_info("Applied port override from command line for existing relay");
|
||||
printf(" Port: %d (overriding configured port)\n", cli_options.port_override);
|
||||
}
|
||||
|
||||
@@ -1631,7 +1742,6 @@ int main(int argc, char* argv[]) {
|
||||
close_database();
|
||||
return 1;
|
||||
}
|
||||
log_success("Unified request validator initialized");
|
||||
|
||||
// Initialize NIP-11 relay information
|
||||
init_relay_info();
|
||||
@@ -1641,11 +1751,28 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
// Initialize NIP-40 expiration configuration
|
||||
init_expiration_config();
|
||||
|
||||
// Update subscription manager configuration
|
||||
update_subscription_manager_config();
|
||||
|
||||
// Initialize subscription manager mutexes
|
||||
if (pthread_mutex_init(&g_subscription_manager.subscriptions_lock, NULL) != 0) {
|
||||
log_error("Failed to initialize subscription manager subscriptions lock");
|
||||
cleanup_configuration_system();
|
||||
nostr_cleanup();
|
||||
close_database();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (pthread_mutex_init(&g_subscription_manager.ip_tracking_lock, NULL) != 0) {
|
||||
log_error("Failed to initialize subscription manager IP tracking lock");
|
||||
pthread_mutex_destroy(&g_subscription_manager.subscriptions_lock);
|
||||
cleanup_configuration_system();
|
||||
nostr_cleanup();
|
||||
close_database();
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
log_info("Starting relay server...");
|
||||
|
||||
// Start WebSocket Nostr relay server (port from configuration)
|
||||
int result = start_websocket_relay(-1, cli_options.strict_port); // Let config system determine port, pass strict_port flag
|
||||
@@ -1654,11 +1781,15 @@ int main(int argc, char* argv[]) {
|
||||
cleanup_relay_info();
|
||||
ginxsom_request_validator_cleanup();
|
||||
cleanup_configuration_system();
|
||||
|
||||
// Cleanup subscription manager mutexes
|
||||
pthread_mutex_destroy(&g_subscription_manager.subscriptions_lock);
|
||||
pthread_mutex_destroy(&g_subscription_manager.ip_tracking_lock);
|
||||
|
||||
nostr_cleanup();
|
||||
close_database();
|
||||
|
||||
if (result == 0) {
|
||||
log_success("Server shutdown complete");
|
||||
} else {
|
||||
log_error("Server shutdown with errors");
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <stdio.h>
|
||||
#include <printf.h>
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_warning(const char* message);
|
||||
|
||||
@@ -224,8 +224,6 @@ void init_relay_info() {
|
||||
g_unified_cache.relay_info.fees = cJSON_CreateObject();
|
||||
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
|
||||
log_success("Relay information initialized with default values");
|
||||
}
|
||||
|
||||
// Clean up relay information JSON objects
|
||||
|
||||
@@ -148,11 +148,6 @@ void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* ps
|
||||
pss->auth_challenge_sent = 0;
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
|
||||
char success_msg[256];
|
||||
snprintf(success_msg, sizeof(success_msg),
|
||||
"NIP-42 authentication successful for pubkey: %.16s...", authenticated_pubkey);
|
||||
log_success(success_msg);
|
||||
|
||||
send_notice_message(wsi, "NIP-42 authentication successful");
|
||||
} else {
|
||||
// Authentication failed
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <stdio.h>
|
||||
#include <printf.h>
|
||||
#include <pthread.h>
|
||||
#include <libwebsockets.h>
|
||||
#include "subscriptions.h"
|
||||
@@ -21,6 +20,13 @@ const char* get_config_value(const char* key);
|
||||
// Forward declarations for NIP-40 expiration functions
|
||||
int is_event_expired(cJSON* event, time_t current_time);
|
||||
|
||||
// Forward declarations for filter validation
|
||||
int validate_filter_values(cJSON* filter_json, char* error_message, size_t error_size);
|
||||
int validate_hex_string(const char* str, size_t expected_len, const char* field_name, char* error_message, size_t error_size);
|
||||
int validate_timestamp_range(long since, long until, char* error_message, size_t error_size);
|
||||
int validate_numeric_limits(int limit, char* error_message, size_t error_size);
|
||||
int validate_search_term(const char* search_term, char* error_message, size_t error_size);
|
||||
|
||||
// Global database variable
|
||||
extern sqlite3* g_db;
|
||||
|
||||
@@ -42,7 +48,14 @@ subscription_filter_t* create_subscription_filter(cJSON* filter_json) {
|
||||
if (!filter_json || !cJSON_IsObject(filter_json)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Validate filter values before creating the filter
|
||||
char error_message[512] = {0};
|
||||
if (!validate_filter_values(filter_json, error_message, sizeof(error_message))) {
|
||||
log_warning(error_message);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
subscription_filter_t* filter = calloc(1, sizeof(subscription_filter_t));
|
||||
if (!filter) {
|
||||
return NULL;
|
||||
@@ -111,28 +124,66 @@ void free_subscription_filter(subscription_filter_t* filter) {
|
||||
free(filter);
|
||||
}
|
||||
|
||||
// Validate subscription ID format and length
|
||||
static int validate_subscription_id(const char* sub_id) {
|
||||
if (!sub_id) {
|
||||
return 0; // NULL pointer
|
||||
}
|
||||
|
||||
size_t len = strlen(sub_id);
|
||||
if (len == 0 || len >= SUBSCRIPTION_ID_MAX_LENGTH) {
|
||||
return 0; // Empty or too long
|
||||
}
|
||||
|
||||
// Check for valid characters (alphanumeric, underscore, hyphen)
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = sub_id[i];
|
||||
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-')) {
|
||||
return 0; // Invalid character
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // Valid
|
||||
}
|
||||
|
||||
// Create a new subscription
|
||||
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) {
|
||||
if (!sub_id || !wsi || !filters_array) {
|
||||
log_error("create_subscription: NULL parameter(s)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Validate subscription ID
|
||||
if (!validate_subscription_id(sub_id)) {
|
||||
log_error("create_subscription: invalid subscription ID format or length");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
subscription_t* sub = calloc(1, sizeof(subscription_t));
|
||||
if (!sub) {
|
||||
log_error("create_subscription: failed to allocate subscription");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Copy subscription ID (truncate if too long)
|
||||
strncpy(sub->id, sub_id, SUBSCRIPTION_ID_MAX_LENGTH - 1);
|
||||
sub->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
|
||||
|
||||
// Copy subscription ID safely (already validated)
|
||||
size_t id_len = strlen(sub_id);
|
||||
memcpy(sub->id, sub_id, id_len);
|
||||
sub->id[id_len] = '\0';
|
||||
|
||||
// Set WebSocket connection
|
||||
sub->wsi = wsi;
|
||||
|
||||
// Set client IP
|
||||
// Set client IP safely
|
||||
if (client_ip) {
|
||||
strncpy(sub->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1);
|
||||
sub->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
||||
size_t ip_len = strlen(client_ip);
|
||||
if (ip_len >= CLIENT_IP_MAX_LENGTH) {
|
||||
ip_len = CLIENT_IP_MAX_LENGTH - 1;
|
||||
}
|
||||
memcpy(sub->client_ip, client_ip, ip_len);
|
||||
sub->client_ip[ip_len] = '\0';
|
||||
} else {
|
||||
sub->client_ip[0] = '\0'; // Ensure null termination
|
||||
}
|
||||
|
||||
// Set timestamps and state
|
||||
@@ -215,42 +266,61 @@ int add_subscription_to_manager(subscription_t* sub) {
|
||||
|
||||
// Remove subscription from global manager (thread-safe)
|
||||
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
|
||||
if (!sub_id) return -1;
|
||||
|
||||
if (!sub_id) {
|
||||
log_error("remove_subscription_from_manager: NULL subscription ID");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate subscription ID format
|
||||
if (!validate_subscription_id(sub_id)) {
|
||||
log_error("remove_subscription_from_manager: invalid subscription ID format");
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
|
||||
subscription_t** current = &g_subscription_manager.active_subscriptions;
|
||||
|
||||
|
||||
while (*current) {
|
||||
subscription_t* sub = *current;
|
||||
|
||||
|
||||
// Match by ID and WebSocket connection
|
||||
if (strcmp(sub->id, sub_id) == 0 && (!wsi || sub->wsi == wsi)) {
|
||||
// Remove from list
|
||||
*current = sub->next;
|
||||
g_subscription_manager.total_subscriptions--;
|
||||
|
||||
|
||||
// Copy data needed for logging before unlocking
|
||||
char client_ip_copy[CLIENT_IP_MAX_LENGTH];
|
||||
int events_sent_copy = sub->events_sent;
|
||||
char sub_id_copy[SUBSCRIPTION_ID_MAX_LENGTH];
|
||||
|
||||
memcpy(client_ip_copy, sub->client_ip, CLIENT_IP_MAX_LENGTH);
|
||||
memcpy(sub_id_copy, sub->id, SUBSCRIPTION_ID_MAX_LENGTH);
|
||||
client_ip_copy[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
||||
sub_id_copy[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
// Log subscription closure to database
|
||||
log_subscription_closed(sub_id, sub->client_ip, "closed");
|
||||
|
||||
|
||||
// Log subscription closure to database (now safe)
|
||||
log_subscription_closed(sub_id_copy, client_ip_copy, "closed");
|
||||
|
||||
// Update events sent counter before freeing
|
||||
update_subscription_events_sent(sub_id, sub->events_sent);
|
||||
update_subscription_events_sent(sub_id_copy, events_sent_copy);
|
||||
|
||||
free_subscription(sub);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
current = &(sub->next);
|
||||
}
|
||||
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
|
||||
char debug_msg[256];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id);
|
||||
log_warning(debug_msg);
|
||||
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -472,52 +542,117 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
}
|
||||
|
||||
int broadcasts = 0;
|
||||
|
||||
// Create a temporary list of matching subscriptions to avoid holding lock during I/O
|
||||
typedef struct temp_sub {
|
||||
struct lws* wsi;
|
||||
char id[SUBSCRIPTION_ID_MAX_LENGTH];
|
||||
char client_ip[CLIENT_IP_MAX_LENGTH];
|
||||
struct temp_sub* next;
|
||||
} temp_sub_t;
|
||||
|
||||
temp_sub_t* matching_subs = NULL;
|
||||
int matching_count = 0;
|
||||
|
||||
// First pass: collect matching subscriptions while holding lock
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||
while (sub) {
|
||||
if (sub->active && event_matches_subscription(event, sub)) {
|
||||
// Create EVENT message for this subscription
|
||||
cJSON* event_msg = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
|
||||
cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub->id));
|
||||
cJSON_AddItemToArray(event_msg, cJSON_Duplicate(event, 1));
|
||||
|
||||
char* msg_str = cJSON_Print(event_msg);
|
||||
if (msg_str) {
|
||||
size_t msg_len = strlen(msg_str);
|
||||
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
||||
if (buf) {
|
||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
||||
|
||||
// Send to WebSocket connection
|
||||
int write_result = lws_write(sub->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
||||
if (write_result >= 0) {
|
||||
sub->events_sent++;
|
||||
broadcasts++;
|
||||
|
||||
// Log event broadcast to database (optional - can be disabled for performance)
|
||||
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||
if (event_id_obj && cJSON_IsString(event_id_obj)) {
|
||||
log_event_broadcast(cJSON_GetStringValue(event_id_obj), sub->id, sub->client_ip);
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
if (sub->active && sub->wsi && event_matches_subscription(event, sub)) {
|
||||
temp_sub_t* temp = malloc(sizeof(temp_sub_t));
|
||||
if (temp) {
|
||||
temp->wsi = sub->wsi;
|
||||
|
||||
// Safely copy subscription ID
|
||||
size_t id_len = strlen(sub->id);
|
||||
if (id_len >= SUBSCRIPTION_ID_MAX_LENGTH) {
|
||||
id_len = SUBSCRIPTION_ID_MAX_LENGTH - 1;
|
||||
}
|
||||
free(msg_str);
|
||||
memcpy(temp->id, sub->id, id_len);
|
||||
temp->id[id_len] = '\0';
|
||||
|
||||
// Safely copy client IP
|
||||
size_t ip_len = strlen(sub->client_ip);
|
||||
if (ip_len >= CLIENT_IP_MAX_LENGTH) {
|
||||
ip_len = CLIENT_IP_MAX_LENGTH - 1;
|
||||
}
|
||||
memcpy(temp->client_ip, sub->client_ip, ip_len);
|
||||
temp->client_ip[ip_len] = '\0';
|
||||
|
||||
temp->next = matching_subs;
|
||||
matching_subs = temp;
|
||||
matching_count++;
|
||||
} else {
|
||||
log_error("broadcast_event_to_subscriptions: failed to allocate temp subscription");
|
||||
}
|
||||
|
||||
cJSON_Delete(event_msg);
|
||||
}
|
||||
sub = sub->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
// Second pass: send messages without holding lock
|
||||
temp_sub_t* current_temp = matching_subs;
|
||||
while (current_temp) {
|
||||
// Create EVENT message for this subscription
|
||||
cJSON* event_msg = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
|
||||
cJSON_AddItemToArray(event_msg, cJSON_CreateString(current_temp->id));
|
||||
cJSON_AddItemToArray(event_msg, cJSON_Duplicate(event, 1));
|
||||
|
||||
char* msg_str = cJSON_Print(event_msg);
|
||||
if (msg_str) {
|
||||
size_t msg_len = strlen(msg_str);
|
||||
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
||||
if (buf) {
|
||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
||||
|
||||
// Send to WebSocket connection with error checking
|
||||
// Note: lws_write can fail if connection is closed, but won't crash
|
||||
int write_result = lws_write(current_temp->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
||||
if (write_result >= 0) {
|
||||
broadcasts++;
|
||||
|
||||
// Update events sent counter for this subscription
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
subscription_t* update_sub = g_subscription_manager.active_subscriptions;
|
||||
while (update_sub) {
|
||||
if (update_sub->wsi == current_temp->wsi &&
|
||||
strcmp(update_sub->id, current_temp->id) == 0) {
|
||||
update_sub->events_sent++;
|
||||
break;
|
||||
}
|
||||
update_sub = update_sub->next;
|
||||
}
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
// Log event broadcast to database (optional - can be disabled for performance)
|
||||
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||
if (event_id_obj && cJSON_IsString(event_id_obj)) {
|
||||
log_event_broadcast(cJSON_GetStringValue(event_id_obj), current_temp->id, current_temp->client_ip);
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
}
|
||||
free(msg_str);
|
||||
}
|
||||
|
||||
sub = sub->next;
|
||||
cJSON_Delete(event_msg);
|
||||
current_temp = current_temp->next;
|
||||
}
|
||||
|
||||
// Clean up temporary subscription list
|
||||
while (matching_subs) {
|
||||
temp_sub_t* next = matching_subs->next;
|
||||
free(matching_subs);
|
||||
matching_subs = next;
|
||||
}
|
||||
|
||||
// Update global statistics
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
g_subscription_manager.total_events_broadcast += broadcasts;
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
return broadcasts;
|
||||
@@ -688,19 +823,476 @@ void log_event_broadcast(const char* event_id, const char* sub_id, const char* c
|
||||
// Update events sent counter for a subscription
|
||||
void update_subscription_events_sent(const char* sub_id, int events_sent) {
|
||||
if (!g_db || !sub_id) return;
|
||||
|
||||
|
||||
const char* sql =
|
||||
"UPDATE subscription_events "
|
||||
"SET events_sent = ? "
|
||||
"WHERE subscription_id = ? AND event_type = 'created'";
|
||||
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_int(stmt, 1, events_sent);
|
||||
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
|
||||
|
||||
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// PER-IP CONNECTION TRACKING
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Get or create IP connection info (thread-safe)
|
||||
ip_connection_info_t* get_or_create_ip_connection(const char* client_ip) {
|
||||
if (!client_ip) return NULL;
|
||||
|
||||
pthread_mutex_lock(&g_subscription_manager.ip_tracking_lock);
|
||||
|
||||
// Look for existing IP connection info
|
||||
ip_connection_info_t* current = g_subscription_manager.ip_connections;
|
||||
while (current) {
|
||||
if (strcmp(current->ip_address, client_ip) == 0) {
|
||||
// Found existing entry, update activity
|
||||
current->last_activity = time(NULL);
|
||||
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
||||
return current;
|
||||
}
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
// Create new IP connection info
|
||||
ip_connection_info_t* new_ip = calloc(1, sizeof(ip_connection_info_t));
|
||||
if (!new_ip) {
|
||||
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Copy IP address safely
|
||||
strncpy(new_ip->ip_address, client_ip, CLIENT_IP_MAX_LENGTH - 1);
|
||||
new_ip->ip_address[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
||||
|
||||
// Initialize tracking data
|
||||
time_t now = time(NULL);
|
||||
new_ip->active_connections = 1;
|
||||
new_ip->total_subscriptions = 0;
|
||||
new_ip->first_connection = now;
|
||||
new_ip->last_activity = now;
|
||||
|
||||
// Add to linked list
|
||||
new_ip->next = g_subscription_manager.ip_connections;
|
||||
g_subscription_manager.ip_connections = new_ip;
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
||||
return new_ip;
|
||||
}
|
||||
|
||||
// Update IP connection activity timestamp
|
||||
void update_ip_connection_activity(const char* client_ip) {
|
||||
if (!client_ip) return;
|
||||
|
||||
pthread_mutex_lock(&g_subscription_manager.ip_tracking_lock);
|
||||
|
||||
ip_connection_info_t* current = g_subscription_manager.ip_connections;
|
||||
while (current) {
|
||||
if (strcmp(current->ip_address, client_ip) == 0) {
|
||||
current->last_activity = time(NULL);
|
||||
break;
|
||||
}
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
||||
}
|
||||
|
||||
// Remove IP connection (when last connection from IP closes)
|
||||
void remove_ip_connection(const char* client_ip) {
|
||||
if (!client_ip) return;
|
||||
|
||||
pthread_mutex_lock(&g_subscription_manager.ip_tracking_lock);
|
||||
|
||||
ip_connection_info_t** current = &g_subscription_manager.ip_connections;
|
||||
while (*current) {
|
||||
ip_connection_info_t* entry = *current;
|
||||
if (strcmp(entry->ip_address, client_ip) == 0) {
|
||||
// Remove from list
|
||||
*current = entry->next;
|
||||
free(entry);
|
||||
break;
|
||||
}
|
||||
current = &((*current)->next);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
||||
}
|
||||
|
||||
// Get total subscriptions for an IP address
|
||||
int get_total_subscriptions_for_ip(const char* client_ip) {
|
||||
if (!client_ip) return 0;
|
||||
|
||||
pthread_mutex_lock(&g_subscription_manager.ip_tracking_lock);
|
||||
|
||||
ip_connection_info_t* current = g_subscription_manager.ip_connections;
|
||||
while (current) {
|
||||
if (strcmp(current->ip_address, client_ip) == 0) {
|
||||
int total = current->total_subscriptions;
|
||||
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
||||
return total;
|
||||
}
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get active connections for an IP address
|
||||
int get_active_connections_for_ip(const char* client_ip) {
|
||||
if (!client_ip) return 0;
|
||||
|
||||
pthread_mutex_lock(&g_subscription_manager.ip_tracking_lock);
|
||||
|
||||
ip_connection_info_t* current = g_subscription_manager.ip_connections;
|
||||
while (current) {
|
||||
if (strcmp(current->ip_address, client_ip) == 0) {
|
||||
int active = current->active_connections;
|
||||
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
||||
return active;
|
||||
}
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// FILTER VALIDATION FUNCTIONS
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Validate hex string format and length
|
||||
*/
|
||||
int validate_hex_string(const char* str, size_t expected_len, const char* field_name, char* error_message, size_t error_size) {
|
||||
if (!str) {
|
||||
snprintf(error_message, error_size, "%s: null value", field_name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t len = strlen(str);
|
||||
if (len != expected_len) {
|
||||
snprintf(error_message, error_size, "%s: invalid length %zu, expected %zu", field_name, len, expected_len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check for valid hex characters
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = str[i];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
||||
snprintf(error_message, error_size, "%s: invalid hex character '%c' at position %zu", field_name, c, i);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate timestamp range (since/until)
|
||||
*/
|
||||
int validate_timestamp_range(long since, long until, char* error_message, size_t error_size) {
|
||||
// Allow zero values (not set)
|
||||
if (since == 0 && until == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check for reasonable timestamp bounds (1970-01-01 to 2100-01-01)
|
||||
if (since != 0 && (since < MIN_TIMESTAMP || since > MAX_TIMESTAMP)) {
|
||||
snprintf(error_message, error_size, "since: timestamp %ld out of valid range", since);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (until != 0 && (until < MIN_TIMESTAMP || until > MAX_TIMESTAMP)) {
|
||||
snprintf(error_message, error_size, "until: timestamp %ld out of valid range", until);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check that since is before until if both are set
|
||||
if (since > 0 && until > 0 && since >= until) {
|
||||
snprintf(error_message, error_size, "since (%ld) must be before until (%ld)", since, until);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate numeric limits
|
||||
*/
|
||||
int validate_numeric_limits(int limit, char* error_message, size_t error_size) {
|
||||
// Allow zero (no limit)
|
||||
if (limit == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check for reasonable limits (1-10000)
|
||||
if (limit < MIN_LIMIT || limit > MAX_LIMIT) {
|
||||
snprintf(error_message, error_size, "limit: value %d out of valid range [%d, %d]", limit, MIN_LIMIT, MAX_LIMIT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate search term for SQL injection and length
|
||||
*/
|
||||
int validate_search_term(const char* search_term, char* error_message, size_t error_size) {
|
||||
if (!search_term) {
|
||||
return 1; // NULL search terms are allowed
|
||||
}
|
||||
|
||||
size_t len = strlen(search_term);
|
||||
|
||||
// Check maximum length
|
||||
if (len > MAX_SEARCH_TERM_LENGTH) {
|
||||
snprintf(error_message, error_size, "search: term too long (%zu characters, max %d)", len, (int)MAX_SEARCH_TERM_LENGTH);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check for potentially dangerous characters that could cause SQL issues
|
||||
// Allow alphanumeric, spaces, and common punctuation
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = search_term[i];
|
||||
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == ' ' || c == '-' || c == '_' ||
|
||||
c == '.' || c == ',' || c == '!' || c == '?' || c == ':' ||
|
||||
c == ';' || c == '"' || c == '\'' || c == '(' || c == ')' ||
|
||||
c == '[' || c == ']' || c == '{' || c == '}' || c == '@' ||
|
||||
c == '#' || c == '$' || c == '%' || c == '^' || c == '&' ||
|
||||
c == '*' || c == '+' || c == '=' || c == '|' || c == '\\' ||
|
||||
c == '/' || c == '<' || c == '>' || c == '~' || c == '`')) {
|
||||
// Reject control characters and other potentially problematic chars
|
||||
if (c < 32 || c == 127) {
|
||||
snprintf(error_message, error_size, "search: invalid character (ASCII %d) at position %zu", (int)c, i);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all filter values in a filter object
|
||||
*/
|
||||
int validate_filter_values(cJSON* filter_json, char* error_message, size_t error_size) {
|
||||
if (!filter_json || !cJSON_IsObject(filter_json)) {
|
||||
snprintf(error_message, error_size, "filter must be a JSON object");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate kinds array
|
||||
cJSON* kinds = cJSON_GetObjectItem(filter_json, "kinds");
|
||||
if (kinds) {
|
||||
if (!cJSON_IsArray(kinds)) {
|
||||
snprintf(error_message, error_size, "kinds must be an array");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int kinds_count = cJSON_GetArraySize(kinds);
|
||||
if (kinds_count > MAX_KINDS_PER_FILTER) {
|
||||
snprintf(error_message, error_size, "kinds array too large (%d items, max %d)", kinds_count, MAX_KINDS_PER_FILTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < kinds_count; i++) {
|
||||
cJSON* kind_item = cJSON_GetArrayItem(kinds, i);
|
||||
if (!cJSON_IsNumber(kind_item)) {
|
||||
snprintf(error_message, error_size, "kinds[%d] must be a number", i);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int kind_val = (int)cJSON_GetNumberValue(kind_item);
|
||||
if (kind_val < 0 || kind_val > 65535) { // Reasonable range for event kinds
|
||||
snprintf(error_message, error_size, "kinds[%d]: invalid event kind %d", i, kind_val);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate authors array
|
||||
cJSON* authors = cJSON_GetObjectItem(filter_json, "authors");
|
||||
if (authors) {
|
||||
if (!cJSON_IsArray(authors)) {
|
||||
snprintf(error_message, error_size, "authors must be an array");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int authors_count = cJSON_GetArraySize(authors);
|
||||
if (authors_count > MAX_AUTHORS_PER_FILTER) {
|
||||
snprintf(error_message, error_size, "authors array too large (%d items, max %d)", authors_count, MAX_AUTHORS_PER_FILTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < authors_count; i++) {
|
||||
cJSON* author_item = cJSON_GetArrayItem(authors, i);
|
||||
if (!cJSON_IsString(author_item)) {
|
||||
snprintf(error_message, error_size, "authors[%d] must be a string", i);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* author_str = cJSON_GetStringValue(author_item);
|
||||
// Allow partial pubkeys (prefix matching), so validate hex but allow shorter lengths
|
||||
size_t author_len = strlen(author_str);
|
||||
if (author_len == 0 || author_len > 64) {
|
||||
snprintf(error_message, error_size, "authors[%d]: invalid length %zu", i, author_len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate hex characters (allow partial)
|
||||
for (size_t j = 0; j < author_len; j++) {
|
||||
char c = author_str[j];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
||||
snprintf(error_message, error_size, "authors[%d]: invalid hex character '%c'", i, c);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate ids array
|
||||
cJSON* ids = cJSON_GetObjectItem(filter_json, "ids");
|
||||
if (ids) {
|
||||
if (!cJSON_IsArray(ids)) {
|
||||
snprintf(error_message, error_size, "ids must be an array");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ids_count = cJSON_GetArraySize(ids);
|
||||
if (ids_count > MAX_IDS_PER_FILTER) {
|
||||
snprintf(error_message, error_size, "ids array too large (%d items, max %d)", ids_count, MAX_IDS_PER_FILTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < ids_count; i++) {
|
||||
cJSON* id_item = cJSON_GetArrayItem(ids, i);
|
||||
if (!cJSON_IsString(id_item)) {
|
||||
snprintf(error_message, error_size, "ids[%d] must be a string", i);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* id_str = cJSON_GetStringValue(id_item);
|
||||
// Allow partial IDs (prefix matching)
|
||||
size_t id_len = strlen(id_str);
|
||||
if (id_len == 0 || id_len > 64) {
|
||||
snprintf(error_message, error_size, "ids[%d]: invalid length %zu", i, id_len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate hex characters
|
||||
for (size_t j = 0; j < id_len; j++) {
|
||||
char c = id_str[j];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
||||
snprintf(error_message, error_size, "ids[%d]: invalid hex character '%c'", i, c);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate since/until timestamps
|
||||
long since_val = 0, until_val = 0;
|
||||
|
||||
cJSON* since = cJSON_GetObjectItem(filter_json, "since");
|
||||
if (since) {
|
||||
if (!cJSON_IsNumber(since)) {
|
||||
snprintf(error_message, error_size, "since must be a number");
|
||||
return 0;
|
||||
}
|
||||
since_val = (long)cJSON_GetNumberValue(since);
|
||||
}
|
||||
|
||||
cJSON* until = cJSON_GetObjectItem(filter_json, "until");
|
||||
if (until) {
|
||||
if (!cJSON_IsNumber(until)) {
|
||||
snprintf(error_message, error_size, "until must be a number");
|
||||
return 0;
|
||||
}
|
||||
until_val = (long)cJSON_GetNumberValue(until);
|
||||
}
|
||||
|
||||
if (!validate_timestamp_range(since_val, until_val, error_message, error_size)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate limit
|
||||
cJSON* limit = cJSON_GetObjectItem(filter_json, "limit");
|
||||
if (limit) {
|
||||
if (!cJSON_IsNumber(limit)) {
|
||||
snprintf(error_message, error_size, "limit must be a number");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int limit_val = (int)cJSON_GetNumberValue(limit);
|
||||
if (!validate_numeric_limits(limit_val, error_message, error_size)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate search term
|
||||
cJSON* search = cJSON_GetObjectItem(filter_json, "search");
|
||||
if (search) {
|
||||
if (!cJSON_IsString(search)) {
|
||||
snprintf(error_message, error_size, "search must be a string");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* search_term = cJSON_GetStringValue(search);
|
||||
if (!validate_search_term(search_term, error_message, error_size)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate tag filters (#e, #p, #t, etc.)
|
||||
cJSON* item = NULL;
|
||||
cJSON_ArrayForEach(item, filter_json) {
|
||||
const char* key = item->string;
|
||||
if (key && strlen(key) >= 2 && key[0] == '#') {
|
||||
if (!cJSON_IsArray(item)) {
|
||||
snprintf(error_message, error_size, "%s must be an array", key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tag_count = cJSON_GetArraySize(item);
|
||||
if (tag_count > MAX_TAG_VALUES_PER_FILTER) {
|
||||
snprintf(error_message, error_size, "%s array too large (%d items, max %d)", key, tag_count, MAX_TAG_VALUES_PER_FILTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < tag_count; i++) {
|
||||
cJSON* tag_value = cJSON_GetArrayItem(item, i);
|
||||
if (!cJSON_IsString(tag_value)) {
|
||||
snprintf(error_message, error_size, "%s[%d] must be a string", key, i);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* tag_str = cJSON_GetStringValue(tag_value);
|
||||
size_t tag_len = strlen(tag_str);
|
||||
if (tag_len > MAX_TAG_VALUE_LENGTH) {
|
||||
snprintf(error_message, error_size, "%s[%d]: tag value too long (%zu characters, max %d)", key, i, tag_len, MAX_TAG_VALUE_LENGTH);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,16 @@ struct subscription {
|
||||
struct subscription* session_next; // Next subscription for this session
|
||||
};
|
||||
|
||||
// Per-IP connection tracking
|
||||
typedef struct ip_connection_info {
|
||||
char ip_address[CLIENT_IP_MAX_LENGTH]; // IP address
|
||||
int active_connections; // Number of active connections from this IP
|
||||
int total_subscriptions; // Total subscriptions across all connections from this IP
|
||||
time_t first_connection; // When first connection from this IP was established
|
||||
time_t last_activity; // Last activity timestamp from this IP
|
||||
struct ip_connection_info* next; // Next in linked list
|
||||
} ip_connection_info_t;
|
||||
|
||||
// Global subscription manager
|
||||
struct subscription_manager {
|
||||
subscription_t* active_subscriptions; // Head of global subscription list
|
||||
@@ -65,6 +75,10 @@ struct subscription_manager {
|
||||
int max_subscriptions_per_client; // Default: 20
|
||||
int max_total_subscriptions; // Default: 5000
|
||||
|
||||
// Per-IP connection tracking
|
||||
ip_connection_info_t* ip_connections; // Head of per-IP connection list
|
||||
pthread_mutex_t ip_tracking_lock; // Thread safety for IP tracking
|
||||
|
||||
// Statistics
|
||||
uint64_t total_created; // Lifetime subscription count
|
||||
uint64_t total_events_broadcast; // Lifetime event broadcast count
|
||||
@@ -81,6 +95,13 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter);
|
||||
int event_matches_subscription(cJSON* event, subscription_t* subscription);
|
||||
int broadcast_event_to_subscriptions(cJSON* event);
|
||||
|
||||
// Per-IP connection tracking functions
|
||||
ip_connection_info_t* get_or_create_ip_connection(const char* client_ip);
|
||||
void update_ip_connection_activity(const char* client_ip);
|
||||
void remove_ip_connection(const char* client_ip);
|
||||
int get_total_subscriptions_for_ip(const char* client_ip);
|
||||
int get_active_connections_for_ip(const char* client_ip);
|
||||
|
||||
// Database logging functions
|
||||
void log_subscription_created(const subscription_t* sub);
|
||||
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason);
|
||||
|
||||
321
src/websockets.c
321
src/websockets.c
@@ -226,7 +226,6 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
free(session_data);
|
||||
lws_set_wsi_user(wsi, NULL);
|
||||
|
||||
log_success("NIP-11 relay information served successfully");
|
||||
return 0; // Close connection after successful transmission
|
||||
}
|
||||
} else if (type == 1) {
|
||||
@@ -238,7 +237,6 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_ESTABLISHED:
|
||||
log_info("WebSocket connection established");
|
||||
memset(pss, 0, sizeof(*pss));
|
||||
pthread_mutex_init(&pss->session_lock, NULL);
|
||||
|
||||
@@ -451,10 +449,6 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
strncpy(error_message, "error: validation failed", sizeof(error_message) - 1);
|
||||
break;
|
||||
}
|
||||
char debug_error_msg[256];
|
||||
snprintf(debug_error_msg, sizeof(debug_error_msg),
|
||||
"DEBUG VALIDATION ERROR: %s", error_message);
|
||||
log_warning(debug_error_msg);
|
||||
}
|
||||
|
||||
// Cleanup event JSON string
|
||||
@@ -511,63 +505,34 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
if (kind_obj && cJSON_IsNumber(kind_obj)) {
|
||||
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||
|
||||
log_info("DEBUG ADMIN: Checking if admin event processing is needed");
|
||||
|
||||
|
||||
// Log reception of Kind 23456 events
|
||||
if (event_kind == 23456) {
|
||||
char* event_json_debug = cJSON_Print(event);
|
||||
char debug_received_msg[1024];
|
||||
snprintf(debug_received_msg, sizeof(debug_received_msg),
|
||||
"RECEIVED Kind %d event: %s", event_kind,
|
||||
event_json_debug ? event_json_debug : "Failed to serialize");
|
||||
log_info(debug_received_msg);
|
||||
|
||||
if (event_json_debug) {
|
||||
free(event_json_debug);
|
||||
}
|
||||
}
|
||||
|
||||
if (event_kind == 23456) {
|
||||
// Enhanced admin event security - check authorization first
|
||||
log_info("DEBUG ADMIN: Admin event detected, checking authorization");
|
||||
|
||||
|
||||
char auth_error[512] = {0};
|
||||
int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error));
|
||||
|
||||
if (auth_result != 0) {
|
||||
// Authorization failed - log and reject
|
||||
log_warning("DEBUG ADMIN: Admin event authorization failed");
|
||||
log_warning("Admin event authorization failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(auth_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, auth_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
|
||||
char debug_auth_error_msg[600];
|
||||
snprintf(debug_auth_error_msg, sizeof(debug_auth_error_msg),
|
||||
"DEBUG ADMIN AUTH ERROR: %.400s", auth_error);
|
||||
log_warning(debug_auth_error_msg);
|
||||
} else {
|
||||
// Authorization successful - process through admin API
|
||||
log_info("DEBUG ADMIN: Admin event authorized, processing through admin API");
|
||||
|
||||
|
||||
char admin_error[512] = {0};
|
||||
int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
|
||||
|
||||
char debug_admin_msg[256];
|
||||
snprintf(debug_admin_msg, sizeof(debug_admin_msg),
|
||||
"DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result);
|
||||
log_info(debug_admin_msg);
|
||||
|
||||
// Log results for Kind 23456 events
|
||||
if (event_kind == 23456) {
|
||||
if (admin_result == 0) {
|
||||
char success_result_msg[256];
|
||||
snprintf(success_result_msg, sizeof(success_result_msg),
|
||||
"SUCCESS: Kind %d event processed successfully", event_kind);
|
||||
log_success(success_result_msg);
|
||||
} else {
|
||||
if (admin_result != 0) {
|
||||
char error_result_msg[512];
|
||||
snprintf(error_result_msg, sizeof(error_result_msg),
|
||||
"ERROR: Kind %d event processing failed: %s", event_kind, admin_error);
|
||||
@@ -576,25 +541,18 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
}
|
||||
|
||||
if (admin_result != 0) {
|
||||
log_error("DEBUG ADMIN: Failed to process admin event through admin API");
|
||||
log_error("Failed to process admin event");
|
||||
result = -1;
|
||||
size_t error_len = strlen(admin_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, admin_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
|
||||
char debug_admin_error_msg[600];
|
||||
snprintf(debug_admin_error_msg, sizeof(debug_admin_error_msg),
|
||||
"DEBUG ADMIN ERROR: %.400s", admin_error);
|
||||
log_error(debug_admin_error_msg);
|
||||
} else {
|
||||
log_success("DEBUG ADMIN: Admin event processed successfully through admin API");
|
||||
// Admin events are processed by the admin API, not broadcast to subscriptions
|
||||
}
|
||||
}
|
||||
} else if (event_kind == 1059) {
|
||||
// Check for NIP-17 gift wrap admin messages
|
||||
log_info("DEBUG NIP17: Detected kind 1059 gift wrap event");
|
||||
|
||||
char nip17_error[512] = {0};
|
||||
cJSON* response_event = process_nip17_admin_message(event, nip17_error, sizeof(nip17_error), wsi);
|
||||
@@ -603,120 +561,81 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
// Check if this is an error or if the command was already handled
|
||||
if (strlen(nip17_error) > 0) {
|
||||
// There was an actual error
|
||||
log_error("DEBUG NIP17: NIP-17 admin message processing failed");
|
||||
log_error("NIP-17 admin message processing failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(nip17_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, nip17_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
|
||||
char debug_nip17_error_msg[600];
|
||||
snprintf(debug_nip17_error_msg, sizeof(debug_nip17_error_msg),
|
||||
"DEBUG NIP17 ERROR: %.400s", nip17_error);
|
||||
log_error(debug_nip17_error_msg);
|
||||
} else {
|
||||
// No error message means the command was already handled (plain text commands)
|
||||
log_success("DEBUG NIP17: NIP-17 admin message processed successfully (already handled)");
|
||||
// Store the original gift wrap event in database
|
||||
if (store_event(event) != 0) {
|
||||
log_error("DEBUG NIP17: Failed to store gift wrap event in database");
|
||||
log_error("Failed to store gift wrap event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_info("DEBUG NIP17: Gift wrap event stored successfully in database");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_success("DEBUG NIP17: NIP-17 admin message processed successfully");
|
||||
// Store the original gift wrap event in database (unlike kind 23456)
|
||||
if (store_event(event) != 0) {
|
||||
log_error("DEBUG NIP17: Failed to store gift wrap event in database");
|
||||
log_error("Failed to store gift wrap event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
|
||||
cJSON_Delete(response_event);
|
||||
} else {
|
||||
log_info("DEBUG NIP17: Gift wrap event stored successfully in database");
|
||||
// Debug: Print response event before broadcasting
|
||||
char* debug_before_broadcast = cJSON_Print(response_event);
|
||||
if (debug_before_broadcast) {
|
||||
log_info("DEBUG EVENT: Before broadcasting response event");
|
||||
printf(" Response Event: %s\n", debug_before_broadcast);
|
||||
free(debug_before_broadcast);
|
||||
}
|
||||
|
||||
// Broadcast RESPONSE event to matching persistent subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(response_event);
|
||||
char debug_broadcast_msg[128];
|
||||
snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
|
||||
"DEBUG NIP17 BROADCAST: Response event broadcast to %d subscriptions", broadcast_count);
|
||||
log_info(debug_broadcast_msg);
|
||||
|
||||
broadcast_event_to_subscriptions(response_event);
|
||||
|
||||
// Clean up response event
|
||||
cJSON_Delete(response_event);
|
||||
}
|
||||
}
|
||||
} else if (event_kind == 14) {
|
||||
// Check for DM stats commands addressed to relay
|
||||
log_info("DEBUG DM: Detected kind 14 DM event");
|
||||
|
||||
char dm_error[512] = {0};
|
||||
int dm_result = process_dm_stats_command(event, dm_error, sizeof(dm_error), wsi);
|
||||
|
||||
if (dm_result != 0) {
|
||||
log_error("DEBUG DM: DM stats command processing failed");
|
||||
log_error("DM stats command processing failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(dm_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, dm_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
|
||||
char debug_dm_error_msg[600];
|
||||
snprintf(debug_dm_error_msg, sizeof(debug_dm_error_msg),
|
||||
"DEBUG DM ERROR: %.400s", dm_error);
|
||||
log_error(debug_dm_error_msg);
|
||||
} else {
|
||||
log_success("DEBUG DM: DM stats command processed successfully");
|
||||
// Store the DM event in database
|
||||
if (store_event(event) != 0) {
|
||||
log_error("DEBUG DM: Failed to store DM event in database");
|
||||
log_error("Failed to store DM event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store DM event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_info("DEBUG DM: DM event stored successfully in database");
|
||||
// Broadcast DM event to matching persistent subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(event);
|
||||
char debug_broadcast_msg[128];
|
||||
snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
|
||||
"DEBUG DM BROADCAST: DM event broadcast to %d subscriptions", broadcast_count);
|
||||
log_info(debug_broadcast_msg);
|
||||
broadcast_event_to_subscriptions(event);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular event - store in database and broadcast
|
||||
log_info("DEBUG STORAGE: Regular event - storing in database");
|
||||
if (store_event(event) != 0) {
|
||||
log_error("DEBUG STORAGE: Failed to store event in database");
|
||||
log_error("Failed to store event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_info("DEBUG STORAGE: Event stored successfully in database");
|
||||
// Broadcast event to matching persistent subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(event);
|
||||
char debug_broadcast_msg[128];
|
||||
snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
|
||||
"DEBUG BROADCAST: Event broadcast to %d subscriptions", broadcast_count);
|
||||
log_info(debug_broadcast_msg);
|
||||
broadcast_event_to_subscriptions(event);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Event without valid kind - try normal storage
|
||||
log_warning("DEBUG STORAGE: Event without valid kind - trying normal storage");
|
||||
log_warning("Event without valid kind - trying normal storage");
|
||||
if (store_event(event) != 0) {
|
||||
log_error("DEBUG STORAGE: Failed to store event without kind in database");
|
||||
log_error("Failed to store event without kind in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_info("DEBUG STORAGE: Event without kind stored successfully in database");
|
||||
broadcast_event_to_subscriptions(event);
|
||||
}
|
||||
}
|
||||
@@ -731,25 +650,13 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : ""));
|
||||
|
||||
// TODO: REPLACE - Remove wasteful cJSON_Print conversion
|
||||
char *response_str = cJSON_Print(response);
|
||||
if (response_str) {
|
||||
char debug_response_msg[512];
|
||||
snprintf(debug_response_msg, sizeof(debug_response_msg),
|
||||
"DEBUG RESPONSE: Sending OK response: %s", response_str);
|
||||
log_info(debug_response_msg);
|
||||
|
||||
size_t response_len = strlen(response_str);
|
||||
unsigned char *buf = malloc(LWS_PRE + response_len);
|
||||
if (buf) {
|
||||
memcpy(buf + LWS_PRE, response_str, response_len);
|
||||
int write_result = lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
|
||||
|
||||
char debug_write_msg[128];
|
||||
snprintf(debug_write_msg, sizeof(debug_write_msg),
|
||||
"DEBUG RESPONSE: lws_write returned %d", write_result);
|
||||
log_info(debug_write_msg);
|
||||
|
||||
lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
|
||||
free(buf);
|
||||
}
|
||||
free(response_str);
|
||||
@@ -872,9 +779,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
}
|
||||
|
||||
char debug_msg[256];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "Closed subscription: %s", subscription_id);
|
||||
log_info(debug_msg);
|
||||
// Subscription closed
|
||||
}
|
||||
} else if (strcmp(msg_type, "AUTH") == 0) {
|
||||
// Handle NIP-42 AUTH message
|
||||
@@ -912,7 +817,6 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_CLOSED:
|
||||
log_info("WebSocket connection closed");
|
||||
|
||||
// Clean up session subscriptions
|
||||
if (pss) {
|
||||
@@ -992,7 +896,7 @@ int check_port_available(int port) {
|
||||
int start_websocket_relay(int port_override, int strict_port) {
|
||||
struct lws_context_creation_info info;
|
||||
|
||||
log_info("Starting libwebsockets-based Nostr relay server...");
|
||||
// Starting libwebsockets-based Nostr relay server
|
||||
|
||||
memset(&info, 0, sizeof(info));
|
||||
// Use port override if provided, otherwise use configuration
|
||||
@@ -1020,9 +924,7 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
|
||||
// Find an available port with pre-checking (or fail immediately in strict mode)
|
||||
while (port_attempts < (strict_port ? 1 : max_port_attempts)) {
|
||||
char attempt_msg[256];
|
||||
snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port);
|
||||
log_info(attempt_msg);
|
||||
// Checking port availability
|
||||
|
||||
// Pre-check if port is available
|
||||
if (!check_port_available(actual_port)) {
|
||||
@@ -1053,9 +955,7 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
// Port appears available, try creating libwebsockets context
|
||||
info.port = actual_port;
|
||||
|
||||
char binding_msg[256];
|
||||
snprintf(binding_msg, sizeof(binding_msg), "Attempting to bind libwebsockets to port %d", actual_port);
|
||||
log_info(binding_msg);
|
||||
// Attempting to bind libwebsockets
|
||||
|
||||
ws_context = lws_create_context(&info);
|
||||
if (ws_context) {
|
||||
@@ -1106,7 +1006,6 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
} else {
|
||||
snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port);
|
||||
}
|
||||
log_success(startup_msg);
|
||||
|
||||
// Main event loop with proper signal handling
|
||||
while (g_server_running && !g_shutdown_flag) {
|
||||
@@ -1118,11 +1017,8 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
}
|
||||
}
|
||||
|
||||
log_info("Shutting down WebSocket server...");
|
||||
lws_context_destroy(ws_context);
|
||||
ws_context = NULL;
|
||||
|
||||
log_success("WebSocket relay shut down cleanly");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1236,7 +1132,7 @@ int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_info("Processing DM stats command from admin");
|
||||
// Processing DM stats command from admin
|
||||
|
||||
// Generate stats JSON
|
||||
char* stats_json = generate_stats_json();
|
||||
@@ -1287,15 +1183,10 @@ int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_
|
||||
}
|
||||
|
||||
// Broadcast to subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(dm_response);
|
||||
char broadcast_msg[128];
|
||||
snprintf(broadcast_msg, sizeof(broadcast_msg),
|
||||
"DM stats response broadcast to %d subscriptions", broadcast_count);
|
||||
log_info(broadcast_msg);
|
||||
broadcast_event_to_subscriptions(dm_response);
|
||||
|
||||
cJSON_Delete(dm_response);
|
||||
|
||||
log_success("DM stats command processed successfully");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1303,13 +1194,17 @@ int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_
|
||||
// Handle NIP-45 COUNT message
|
||||
int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss) {
|
||||
(void)pss; // Suppress unused parameter warning
|
||||
log_info("Handling COUNT message for subscription");
|
||||
|
||||
if (!cJSON_IsArray(filters)) {
|
||||
log_error("COUNT filters is not an array");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parameter binding helpers
|
||||
char** bind_params = NULL;
|
||||
int bind_param_count = 0;
|
||||
int bind_param_capacity = 0;
|
||||
|
||||
int total_count = 0;
|
||||
|
||||
// Process each filter in the array
|
||||
@@ -1320,6 +1215,15 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset bind params for this filter
|
||||
for (int j = 0; j < bind_param_count; j++) {
|
||||
free(bind_params[j]);
|
||||
}
|
||||
free(bind_params);
|
||||
bind_params = NULL;
|
||||
bind_param_count = 0;
|
||||
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_ptr = sql + strlen(sql);
|
||||
@@ -1359,56 +1263,88 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
|
||||
// Handle authors filter
|
||||
cJSON* authors = cJSON_GetObjectItem(filter, "authors");
|
||||
if (authors && cJSON_IsArray(authors)) {
|
||||
int author_count = cJSON_GetArraySize(authors);
|
||||
int author_count = 0;
|
||||
// Count valid authors
|
||||
for (int a = 0; a < cJSON_GetArraySize(authors); a++) {
|
||||
cJSON* author = cJSON_GetArrayItem(authors, a);
|
||||
if (cJSON_IsString(author)) {
|
||||
author_count++;
|
||||
}
|
||||
}
|
||||
if (author_count > 0) {
|
||||
snprintf(sql_ptr, remaining, " AND pubkey IN (");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
|
||||
for (int a = 0; a < author_count; a++) {
|
||||
cJSON* author = cJSON_GetArrayItem(authors, a);
|
||||
if (cJSON_IsString(author)) {
|
||||
if (a > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(author));
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
if (a > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "?");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
}
|
||||
snprintf(sql_ptr, remaining, ")");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
|
||||
// Add author values to bind params
|
||||
for (int a = 0; a < cJSON_GetArraySize(authors); a++) {
|
||||
cJSON* author = cJSON_GetArrayItem(authors, a);
|
||||
if (cJSON_IsString(author)) {
|
||||
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(author));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ids filter
|
||||
cJSON* ids = cJSON_GetObjectItem(filter, "ids");
|
||||
if (ids && cJSON_IsArray(ids)) {
|
||||
int id_count = cJSON_GetArraySize(ids);
|
||||
int id_count = 0;
|
||||
// Count valid ids
|
||||
for (int i = 0; i < cJSON_GetArraySize(ids); i++) {
|
||||
cJSON* id = cJSON_GetArrayItem(ids, i);
|
||||
if (cJSON_IsString(id)) {
|
||||
id_count++;
|
||||
}
|
||||
}
|
||||
if (id_count > 0) {
|
||||
snprintf(sql_ptr, remaining, " AND id IN (");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
|
||||
for (int i = 0; i < id_count; i++) {
|
||||
cJSON* id = cJSON_GetArrayItem(ids, i);
|
||||
if (cJSON_IsString(id)) {
|
||||
if (i > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(id));
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
if (i > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "?");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
}
|
||||
snprintf(sql_ptr, remaining, ")");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
|
||||
// Add id values to bind params
|
||||
for (int i = 0; i < cJSON_GetArraySize(ids); i++) {
|
||||
cJSON* id = cJSON_GetArrayItem(ids, i);
|
||||
if (cJSON_IsString(id)) {
|
||||
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(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1421,29 +1357,50 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
|
||||
const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.)
|
||||
|
||||
if (cJSON_IsArray(filter_item)) {
|
||||
int tag_value_count = cJSON_GetArraySize(filter_item);
|
||||
int tag_value_count = 0;
|
||||
// Count valid tag values
|
||||
for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) {
|
||||
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
|
||||
if (cJSON_IsString(tag_value)) {
|
||||
tag_value_count++;
|
||||
}
|
||||
}
|
||||
if (tag_value_count > 0) {
|
||||
// Use EXISTS with JSON extraction to check for matching tags
|
||||
snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = '%s' AND json_extract(value, '$[1]') IN (", tag_name);
|
||||
// Use EXISTS with parameterized query
|
||||
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);
|
||||
|
||||
for (int i = 0; i < tag_value_count; i++) {
|
||||
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
|
||||
if (cJSON_IsString(tag_value)) {
|
||||
if (i > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(tag_value));
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
if (i > 0) {
|
||||
snprintf(sql_ptr, remaining, ",");
|
||||
sql_ptr++;
|
||||
remaining--;
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "?");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
}
|
||||
snprintf(sql_ptr, remaining, "))");
|
||||
sql_ptr += strlen(sql_ptr);
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
|
||||
// Add tag name and values to bind params
|
||||
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);
|
||||
for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) {
|
||||
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
|
||||
if (cJSON_IsString(tag_value)) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1493,10 +1450,7 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
|
||||
remaining = sizeof(sql) - strlen(sql);
|
||||
}
|
||||
|
||||
// Debug: Log the SQL query being executed
|
||||
char debug_msg[1280];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "Executing COUNT SQL: %s", sql);
|
||||
log_info(debug_msg);
|
||||
// Execute count query
|
||||
|
||||
// Execute count query
|
||||
sqlite3_stmt* stmt;
|
||||
@@ -1508,22 +1462,23 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bind parameters
|
||||
for (int i = 0; i < bind_param_count; i++) {
|
||||
sqlite3_bind_text(stmt, i + 1, bind_params[i], -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
int filter_count = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
filter_count = sqlite3_column_int(stmt, 0);
|
||||
}
|
||||
|
||||
char count_debug[128];
|
||||
snprintf(count_debug, sizeof(count_debug), "Filter %d returned count: %d", i + 1, filter_count);
|
||||
log_info(count_debug);
|
||||
// Filter count calculated
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
total_count += filter_count;
|
||||
}
|
||||
|
||||
char total_debug[128];
|
||||
snprintf(total_debug, sizeof(total_debug), "Total COUNT result: %d", total_count);
|
||||
log_info(total_debug);
|
||||
// Total count calculated
|
||||
|
||||
// Send COUNT response - NIP-45 format: ["COUNT", <subscription_id>, {"count": <count>}]
|
||||
cJSON* count_response = cJSON_CreateArray();
|
||||
@@ -1548,5 +1503,11 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
|
||||
}
|
||||
cJSON_Delete(count_response);
|
||||
|
||||
// Cleanup bind params
|
||||
for (int i = 0; i < bind_param_count; i++) {
|
||||
free(bind_params[i]);
|
||||
}
|
||||
free(bind_params);
|
||||
|
||||
return total_count;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#define CHALLENGE_MAX_LENGTH 128
|
||||
#define AUTHENTICATED_PUBKEY_MAX_LENGTH 65 // 64 hex + null
|
||||
|
||||
// Enhanced per-session data with subscription management and NIP-42 authentication
|
||||
// Enhanced per-session data with subscription management, NIP-42 authentication, and rate limiting
|
||||
struct per_session_data {
|
||||
int authenticated;
|
||||
struct subscription* subscriptions; // Head of this session's subscription list
|
||||
@@ -30,6 +30,12 @@ struct per_session_data {
|
||||
int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
|
||||
int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
|
||||
int auth_challenge_sent; // Whether challenge has been sent (0/1)
|
||||
|
||||
// Rate limiting for subscription attempts
|
||||
int failed_subscription_attempts; // Count of failed subscription attempts
|
||||
time_t last_failed_attempt; // Timestamp of last failed attempt
|
||||
time_t rate_limit_until; // Time until rate limiting expires
|
||||
int consecutive_failures; // Consecutive failed attempts for backoff
|
||||
};
|
||||
|
||||
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||
|
||||
63
tests/subscription_limits.sh
Executable file
63
tests/subscription_limits.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Simple test script to verify subscription limit enforcement and rate limiting
|
||||
# This script tests that subscription limits are enforced early
|
||||
|
||||
set -e
|
||||
|
||||
RELAY_URL="ws://127.0.0.1:8888"
|
||||
|
||||
echo "=== Subscription Limit Test ==="
|
||||
echo "[INFO] Testing relay at: $RELAY_URL"
|
||||
echo "[INFO] Note: This test assumes default subscription limits (max 25 per client)"
|
||||
echo ""
|
||||
|
||||
# Test basic connectivity first
|
||||
echo "=== Test 1: Basic Connectivity ==="
|
||||
echo "[INFO] Testing basic WebSocket connection..."
|
||||
|
||||
# Send a simple REQ message
|
||||
response=$(echo '["REQ","basic_test",{}]' | timeout 5 websocat -n1 "$RELAY_URL" 2>/dev/null || echo "TIMEOUT")
|
||||
|
||||
if echo "$response" | grep -q "EOSE\|EVENT\|NOTICE"; then
|
||||
echo "[PASS] Basic connectivity works"
|
||||
else
|
||||
echo "[FAIL] Basic connectivity failed. Response: $response"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test subscription limits
|
||||
echo "=== Test 2: Subscription Limit Enforcement ==="
|
||||
echo "[INFO] Testing subscription limits by creating multiple subscriptions..."
|
||||
|
||||
success_count=0
|
||||
limit_hit=false
|
||||
|
||||
# Create multiple subscriptions in sequence (each in its own connection)
|
||||
for i in {1..30}; do
|
||||
echo "[INFO] Creating subscription $i..."
|
||||
sub_id="limit_test_$i_$(date +%s%N)"
|
||||
response=$(echo "[\"REQ\",\"$sub_id\",{}]" | timeout 5 websocat -n1 "$RELAY_URL" 2>/dev/null || echo "TIMEOUT")
|
||||
|
||||
if echo "$response" | grep -q "CLOSED.*$sub_id.*exceeded"; then
|
||||
echo "[INFO] Hit subscription limit at subscription $i"
|
||||
limit_hit=true
|
||||
break
|
||||
elif echo "$response" | grep -q "EOSE\|EVENT"; then
|
||||
((success_count++))
|
||||
else
|
||||
echo "[WARN] Unexpected response for subscription $i: $response"
|
||||
fi
|
||||
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [ "$limit_hit" = true ]; then
|
||||
echo "[PASS] Subscription limit enforcement working (limit hit after $success_count subscriptions)"
|
||||
else
|
||||
echo "[WARN] Subscription limit not hit after 30 attempts"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "=== Test Complete ==="
|
||||
Reference in New Issue
Block a user