Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f272264960 | ||
|
|
cb3171b390 | ||
|
|
03f036d60d | ||
|
|
9b35f463ae | ||
|
|
7b74486519 | ||
|
|
b276b44ded | ||
|
|
3792649ed9 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,4 +11,3 @@ copy_executable_local.sh
|
||||
nostr_login_lite/
|
||||
style_guide/
|
||||
nostr-tools
|
||||
|
||||
|
||||
612
STATIC_MUSL_GUIDE.md
Normal file
612
STATIC_MUSL_GUIDE.md
Normal file
@@ -0,0 +1,612 @@
|
||||
# Static MUSL Build Guide for C Programs
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to build truly portable static binaries using Alpine Linux and MUSL libc. These binaries have **zero runtime dependencies** and work on any Linux distribution without modification.
|
||||
|
||||
This guide is specifically tailored for C programs that use:
|
||||
- **nostr_core_lib** - Nostr protocol implementation
|
||||
- **nostr_login_lite** - Nostr authentication library
|
||||
- Common dependencies: libwebsockets, OpenSSL, SQLite, curl, secp256k1
|
||||
|
||||
## Why MUSL Static Binaries?
|
||||
|
||||
### Advantages Over glibc
|
||||
|
||||
| Feature | MUSL Static | glibc Static | glibc Dynamic |
|
||||
|---------|-------------|--------------|---------------|
|
||||
| **Portability** | ✓ Any Linux | ⚠ glibc only | ✗ Requires matching libs |
|
||||
| **Binary Size** | ~7-10 MB | ~12-15 MB | ~2-3 MB |
|
||||
| **Dependencies** | None | NSS libs | Many system libs |
|
||||
| **Deployment** | Single file | Single file + NSS | Binary + libraries |
|
||||
| **Compatibility** | Universal | glibc version issues | Library version hell |
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **True Portability**: Works on Alpine, Ubuntu, Debian, CentOS, Arch, etc.
|
||||
2. **No Library Hell**: No `GLIBC_2.XX not found` errors
|
||||
3. **Simple Deployment**: Just copy one file
|
||||
4. **Reproducible Builds**: Same Docker image = same binary
|
||||
5. **Security**: No dependency on system libraries with vulnerabilities
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker installed and running
|
||||
- Your C project with source code
|
||||
- Internet connection for downloading dependencies
|
||||
|
||||
### Basic Build Process
|
||||
|
||||
```bash
|
||||
# 1. Copy the Dockerfile template (see below)
|
||||
cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static
|
||||
|
||||
# 2. Customize for your project (see Customization section)
|
||||
vim Dockerfile.static
|
||||
|
||||
# 3. Build the static binary
|
||||
docker build --platform linux/amd64 -f Dockerfile.static -t my-app-builder .
|
||||
|
||||
# 4. Extract the binary
|
||||
docker create --name temp-container my-app-builder
|
||||
docker cp temp-container:/build/my_app_static ./my_app_static
|
||||
docker rm temp-container
|
||||
|
||||
# 5. Verify it's static
|
||||
ldd ./my_app_static # Should show "not a dynamic executable"
|
||||
```
|
||||
|
||||
## Dockerfile Template
|
||||
|
||||
Here's a complete Dockerfile template you can customize for your project:
|
||||
|
||||
```dockerfile
|
||||
# Alpine-based MUSL static binary builder
|
||||
# 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
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Build libsecp256k1 static (required for Nostr)
|
||||
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 (if needed for WebSocket support)
|
||||
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 git configuration for submodules
|
||||
COPY .gitmodules /build/.gitmodules
|
||||
COPY .git /build/.git
|
||||
|
||||
# Initialize submodules
|
||||
RUN git submodule update --init --recursive
|
||||
|
||||
# Copy and build nostr_core_lib
|
||||
COPY nostr_core_lib /build/nostr_core_lib/
|
||||
RUN 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
|
||||
|
||||
# Copy and build nostr_login_lite (if used)
|
||||
# COPY nostr_login_lite /build/nostr_login_lite/
|
||||
# RUN cd nostr_login_lite && make static
|
||||
|
||||
# Copy your application source
|
||||
COPY src/ /build/src/
|
||||
COPY Makefile /build/Makefile
|
||||
|
||||
# Build your application with full static linking
|
||||
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/*.c \
|
||||
-o /build/my_app_static \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
|
||||
-lcurl -lz -lpthread -lm -ldl && \
|
||||
strip /build/my_app_static
|
||||
|
||||
# Verify it's truly static
|
||||
RUN echo "=== Binary Information ===" && \
|
||||
file /build/my_app_static && \
|
||||
ls -lh /build/my_app_static && \
|
||||
echo "=== Checking for dynamic dependencies ===" && \
|
||||
(ldd /build/my_app_static 2>&1 || echo "Binary is static")
|
||||
|
||||
# Output stage - just the binary
|
||||
FROM scratch AS output
|
||||
COPY --from=builder /build/my_app_static /my_app_static
|
||||
```
|
||||
|
||||
## Customization Guide
|
||||
|
||||
### 1. Adjust Dependencies
|
||||
|
||||
**Add dependencies** by modifying the `apk add` section:
|
||||
|
||||
```dockerfile
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
musl-dev \
|
||||
# Add your dependencies here:
|
||||
libpng-dev \
|
||||
libpng-static \
|
||||
libjpeg-turbo-dev \
|
||||
libjpeg-turbo-static
|
||||
```
|
||||
|
||||
**Remove unused dependencies** to speed up builds:
|
||||
- Remove `libwebsockets` section if you don't need WebSocket support
|
||||
- Remove `sqlite` if you don't use databases
|
||||
- Remove `curl` if you don't make HTTP requests
|
||||
|
||||
### 2. Configure nostr_core_lib NIPs
|
||||
|
||||
Specify which NIPs your application needs:
|
||||
|
||||
```bash
|
||||
./build.sh --nips=1,6,19 # Minimal: Basic protocol, keys, bech32
|
||||
./build.sh --nips=1,6,13,17,19,44,59 # Full: All common NIPs
|
||||
./build.sh --nips=all # Everything available
|
||||
```
|
||||
|
||||
**Common NIP combinations:**
|
||||
- **Basic client**: `1,6,19` (events, keys, bech32)
|
||||
- **With encryption**: `1,6,19,44` (add modern encryption)
|
||||
- **With DMs**: `1,6,17,19,44,59` (add private messages)
|
||||
- **Relay/server**: `1,6,13,17,19,42,44,59` (add PoW, auth)
|
||||
|
||||
### 3. Modify Compilation Flags
|
||||
|
||||
**For your application:**
|
||||
|
||||
```dockerfile
|
||||
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \ # REQUIRED for MUSL
|
||||
-I. -Inostr_core_lib \ # Include paths
|
||||
src/*.c \ # Your source files
|
||||
-o /build/my_app_static \ # Output binary
|
||||
nostr_core_lib/libnostr_core_x64.a \ # Nostr library
|
||||
-lwebsockets -lssl -lcrypto \ # Link libraries
|
||||
-lsqlite3 -lsecp256k1 -lcurl \
|
||||
-lz -lpthread -lm -ldl
|
||||
```
|
||||
|
||||
**Debug build** (with symbols, no optimization):
|
||||
|
||||
```dockerfile
|
||||
RUN gcc -static -g -O0 -DDEBUG \
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||
# ... rest of flags
|
||||
```
|
||||
|
||||
### 4. Multi-Architecture Support
|
||||
|
||||
Build for different architectures:
|
||||
|
||||
```bash
|
||||
# x86_64 (Intel/AMD)
|
||||
docker build --platform linux/amd64 -f Dockerfile.static -t my-app-x86 .
|
||||
|
||||
# ARM64 (Apple Silicon, Raspberry Pi 4+)
|
||||
docker build --platform linux/arm64 -f Dockerfile.static -t my-app-arm64 .
|
||||
```
|
||||
|
||||
## Build Script Template
|
||||
|
||||
Create a `build_static.sh` script for convenience:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
DOCKERFILE="$SCRIPT_DIR/Dockerfile.static"
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="my_app_static_x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
PLATFORM="linux/arm64"
|
||||
OUTPUT_NAME="my_app_static_arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown architecture: $ARCH"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Building for platform: $PLATFORM"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# Build Docker image
|
||||
docker build \
|
||||
--platform "$PLATFORM" \
|
||||
-f "$DOCKERFILE" \
|
||||
-t my-app-builder:latest \
|
||||
--progress=plain \
|
||||
.
|
||||
|
||||
# Extract binary
|
||||
CONTAINER_ID=$(docker create my-app-builder:latest)
|
||||
docker cp "$CONTAINER_ID:/build/my_app_static" "$BUILD_DIR/$OUTPUT_NAME"
|
||||
docker rm "$CONTAINER_ID"
|
||||
|
||||
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
|
||||
|
||||
echo "✓ Build complete: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo "✓ Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||
|
||||
# Verify
|
||||
if ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 | grep -q "not a dynamic executable"; then
|
||||
echo "✓ Binary is fully static"
|
||||
else
|
||||
echo "⚠ Warning: Binary may have dynamic dependencies"
|
||||
fi
|
||||
```
|
||||
|
||||
Make it executable:
|
||||
|
||||
```bash
|
||||
chmod +x build_static.sh
|
||||
./build_static.sh
|
||||
```
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue 1: Fortification Errors
|
||||
|
||||
**Error:**
|
||||
```
|
||||
undefined reference to '__snprintf_chk'
|
||||
undefined reference to '__fprintf_chk'
|
||||
```
|
||||
|
||||
**Cause**: GCC's `-O2` enables fortification by default, which uses glibc-specific functions.
|
||||
|
||||
**Solution**: Add these flags to **all** compilation commands:
|
||||
```bash
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0
|
||||
```
|
||||
|
||||
This must be applied to:
|
||||
1. nostr_core_lib build.sh
|
||||
2. Your application compilation
|
||||
3. Any other libraries you build
|
||||
|
||||
### Issue 2: Missing Symbols from nostr_core_lib
|
||||
|
||||
**Error:**
|
||||
```
|
||||
undefined reference to 'nostr_create_event'
|
||||
undefined reference to 'nostr_sign_event'
|
||||
```
|
||||
|
||||
**Cause**: Required NIPs not included in nostr_core_lib build.
|
||||
|
||||
**Solution**: Add missing NIPs:
|
||||
```bash
|
||||
./build.sh --nips=1,6,19 # Add the NIPs you need
|
||||
```
|
||||
|
||||
### Issue 3: Docker Permission Denied
|
||||
|
||||
**Error:**
|
||||
```
|
||||
permission denied while trying to connect to the Docker daemon socket
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
newgrp docker # Or logout and login
|
||||
```
|
||||
|
||||
### Issue 4: Binary Won't Run on Target System
|
||||
|
||||
**Checks**:
|
||||
```bash
|
||||
# 1. Verify it's static
|
||||
ldd my_app_static # Should show "not a dynamic executable"
|
||||
|
||||
# 2. Check architecture
|
||||
file my_app_static # Should match target system
|
||||
|
||||
# 3. Test on different distributions
|
||||
docker run --rm -v $(pwd):/app alpine:latest /app/my_app_static --version
|
||||
docker run --rm -v $(pwd):/app ubuntu:latest /app/my_app_static --version
|
||||
```
|
||||
|
||||
## Project Structure Example
|
||||
|
||||
Organize your project for easy static builds:
|
||||
|
||||
```
|
||||
my-nostr-app/
|
||||
├── src/
|
||||
│ ├── main.c
|
||||
│ ├── handlers.c
|
||||
│ └── utils.c
|
||||
├── nostr_core_lib/ # Git submodule
|
||||
├── nostr_login_lite/ # Git submodule (if used)
|
||||
├── Dockerfile.static # Static build Dockerfile
|
||||
├── build_static.sh # Build script
|
||||
├── Makefile # Regular build
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Makefile Integration
|
||||
|
||||
Add static build targets to your Makefile:
|
||||
|
||||
```makefile
|
||||
# Regular dynamic build
|
||||
all: my_app
|
||||
|
||||
my_app: src/*.c
|
||||
gcc -O2 src/*.c -o my_app \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
-lssl -lcrypto -lsecp256k1 -lz -lpthread -lm
|
||||
|
||||
# Static MUSL build via Docker
|
||||
static:
|
||||
./build_static.sh
|
||||
|
||||
# Clean
|
||||
clean:
|
||||
rm -f my_app build/my_app_static_*
|
||||
|
||||
.PHONY: all static clean
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Single Binary Deployment
|
||||
|
||||
```bash
|
||||
# Copy to server
|
||||
scp build/my_app_static_x86_64 user@server:/opt/my-app/
|
||||
|
||||
# Run (no dependencies needed!)
|
||||
ssh user@server
|
||||
/opt/my-app/my_app_static_x86_64
|
||||
```
|
||||
|
||||
### SystemD Service
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=My Nostr Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=myapp
|
||||
WorkingDirectory=/opt/my-app
|
||||
ExecStart=/opt/my-app/my_app_static_x86_64
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Docker Container (Minimal)
|
||||
|
||||
```dockerfile
|
||||
FROM scratch
|
||||
COPY my_app_static_x86_64 /app
|
||||
ENTRYPOINT ["/app"]
|
||||
```
|
||||
|
||||
Build and run:
|
||||
```bash
|
||||
docker build -t my-app:latest .
|
||||
docker run --rm my-app:latest --help
|
||||
```
|
||||
|
||||
## Reusing c-relay Files
|
||||
|
||||
You can directly copy these files from c-relay:
|
||||
|
||||
### 1. Dockerfile.alpine-musl
|
||||
```bash
|
||||
cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static
|
||||
```
|
||||
|
||||
Then customize:
|
||||
- Change binary name (line 125)
|
||||
- Adjust source files (line 122-124)
|
||||
- Modify include paths (line 120-121)
|
||||
|
||||
### 2. build_static.sh
|
||||
```bash
|
||||
cp /path/to/c-relay/build_static.sh ./
|
||||
```
|
||||
|
||||
Then customize:
|
||||
- Change `OUTPUT_NAME` variable (lines 66, 70)
|
||||
- Update Docker image name (line 98)
|
||||
- Modify verification commands (lines 180-184)
|
||||
|
||||
### 3. .dockerignore (Optional)
|
||||
```bash
|
||||
cp /path/to/c-relay/.dockerignore ./
|
||||
```
|
||||
|
||||
Helps speed up Docker builds by excluding unnecessary files.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Version Control**: Commit your Dockerfile and build script
|
||||
2. **Tag Builds**: Include git commit hash in binary version
|
||||
3. **Test Thoroughly**: Verify on multiple distributions
|
||||
4. **Document Dependencies**: List required NIPs and libraries
|
||||
5. **Automate**: Use CI/CD to build on every commit
|
||||
6. **Archive Binaries**: Keep old versions for rollback
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Metric | MUSL Static | glibc Dynamic |
|
||||
|--------|-------------|---------------|
|
||||
| Binary Size | 7-10 MB | 2-3 MB + libs |
|
||||
| Startup Time | ~50ms | ~40ms |
|
||||
| Memory Usage | Similar | Similar |
|
||||
| Portability | ✓ Universal | ✗ System-dependent |
|
||||
| Deployment | Single file | Binary + libraries |
|
||||
|
||||
## References
|
||||
|
||||
- [MUSL libc](https://musl.libc.org/)
|
||||
- [Alpine Linux](https://alpinelinux.org/)
|
||||
- [nostr_core_lib](https://github.com/chebizarro/nostr_core_lib)
|
||||
- [Static Linking Best Practices](https://www.musl-libc.org/faq.html)
|
||||
- [c-relay Implementation](./docs/musl_static_build.md)
|
||||
|
||||
## Example: Minimal Nostr Client
|
||||
|
||||
Here's a complete example of building a minimal Nostr client:
|
||||
|
||||
```c
|
||||
// minimal_client.c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
// Generate keypair
|
||||
char nsec[64], npub[64];
|
||||
nostr_generate_keypair(nsec, npub);
|
||||
|
||||
printf("Generated keypair:\n");
|
||||
printf("Private key (nsec): %s\n", nsec);
|
||||
printf("Public key (npub): %s\n", npub);
|
||||
|
||||
// Create event
|
||||
cJSON *event = nostr_create_event(1, "Hello, Nostr!", NULL);
|
||||
nostr_sign_event(event, nsec);
|
||||
|
||||
char *json = cJSON_Print(event);
|
||||
printf("\nSigned event:\n%s\n", json);
|
||||
|
||||
free(json);
|
||||
cJSON_Delete(event);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Dockerfile.static:**
|
||||
```dockerfile
|
||||
FROM alpine:3.19 AS builder
|
||||
RUN apk add --no-cache build-base musl-dev git autoconf automake libtool \
|
||||
openssl-dev openssl-libs-static zlib-dev zlib-static
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Build secp256k1
|
||||
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
|
||||
|
||||
# Copy and build nostr_core_lib
|
||||
COPY nostr_core_lib /build/nostr_core_lib/
|
||||
RUN cd nostr_core_lib && \
|
||||
sed -i 's/CFLAGS="-Wall/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall/' build.sh && \
|
||||
./build.sh --nips=1,6,19
|
||||
|
||||
# Build application
|
||||
COPY minimal_client.c /build/
|
||||
RUN gcc -static -O2 -Wall -std=c99 \
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||
-Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson \
|
||||
minimal_client.c -o /build/minimal_client_static \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
-lssl -lcrypto -lsecp256k1 -lz -lpthread -lm -ldl && \
|
||||
strip /build/minimal_client_static
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /build/minimal_client_static /minimal_client_static
|
||||
```
|
||||
|
||||
**Build and run:**
|
||||
```bash
|
||||
docker build -f Dockerfile.static -t minimal-client .
|
||||
docker create --name temp minimal-client
|
||||
docker cp temp:/minimal_client_static ./
|
||||
docker rm temp
|
||||
|
||||
./minimal_client_static
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Static MUSL binaries provide the best portability for C applications. While they're slightly larger than dynamic binaries, the benefits of zero dependencies and universal compatibility make them ideal for:
|
||||
|
||||
- Server deployments across different Linux distributions
|
||||
- Embedded systems and IoT devices
|
||||
- Docker containers (FROM scratch)
|
||||
- Distribution to users without dependency management
|
||||
- Long-term archival and reproducibility
|
||||
|
||||
Follow this guide to create portable, self-contained binaries for your Nostr applications!
|
||||
@@ -3,17 +3,17 @@
|
||||
# Copy the binary to the deployment location
|
||||
cp build/c_relay_x86 ~/Storage/c_relay/crelay
|
||||
|
||||
# Copy the local service file to systemd
|
||||
# sudo cp systemd/c-relay-local.service /etc/systemd/system/
|
||||
# Copy the service file to systemd (use the main service file)
|
||||
sudo cp systemd/c-relay.service /etc/systemd/system/c-relay-local.service
|
||||
|
||||
# Reload systemd daemon to pick up the new service
|
||||
# sudo systemctl daemon-reload
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Enable the service (if not already enabled)
|
||||
# sudo systemctl enable c-relay-local.service
|
||||
sudo systemctl enable c-relay-local.service
|
||||
|
||||
# Restart the service
|
||||
# sudo systemctl restart c-relay-local.service
|
||||
sudo systemctl restart c-relay-local.service
|
||||
|
||||
# Show service status
|
||||
# sudo systemctl status c-relay-local.service --no-pager -l
|
||||
sudo systemctl status c-relay-local.service --no-pager -l
|
||||
|
||||
532
docs/subscription_cleanup_simplified.md
Normal file
532
docs/subscription_cleanup_simplified.md
Normal file
@@ -0,0 +1,532 @@
|
||||
# Subscription Cleanup - Simplified Design
|
||||
|
||||
## Problem Summary
|
||||
|
||||
The c-relay Nostr relay experienced severe performance degradation (90-100% CPU) due to subscription accumulation in the database. Investigation revealed **323,644 orphaned subscriptions** that were never properly closed when WebSocket connections dropped.
|
||||
|
||||
## Solution: Two-Component Approach
|
||||
|
||||
This simplified design focuses on two pragmatic solutions that align with Nostr's stateless design:
|
||||
|
||||
1. **Startup Cleanup**: Close all subscriptions on relay restart
|
||||
2. **Connection Age Limit**: Disconnect clients after a configurable time period
|
||||
|
||||
Both solutions force clients to reconnect and re-establish subscriptions, which is standard Nostr behavior.
|
||||
|
||||
---
|
||||
|
||||
## Component 1: Startup Cleanup
|
||||
|
||||
### Purpose
|
||||
Ensure clean state on every relay restart by closing all subscriptions in the database.
|
||||
|
||||
### Implementation
|
||||
|
||||
**File:** [`src/subscriptions.c`](src/subscriptions.c)
|
||||
|
||||
**New Function:**
|
||||
```c
|
||||
void cleanup_all_subscriptions_on_startup(void) {
|
||||
if (!g_db) {
|
||||
DEBUG_ERROR("Database not initialized for startup cleanup");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOG("Startup cleanup: Marking all active subscriptions as disconnected");
|
||||
|
||||
// Mark all 'created' subscriptions as disconnected
|
||||
const char* update_sql =
|
||||
"UPDATE subscriptions "
|
||||
"SET ended_at = strftime('%s', 'now') "
|
||||
"WHERE event_type = 'created' AND ended_at IS NULL";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to prepare startup cleanup query: %s", sqlite3_errmsg(g_db));
|
||||
return;
|
||||
}
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
int updated_count = sqlite3_changes(g_db);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (updated_count > 0) {
|
||||
// Log a single 'disconnected' event for the startup cleanup
|
||||
const char* insert_sql =
|
||||
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
||||
"VALUES ('startup_cleanup', '', 'system', 'disconnected')";
|
||||
|
||||
rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
DEBUG_LOG("Startup cleanup: Marked %d subscriptions as disconnected", updated_count);
|
||||
} else {
|
||||
DEBUG_LOG("Startup cleanup: No active subscriptions found");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Integration Point:** [`src/main.c:1810`](src/main.c:1810)
|
||||
|
||||
```c
|
||||
// Initialize subscription manager mutexes
|
||||
if (pthread_mutex_init(&g_subscription_manager.subscriptions_lock, NULL) != 0) {
|
||||
DEBUG_ERROR("Failed to initialize subscriptions mutex");
|
||||
sqlite3_close(g_db);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (pthread_mutex_init(&g_subscription_manager.ip_tracking_lock, NULL) != 0) {
|
||||
DEBUG_ERROR("Failed to initialize IP tracking mutex");
|
||||
pthread_mutex_destroy(&g_subscription_manager.subscriptions_lock);
|
||||
sqlite3_close(g_db);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// **NEW: Startup cleanup - close all subscriptions**
|
||||
cleanup_all_subscriptions_on_startup();
|
||||
|
||||
// Start WebSocket relay server
|
||||
DEBUG_LOG("Starting WebSocket relay server...");
|
||||
if (start_websocket_relay(port_override, strict_port) != 0) {
|
||||
DEBUG_ERROR("Failed to start WebSocket relay");
|
||||
// ... cleanup code
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits
|
||||
- **Immediate relief**: Restart relay to fix subscription issues
|
||||
- **Clean slate**: Every restart starts with zero active subscriptions
|
||||
- **Simple**: Single SQL UPDATE statement
|
||||
- **Nostr-aligned**: Clients are designed to reconnect after relay restart
|
||||
- **No configuration needed**: Always runs on startup
|
||||
|
||||
---
|
||||
|
||||
## Component 2: Connection Age Limit
|
||||
|
||||
### Purpose
|
||||
Automatically disconnect clients after a configurable time period, forcing them to reconnect and re-establish subscriptions.
|
||||
|
||||
### Why Disconnect Instead of Just Closing Subscriptions?
|
||||
|
||||
**Option 1: Send CLOSED message (keep connection)**
|
||||
- ❌ Not all clients handle `CLOSED` messages properly
|
||||
- ❌ Silent failure - client thinks it's subscribed but isn't
|
||||
- ❌ Partial cleanup - connection still consumes resources
|
||||
- ❌ More complex to implement
|
||||
|
||||
**Option 2: Disconnect client entirely (force reconnection)** ✅
|
||||
- ✅ Universal compatibility - all clients handle WebSocket reconnection
|
||||
- ✅ Complete resource cleanup (memory, file descriptors, etc.)
|
||||
- ✅ Simple implementation - single operation
|
||||
- ✅ Well-tested code path (same as network interruptions)
|
||||
- ✅ Forces re-authentication if needed
|
||||
|
||||
### Implementation
|
||||
|
||||
**File:** [`src/websockets.c`](src/websockets.c)
|
||||
|
||||
**New Function:**
|
||||
```c
|
||||
/**
|
||||
* Check connection age and disconnect clients that have been connected too long.
|
||||
* This forces clients to reconnect and re-establish subscriptions.
|
||||
*
|
||||
* Uses libwebsockets' lws_vhost_foreach_wsi() to iterate through all active
|
||||
* connections and checks their connection_established timestamp from per_session_data.
|
||||
*/
|
||||
void check_connection_age(int max_connection_seconds) {
|
||||
if (max_connection_seconds <= 0 || !ws_context) {
|
||||
return;
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
time_t cutoff = now - max_connection_seconds;
|
||||
|
||||
// Get the default vhost
|
||||
struct lws_vhost *vhost = lws_get_vhost_by_name(ws_context, "default");
|
||||
if (!vhost) {
|
||||
DEBUG_ERROR("Failed to get vhost for connection age check");
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate through all active WebSocket connections
|
||||
// Note: lws_vhost_foreach_wsi() calls our callback for each connection
|
||||
struct lws *wsi = NULL;
|
||||
while ((wsi = lws_vhost_foreach_wsi(vhost, wsi)) != NULL) {
|
||||
// Get per-session data which contains connection_established timestamp
|
||||
struct per_session_data *pss = (struct per_session_data *)lws_wsi_user(wsi);
|
||||
|
||||
if (pss && pss->connection_established > 0) {
|
||||
// Check if connection is older than cutoff
|
||||
if (pss->connection_established < cutoff) {
|
||||
// Connection is too old - close it
|
||||
long age_seconds = now - pss->connection_established;
|
||||
|
||||
DEBUG_LOG("Closing connection from %s (age: %lds, limit: %ds)",
|
||||
pss->client_ip, age_seconds, max_connection_seconds);
|
||||
|
||||
// Close with normal status and reason message
|
||||
lws_close_reason(wsi, LWS_CLOSE_STATUS_NORMAL,
|
||||
(unsigned char *)"connection age limit", 21);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Implementation Details:**
|
||||
|
||||
1. **No database needed**: Active connections are tracked by libwebsockets itself
|
||||
2. **Uses existing timestamp**: `pss->connection_established` is already set on line 456 of websockets.c
|
||||
3. **Built-in iterator**: `lws_vhost_foreach_wsi()` safely iterates through all active connections
|
||||
4. **Per-session data**: Each connection's `per_session_data` is accessible via `lws_wsi_user()`
|
||||
5. **Safe closure**: `lws_close_reason()` properly closes the WebSocket with a status code and message
|
||||
|
||||
**Integration Point:** [`src/websockets.c:2176`](src/websockets.c:2176) - in existing event loop
|
||||
|
||||
```c
|
||||
// Main event loop with proper signal handling
|
||||
while (g_server_running && !g_shutdown_flag) {
|
||||
int result = lws_service(ws_context, 1000);
|
||||
|
||||
if (result < 0) {
|
||||
DEBUG_ERROR("libwebsockets service error");
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if it's time to post status update
|
||||
time_t current_time = time(NULL);
|
||||
int status_post_hours = get_config_int("kind_1_status_posts_hours", 0);
|
||||
|
||||
if (status_post_hours > 0) {
|
||||
int seconds_interval = status_post_hours * 3600;
|
||||
if (current_time - last_status_post_time >= seconds_interval) {
|
||||
last_status_post_time = current_time;
|
||||
generate_and_post_status_event();
|
||||
}
|
||||
}
|
||||
|
||||
// **NEW: Check for connection age limit**
|
||||
int max_connection_seconds = get_config_int("max_connection_seconds", 86400); // Default 24 hours
|
||||
if (max_connection_seconds > 0) {
|
||||
check_connection_age(max_connection_seconds);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
**Parameter:** `max_connection_seconds`
|
||||
- **Default:** `86400` (24 hours)
|
||||
- **Range:** `0` = disabled, `>0` = disconnect after X seconds
|
||||
- **Units:** Seconds (for consistency with other time-based configs)
|
||||
|
||||
**Example configurations:**
|
||||
```json
|
||||
{
|
||||
"max_connection_seconds": 86400 // 86400 seconds = 24 hours (default)
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"max_connection_seconds": 43200 // 43200 seconds = 12 hours
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"max_connection_seconds": 3600 // 3600 seconds = 1 hour
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"max_connection_seconds": 0 // Disabled
|
||||
}
|
||||
```
|
||||
|
||||
### Client Behavior
|
||||
|
||||
When disconnected due to age limit, clients will:
|
||||
1. Detect WebSocket closure
|
||||
2. Wait briefly (exponential backoff)
|
||||
3. Reconnect to relay
|
||||
4. Re-authenticate if needed (NIP-42)
|
||||
5. Re-establish all subscriptions
|
||||
6. Resume normal operation
|
||||
|
||||
This is **exactly what happens** during network interruptions, so it's a well-tested code path in all Nostr clients.
|
||||
|
||||
### Benefits
|
||||
- **No new threads**: Uses existing event loop
|
||||
- **Minimal overhead**: Check runs once per second (same as `lws_service`)
|
||||
- **Simple implementation**: Iterate through active connections
|
||||
- **Consistent pattern**: Matches existing status post checking
|
||||
- **Universal compatibility**: All clients handle reconnection
|
||||
- **Complete cleanup**: Frees all resources associated with connection
|
||||
- **Configurable**: Can be adjusted per relay needs or disabled entirely
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Startup Cleanup (1-2 hours)
|
||||
|
||||
1. **Add `cleanup_all_subscriptions_on_startup()` function**
|
||||
- File: [`src/subscriptions.c`](src/subscriptions.c)
|
||||
- SQL UPDATE to mark all active subscriptions as disconnected
|
||||
- Add logging for cleanup count
|
||||
|
||||
2. **Integrate in main()**
|
||||
- File: [`src/main.c:1810`](src/main.c:1810)
|
||||
- Call after mutex initialization, before WebSocket server start
|
||||
|
||||
3. **Test**
|
||||
- Create subscriptions in database
|
||||
- Restart relay
|
||||
- Verify all subscriptions marked as disconnected
|
||||
- Verify `active_subscriptions_log` shows 0 subscriptions
|
||||
|
||||
**Estimated Time:** 1-2 hours
|
||||
|
||||
### Phase 2: Connection Age Limit (2-3 hours)
|
||||
|
||||
1. **Add `check_connection_age()` function**
|
||||
- File: [`src/websockets.c`](src/websockets.c)
|
||||
- Iterate through active connections
|
||||
- Close connections older than limit
|
||||
|
||||
2. **Integrate in event loop**
|
||||
- File: [`src/websockets.c:2176`](src/websockets.c:2176)
|
||||
- Add check after status post check
|
||||
- Use same pattern as status posts
|
||||
|
||||
3. **Add configuration parameter**
|
||||
- Add `max_connection_seconds` to default config
|
||||
- Default: 86400 (24 hours)
|
||||
|
||||
4. **Test**
|
||||
- Connect client
|
||||
- Wait for timeout (or reduce timeout for testing)
|
||||
- Verify client disconnected
|
||||
- Verify client reconnects automatically
|
||||
- Verify subscriptions re-established
|
||||
|
||||
**Estimated Time:** 2-3 hours
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Startup Cleanup Tests
|
||||
|
||||
```bash
|
||||
# Test 1: Clean startup with existing subscriptions
|
||||
- Create 100 active subscriptions in database
|
||||
- Restart relay
|
||||
- Verify all subscriptions marked as disconnected
|
||||
- Verify active_subscriptions_log shows 0 subscriptions
|
||||
|
||||
# Test 2: Clean startup with no subscriptions
|
||||
- Start relay with empty database
|
||||
- Verify no errors
|
||||
- Verify startup cleanup logs "No active subscriptions found"
|
||||
|
||||
# Test 3: Clients reconnect after restart
|
||||
- Create subscriptions before restart
|
||||
- Restart relay
|
||||
- Connect clients and create new subscriptions
|
||||
- Verify new subscriptions tracked correctly
|
||||
```
|
||||
|
||||
### Connection Age Limit Tests
|
||||
|
||||
```bash
|
||||
# Test 1: Connection disconnected after timeout
|
||||
- Set max_connection_seconds to 60 (for testing)
|
||||
- Connect client
|
||||
- Wait 61 seconds
|
||||
- Verify client disconnected
|
||||
- Verify client reconnects automatically
|
||||
|
||||
# Test 2: Subscriptions re-established after reconnection
|
||||
- Connect client with subscriptions
|
||||
- Wait for timeout
|
||||
- Verify client reconnects
|
||||
- Verify subscriptions re-established
|
||||
- Verify events still delivered
|
||||
|
||||
# Test 3: Disabled when set to 0
|
||||
- Set max_connection_seconds to 0
|
||||
- Connect client
|
||||
- Wait extended period
|
||||
- Verify client NOT disconnected
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```bash
|
||||
# Test 1: Combined behavior
|
||||
- Start relay (startup cleanup runs)
|
||||
- Connect multiple clients
|
||||
- Create subscriptions
|
||||
- Wait for connection timeout
|
||||
- Verify clients reconnect
|
||||
- Restart relay
|
||||
- Verify clean state
|
||||
|
||||
# Test 2: Load test
|
||||
- Connect 100 clients
|
||||
- Each creates 5 subscriptions
|
||||
- Wait for connection timeout
|
||||
- Verify all clients reconnect
|
||||
- Verify all subscriptions re-established
|
||||
- Monitor CPU usage (should remain low)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Component 1: Startup Cleanup
|
||||
- ✅ Relay starts with zero active subscriptions
|
||||
- ✅ All previous subscriptions marked as disconnected on startup
|
||||
- ✅ Clients successfully reconnect and re-establish subscriptions
|
||||
- ✅ Relay restart can be used as emergency fix for subscription issues
|
||||
- ✅ No errors during startup cleanup process
|
||||
|
||||
### Component 2: Connection Age Limit
|
||||
- ✅ Clients disconnected after configured time period
|
||||
- ✅ Clients automatically reconnect
|
||||
- ✅ Subscriptions re-established after reconnection
|
||||
- ✅ No impact on relay performance
|
||||
- ✅ Configuration parameter works correctly (including disabled state)
|
||||
|
||||
### Overall Success
|
||||
- ✅ CPU usage remains low (<10%)
|
||||
- ✅ No orphaned subscriptions accumulate
|
||||
- ✅ Database size remains stable
|
||||
- ✅ No manual intervention required
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
**New Configuration Parameters:**
|
||||
|
||||
```json
|
||||
{
|
||||
"max_connection_seconds": 86400
|
||||
}
|
||||
```
|
||||
|
||||
**Recommended Settings:**
|
||||
|
||||
- **Production:**
|
||||
- `max_connection_seconds: 86400` (24 hours)
|
||||
|
||||
- **Development:**
|
||||
- `max_connection_seconds: 3600` (1 hour for faster testing)
|
||||
|
||||
- **High-traffic:**
|
||||
- `max_connection_seconds: 43200` (12 hours)
|
||||
|
||||
- **Disabled:**
|
||||
- `max_connection_seconds: 0`
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise after deployment:
|
||||
|
||||
1. **Disable connection age limit:**
|
||||
- Set `max_connection_seconds: 0` in config
|
||||
- Restart relay
|
||||
- Monitor for stability
|
||||
|
||||
2. **Revert code changes:**
|
||||
- Remove `check_connection_age()` call from event loop
|
||||
- Remove `cleanup_all_subscriptions_on_startup()` call from main
|
||||
- Restart relay
|
||||
|
||||
3. **Database cleanup (if needed):**
|
||||
- Manually clean up orphaned subscriptions using SQL:
|
||||
```sql
|
||||
UPDATE subscriptions
|
||||
SET ended_at = strftime('%s', 'now')
|
||||
WHERE event_type = 'created' AND ended_at IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comparison with Original Design
|
||||
|
||||
### Original Design (5 Components)
|
||||
1. Startup cleanup
|
||||
2. Fix WebSocket disconnection logging
|
||||
3. Enhance subscription removal with reason parameter
|
||||
4. Periodic cleanup task (background thread)
|
||||
5. Optimize database VIEW
|
||||
6. Subscription expiration (optional)
|
||||
|
||||
### Simplified Design (2 Components)
|
||||
1. Startup cleanup
|
||||
2. Connection age limit
|
||||
|
||||
### Why Simplified is Better
|
||||
|
||||
**Advantages:**
|
||||
- **Simpler**: 2 components vs 5-6 components
|
||||
- **Faster to implement**: 3-5 hours vs 11-17 hours
|
||||
- **Easier to maintain**: Less code, fewer moving parts
|
||||
- **More reliable**: Fewer potential failure points
|
||||
- **Nostr-aligned**: Leverages client reconnection behavior
|
||||
- **No new threads**: Uses existing event loop
|
||||
- **Universal compatibility**: All clients handle reconnection
|
||||
|
||||
**What We're Not Losing:**
|
||||
- Startup cleanup is identical in both designs
|
||||
- Connection age limit achieves the same goal as periodic cleanup + expiration
|
||||
- Disconnection forces complete cleanup (better than just logging)
|
||||
- Database VIEW optimization not needed if subscriptions don't accumulate
|
||||
|
||||
**Trade-offs:**
|
||||
- Less granular logging (but simpler)
|
||||
- No historical subscription analytics (but cleaner database)
|
||||
- Clients must reconnect periodically (but this is standard Nostr behavior)
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This simplified design solves the subscription accumulation problem with two pragmatic solutions:
|
||||
|
||||
1. **Startup cleanup** ensures every relay restart starts with a clean slate
|
||||
2. **Connection age limit** prevents long-term accumulation by forcing periodic reconnection
|
||||
|
||||
Both solutions align with Nostr's stateless design where clients are expected to handle reconnection. The implementation is simple, maintainable, and leverages existing code patterns.
|
||||
|
||||
**Key Benefits:**
|
||||
- ✅ Solves the root problem (subscription accumulation)
|
||||
- ✅ Simple to implement (3-5 hours total)
|
||||
- ✅ Easy to maintain (minimal code)
|
||||
- ✅ Universal compatibility (all clients handle reconnection)
|
||||
- ✅ No new threads or background tasks
|
||||
- ✅ Configurable and can be disabled if needed
|
||||
- ✅ Relay restart as emergency fix
|
||||
|
||||
**Next Steps:**
|
||||
1. Implement Component 1 (Startup Cleanup)
|
||||
2. Test thoroughly
|
||||
3. Implement Component 2 (Connection Age Limit)
|
||||
4. Test thoroughly
|
||||
5. Deploy to production
|
||||
6. Monitor CPU usage and subscription counts
|
||||
1
nips
1
nips
Submodule nips deleted from 8c45ff5d96
@@ -88,4 +88,5 @@ sudo ufw delete allow 8888/tcp
|
||||
|
||||
lsof -i :7777
|
||||
kill $(lsof -t -i :7777)
|
||||
kill -9 $(lsof -t -i :7777)
|
||||
kill -9 $(lsof -t -i :7777)
|
||||
|
||||
|
||||
BIN
screenshots/2025-11-11_11-57.png
Normal file
BIN
screenshots/2025-11-11_11-57.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 738 KiB |
BIN
screenshots/2025-11-13_15-53.png
Normal file
BIN
screenshots/2025-11-13_15-53.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
screenshots/2025-11-13_15-53_1.png
Normal file
BIN
screenshots/2025-11-13_15-53_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
screenshots/raffles.png
Normal file
BIN
screenshots/raffles.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
@@ -239,7 +239,7 @@ cJSON* query_subscription_details(void) {
|
||||
const char* sql =
|
||||
"SELECT * "
|
||||
"FROM active_subscriptions_log "
|
||||
"ORDER BY created_at DESC LIMIT 100";
|
||||
"ORDER BY created_at DESC";
|
||||
|
||||
// DEBUG: Log the query results for debugging subscription_details
|
||||
DEBUG_LOG("=== SUBSCRIPTION_DETAILS QUERY DEBUG ===");
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#ifdef VERSION
|
||||
#undef VERSION
|
||||
#endif
|
||||
#ifdef VERSION_MAJOR
|
||||
#undef VERSION_MAJOR
|
||||
#endif
|
||||
#ifdef VERSION_MINOR
|
||||
#undef VERSION_MINOR
|
||||
#endif
|
||||
|
||||
@@ -65,6 +65,9 @@ static const struct {
|
||||
{"max_total_subscriptions", "5000"},
|
||||
{"max_filters_per_subscription", "10"},
|
||||
|
||||
// Connection Management
|
||||
{"max_connection_seconds", "86400"}, // 24 hours (0 = disabled)
|
||||
|
||||
// Event Processing Limits
|
||||
{"max_event_tags", "100"},
|
||||
{"max_content_length", "8196"},
|
||||
|
||||
@@ -1807,7 +1807,8 @@ int main(int argc, char* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
// Cleanup orphaned subscriptions from previous runs
|
||||
cleanup_all_subscriptions_on_startup();
|
||||
|
||||
// Start WebSocket Nostr relay server (port from CLI override or configuration)
|
||||
int result = start_websocket_relay(cli_options.port_override, cli_options.strict_port); // Use CLI port override if specified, otherwise config
|
||||
|
||||
@@ -12,8 +12,12 @@
|
||||
// Version information (auto-updated by build system)
|
||||
#define VERSION_MAJOR 1
|
||||
#define VERSION_MINOR 0
|
||||
#define VERSION_PATCH 2
|
||||
#define VERSION "v1.0.2"
|
||||
#define VERSION_PATCH 8
|
||||
#define VERSION "v1.0.8"
|
||||
|
||||
// Avoid VERSION_MAJOR redefinition warning from nostr_core_lib
|
||||
#undef VERSION_MAJOR
|
||||
#define VERSION_MAJOR 1
|
||||
|
||||
// Relay metadata (authoritative source for NIP-11 information)
|
||||
#define RELAY_NAME "C-Relay"
|
||||
|
||||
@@ -999,6 +999,44 @@ void update_subscription_events_sent(const char* sub_id, int events_sent) {
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup all subscriptions on startup
|
||||
void cleanup_all_subscriptions_on_startup(void) {
|
||||
if (!g_db) {
|
||||
DEBUG_ERROR("Database not available for startup cleanup");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOG("Performing startup subscription cleanup");
|
||||
|
||||
// Mark all active subscriptions as disconnected
|
||||
const char* sql =
|
||||
"UPDATE subscriptions "
|
||||
"SET ended_at = strftime('%s', 'now') "
|
||||
"WHERE event_type = 'created' AND ended_at IS NULL";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to prepare startup cleanup query");
|
||||
return;
|
||||
}
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
int changes = sqlite3_changes(g_db);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR("Failed to execute startup cleanup");
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes > 0) {
|
||||
DEBUG_LOG("Startup cleanup: marked %d orphaned subscriptions as disconnected", changes);
|
||||
} else {
|
||||
DEBUG_LOG("Startup cleanup: no orphaned subscriptions found");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -120,4 +120,7 @@ void update_subscription_events_sent(const char* sub_id, int events_sent);
|
||||
// Subscription query functions
|
||||
int has_subscriptions_for_kind(int event_kind);
|
||||
|
||||
// Startup cleanup function
|
||||
void cleanup_all_subscriptions_on_startup(void);
|
||||
|
||||
#endif // SUBSCRIPTIONS_H
|
||||
811
src/websockets.c
811
src/websockets.c
@@ -471,18 +471,720 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
|
||||
case LWS_CALLBACK_RECEIVE:
|
||||
if (len > 0) {
|
||||
DEBUG_TRACE("LWS_CALLBACK_RECEIVE: received %zu bytes", len);
|
||||
|
||||
// Check if client is rate limited for malformed requests
|
||||
if (is_client_rate_limited_for_malformed_requests(pss)) {
|
||||
send_notice_message(wsi, "error: too many malformed requests - temporarily blocked");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if this is a fragmented message
|
||||
int is_first_fragment = lws_is_first_fragment(wsi);
|
||||
int is_final_fragment = lws_is_final_fragment(wsi);
|
||||
size_t remaining_payload = lws_remaining_packet_payload(wsi);
|
||||
|
||||
DEBUG_TRACE("Fragment info: first=%d, final=%d, remaining=%zu, reassembly_active=%d",
|
||||
is_first_fragment, is_final_fragment, remaining_payload, pss->reassembly_active);
|
||||
|
||||
// Handle message reassembly for fragmented messages
|
||||
// Only use reassembly if message is actually fragmented (not both first and final)
|
||||
int is_fragmented = (is_first_fragment && !is_final_fragment) || pss->reassembly_active;
|
||||
if (is_fragmented) {
|
||||
// Start or continue reassembly
|
||||
if (is_first_fragment) {
|
||||
// First fragment - initialize reassembly buffer
|
||||
if (pss->reassembly_buffer) {
|
||||
DEBUG_WARN("Starting new reassembly but buffer already exists - cleaning up");
|
||||
free(pss->reassembly_buffer);
|
||||
pss->reassembly_buffer = NULL; // Ensure buffer is NULL after cleanup
|
||||
}
|
||||
pss->reassembly_size = 0;
|
||||
pss->reassembly_capacity = 0;
|
||||
pss->reassembly_active = 1;
|
||||
DEBUG_TRACE("Starting message reassembly");
|
||||
}
|
||||
|
||||
// Ensure buffer has enough capacity
|
||||
size_t needed_capacity = pss->reassembly_size + len + 1; // +1 for null terminator
|
||||
if (needed_capacity > pss->reassembly_capacity) {
|
||||
size_t new_capacity = pss->reassembly_capacity == 0 ? 8192 : pss->reassembly_capacity * 2;
|
||||
while (new_capacity < needed_capacity) {
|
||||
new_capacity *= 2;
|
||||
}
|
||||
|
||||
char* new_buffer = realloc(pss->reassembly_buffer, new_capacity);
|
||||
if (!new_buffer) {
|
||||
DEBUG_ERROR("Failed to allocate reassembly buffer (capacity %zu)", new_capacity);
|
||||
// Clean up and abort reassembly
|
||||
free(pss->reassembly_buffer);
|
||||
pss->reassembly_buffer = NULL;
|
||||
pss->reassembly_size = 0;
|
||||
pss->reassembly_capacity = 0;
|
||||
pss->reassembly_active = 0;
|
||||
send_notice_message(wsi, "error: message too large - memory allocation failed");
|
||||
return 0;
|
||||
}
|
||||
pss->reassembly_buffer = new_buffer;
|
||||
pss->reassembly_capacity = new_capacity;
|
||||
DEBUG_TRACE("Expanded reassembly buffer to %zu bytes", new_capacity);
|
||||
}
|
||||
|
||||
// Append fragment to buffer
|
||||
memcpy(pss->reassembly_buffer + pss->reassembly_size, in, len);
|
||||
pss->reassembly_size += len;
|
||||
|
||||
// Check if this is the final fragment
|
||||
if (is_final_fragment) {
|
||||
// Message complete - process it
|
||||
pss->reassembly_buffer[pss->reassembly_size] = '\0';
|
||||
pss->reassembly_active = 0;
|
||||
|
||||
DEBUG_TRACE("Message reassembly complete: total size %zu bytes", pss->reassembly_size);
|
||||
|
||||
// Process the complete message
|
||||
char* complete_message = pss->reassembly_buffer;
|
||||
size_t message_len = pss->reassembly_size;
|
||||
|
||||
// Reset reassembly state (keep buffer pointer for processing)
|
||||
pss->reassembly_size = 0;
|
||||
pss->reassembly_active = 0;
|
||||
|
||||
// Parse JSON message
|
||||
DEBUG_TRACE("Parsing reassembled JSON message of length %zu", message_len);
|
||||
cJSON* json = cJSON_Parse(complete_message);
|
||||
|
||||
// Process the message (same logic as before)
|
||||
if (json && cJSON_IsArray(json)) {
|
||||
// Get message type
|
||||
cJSON* type = cJSON_GetArrayItem(json, 0);
|
||||
if (type && cJSON_IsString(type)) {
|
||||
const char* msg_type = cJSON_GetStringValue(type);
|
||||
|
||||
if (strcmp(msg_type, "EVENT") == 0) {
|
||||
// Handle EVENT message
|
||||
cJSON* event = cJSON_GetArrayItem(json, 1);
|
||||
if (event && cJSON_IsObject(event)) {
|
||||
// Extract event JSON string for unified validator
|
||||
char *event_json_str = cJSON_Print(event);
|
||||
if (!event_json_str) {
|
||||
DEBUG_ERROR("Failed to serialize event JSON for validation");
|
||||
cJSON* error_response = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(error_response, cJSON_CreateString("OK"));
|
||||
cJSON_AddItemToArray(error_response, cJSON_CreateString("unknown"));
|
||||
cJSON_AddItemToArray(error_response, cJSON_CreateBool(0));
|
||||
cJSON_AddItemToArray(error_response, cJSON_CreateString("error: failed to process event"));
|
||||
|
||||
char *error_str = cJSON_Print(error_response);
|
||||
if (error_str) {
|
||||
size_t error_len = strlen(error_str);
|
||||
// Use proper message queue system instead of direct lws_write
|
||||
if (queue_message(wsi, pss, error_str, error_len, LWS_WRITE_TEXT) != 0) {
|
||||
DEBUG_ERROR("Failed to queue error response message");
|
||||
}
|
||||
free(error_str);
|
||||
}
|
||||
cJSON_Delete(error_response);
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Call unified validator with JSON string
|
||||
size_t event_json_len = strlen(event_json_str);
|
||||
int validation_result = nostr_validate_unified_request(event_json_str, event_json_len);
|
||||
|
||||
// Map validation result to old result format (0 = success, -1 = failure)
|
||||
int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1;
|
||||
|
||||
// Generate error message based on validation result
|
||||
char error_message[512] = {0};
|
||||
if (result != 0) {
|
||||
switch (validation_result) {
|
||||
case NOSTR_ERROR_INVALID_INPUT:
|
||||
strncpy(error_message, "invalid: malformed event structure", sizeof(error_message) - 1);
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
|
||||
strncpy(error_message, "invalid: signature verification failed", sizeof(error_message) - 1);
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_ID:
|
||||
strncpy(error_message, "invalid: event id verification failed", sizeof(error_message) - 1);
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
|
||||
strncpy(error_message, "invalid: invalid pubkey format", sizeof(error_message) - 1);
|
||||
break;
|
||||
case -103: // NOSTR_ERROR_EVENT_EXPIRED
|
||||
strncpy(error_message, "rejected: event expired", sizeof(error_message) - 1);
|
||||
break;
|
||||
case -102: // NOSTR_ERROR_NIP42_DISABLED
|
||||
strncpy(error_message, "auth-required: NIP-42 authentication required", sizeof(error_message) - 1);
|
||||
break;
|
||||
case -101: // NOSTR_ERROR_AUTH_REQUIRED
|
||||
strncpy(error_message, "blocked: pubkey not authorized", sizeof(error_message) - 1);
|
||||
break;
|
||||
default:
|
||||
strncpy(error_message, "error: validation failed", sizeof(error_message) - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup event JSON string
|
||||
free(event_json_str);
|
||||
|
||||
// Check for NIP-70 protected events
|
||||
if (result == 0) {
|
||||
// Check if event has protected tag ["-"]
|
||||
int is_protected_event = 0;
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (tags && cJSON_IsArray(tags)) {
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 1) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
if (tag_name && cJSON_IsString(tag_name) &&
|
||||
strcmp(cJSON_GetStringValue(tag_name), "-") == 0) {
|
||||
is_protected_event = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_protected_event) {
|
||||
// Check if protected events are enabled using config
|
||||
int protected_events_enabled = get_config_bool("nip70_protected_events_enabled", 0);
|
||||
|
||||
if (!protected_events_enabled) {
|
||||
// Protected events not supported
|
||||
result = -1;
|
||||
strncpy(error_message, "blocked: protected events not supported", sizeof(error_message) - 1);
|
||||
error_message[sizeof(error_message) - 1] = '\0';
|
||||
DEBUG_WARN("Protected event rejected: protected events not enabled");
|
||||
} else {
|
||||
// Protected events enabled - check authentication
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
|
||||
|
||||
if (!pss || !pss->authenticated ||
|
||||
!event_pubkey || strcmp(pss->authenticated_pubkey, event_pubkey) != 0) {
|
||||
// Not authenticated or pubkey mismatch
|
||||
result = -1;
|
||||
strncpy(error_message, "auth-required: protected event requires authentication", sizeof(error_message) - 1);
|
||||
error_message[sizeof(error_message) - 1] = '\0';
|
||||
DEBUG_WARN("Protected event rejected: authentication required");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for admin events (kind 23456) and intercept them
|
||||
if (result == 0) {
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
if (kind_obj && cJSON_IsNumber(kind_obj)) {
|
||||
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||
|
||||
DEBUG_TRACE("Processing event kind %d, message length: %zu", event_kind, message_len);
|
||||
|
||||
// Log reception of Kind 23456 events
|
||||
if (event_kind == 23456) {
|
||||
DEBUG_LOG("Admin event (kind 23456) received");
|
||||
}
|
||||
|
||||
if (event_kind == 23456) {
|
||||
// Enhanced admin event security - check authorization first
|
||||
|
||||
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
|
||||
DEBUG_WARN("Admin event authorization failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(auth_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, auth_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
} else {
|
||||
// Authorization successful - process through admin API
|
||||
|
||||
char admin_error[512] = {0};
|
||||
int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
|
||||
|
||||
// Log results for Kind 23456 events
|
||||
if (event_kind == 23456) {
|
||||
if (admin_result != 0) {
|
||||
char error_result_msg[512];
|
||||
if (strlen(admin_error) > 0) {
|
||||
// Safely truncate admin_error if too long
|
||||
size_t max_error_len = sizeof(error_result_msg) - 50; // Leave room for prefix
|
||||
size_t error_len = strlen(admin_error);
|
||||
if (error_len > max_error_len) {
|
||||
error_len = max_error_len;
|
||||
}
|
||||
char truncated_error[512];
|
||||
memcpy(truncated_error, admin_error, error_len);
|
||||
truncated_error[error_len] = '\0';
|
||||
|
||||
// Use a safer approach to avoid truncation warning
|
||||
size_t prefix_len = snprintf(error_result_msg, sizeof(error_result_msg),
|
||||
"ERROR: Kind %d event processing failed: ", event_kind);
|
||||
if (prefix_len < sizeof(error_result_msg)) {
|
||||
size_t remaining = sizeof(error_result_msg) - prefix_len;
|
||||
size_t copy_len = strlen(truncated_error);
|
||||
if (copy_len >= remaining) {
|
||||
copy_len = remaining - 1;
|
||||
}
|
||||
memcpy(error_result_msg + prefix_len, truncated_error, copy_len);
|
||||
error_result_msg[prefix_len + copy_len] = '\0';
|
||||
}
|
||||
} else {
|
||||
snprintf(error_result_msg, sizeof(error_result_msg),
|
||||
"ERROR: Kind %d event processing failed", event_kind);
|
||||
}
|
||||
DEBUG_ERROR(error_result_msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (admin_result != 0) {
|
||||
DEBUG_ERROR("Failed to process admin event");
|
||||
result = -1;
|
||||
size_t error_len = strlen(admin_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, admin_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
} else {
|
||||
// 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
|
||||
|
||||
char nip17_error[512] = {0};
|
||||
cJSON* response_event = process_nip17_admin_message(event, nip17_error, sizeof(nip17_error), wsi);
|
||||
|
||||
if (!response_event) {
|
||||
// Check if this is an error or if the command was already handled
|
||||
if (strlen(nip17_error) > 0) {
|
||||
// There was an actual error
|
||||
DEBUG_ERROR("NIP-17 admin message processing failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(nip17_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, nip17_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
|
||||
} else {
|
||||
// No error message means the command was already handled (plain text commands)
|
||||
// Store the original gift wrap event in database
|
||||
if (store_event(event) != 0) {
|
||||
DEBUG_ERROR("Failed to store gift wrap event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Store the original gift wrap event in database (unlike kind 23456)
|
||||
if (store_event(event) != 0) {
|
||||
DEBUG_ERROR("Failed to store gift wrap event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
|
||||
cJSON_Delete(response_event);
|
||||
} else {
|
||||
// Broadcast RESPONSE event to matching persistent subscriptions
|
||||
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
|
||||
|
||||
char dm_error[512] = {0};
|
||||
int dm_result = process_dm_stats_command(event, dm_error, sizeof(dm_error), wsi);
|
||||
|
||||
if (dm_result != 0) {
|
||||
DEBUG_ERROR("DM stats command processing failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(dm_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, dm_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
|
||||
} else {
|
||||
// Store the DM event in database
|
||||
if (store_event(event) != 0) {
|
||||
DEBUG_ERROR("Failed to store DM event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store DM event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
// Broadcast DM event to matching persistent subscriptions
|
||||
broadcast_event_to_subscriptions(event);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if this is an ephemeral event (kinds 20000-29999)
|
||||
// Per NIP-01: ephemeral events are broadcast but never stored
|
||||
if (event_kind >= 20000 && event_kind < 30000) {
|
||||
DEBUG_TRACE("Ephemeral event (kind %d) - broadcasting without storage", event_kind);
|
||||
// Broadcast directly to subscriptions without database storage
|
||||
broadcast_event_to_subscriptions(event);
|
||||
} else {
|
||||
DEBUG_TRACE("Storing regular event in database");
|
||||
// Regular event - store in database and broadcast
|
||||
if (store_event(event) != 0) {
|
||||
DEBUG_ERROR("Failed to store event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
DEBUG_LOG("Event stored and broadcast (kind %d)", event_kind);
|
||||
// Broadcast event to matching persistent subscriptions
|
||||
broadcast_event_to_subscriptions(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Event without valid kind - try normal storage
|
||||
DEBUG_WARN("Event without valid kind - trying normal storage");
|
||||
if (store_event(event) != 0) {
|
||||
DEBUG_ERROR("Failed to store event without kind in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
broadcast_event_to_subscriptions(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send OK response
|
||||
cJSON* event_id = cJSON_GetObjectItem(event, "id");
|
||||
if (event_id && cJSON_IsString(event_id)) {
|
||||
cJSON* response = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id)));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : ""));
|
||||
|
||||
char *response_str = cJSON_Print(response);
|
||||
if (response_str) {
|
||||
size_t response_len = strlen(response_str);
|
||||
|
||||
// DEBUG: Log WebSocket frame details before sending
|
||||
DEBUG_TRACE("WS_FRAME_SEND: type=OK len=%zu data=%.100s%s",
|
||||
response_len,
|
||||
response_str,
|
||||
response_len > 100 ? "..." : "");
|
||||
|
||||
// Queue message for proper libwebsockets pattern
|
||||
if (queue_message(wsi, pss, response_str, response_len, LWS_WRITE_TEXT) != 0) {
|
||||
DEBUG_ERROR("Failed to queue OK response message");
|
||||
}
|
||||
|
||||
free(response_str);
|
||||
}
|
||||
cJSON_Delete(response);
|
||||
}
|
||||
}
|
||||
} else if (strcmp(msg_type, "REQ") == 0) {
|
||||
DEBUG_TRACE("REQ message received, starting processing");
|
||||
|
||||
// Check NIP-42 authentication for REQ subscriptions if required
|
||||
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
|
||||
DEBUG_TRACE("REQ rejected: NIP-42 authentication required");
|
||||
if (!pss->auth_challenge_sent) {
|
||||
send_nip42_auth_challenge(wsi, pss);
|
||||
} else {
|
||||
send_notice_message(wsi, "NIP-42 authentication required for subscriptions");
|
||||
DEBUG_WARN("REQ rejected: NIP-42 authentication required");
|
||||
}
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("REQ message passed authentication check");
|
||||
|
||||
// Handle REQ message
|
||||
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||
|
||||
if (sub_id && cJSON_IsString(sub_id)) {
|
||||
const char* subscription_id = cJSON_GetStringValue(sub_id);
|
||||
|
||||
DEBUG_TRACE("Processing REQ message for subscription %s", subscription_id);
|
||||
|
||||
// Validate subscription ID before processing
|
||||
if (!subscription_id) {
|
||||
DEBUG_TRACE("REQ rejected: NULL subscription ID");
|
||||
send_notice_message(wsi, "error: invalid subscription ID");
|
||||
DEBUG_WARN("REQ rejected: NULL subscription ID");
|
||||
record_malformed_request(pss);
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate subscription ID
|
||||
if (!validate_subscription_id(subscription_id)) {
|
||||
DEBUG_TRACE("REQ rejected: invalid subscription ID format");
|
||||
send_notice_message(wsi, "error: invalid subscription ID");
|
||||
DEBUG_WARN("REQ rejected: invalid subscription ID");
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("REQ subscription ID validated: %s", subscription_id);
|
||||
|
||||
// Create array of filter objects from position 2 onwards
|
||||
cJSON* filters = cJSON_CreateArray();
|
||||
if (!filters) {
|
||||
DEBUG_TRACE("REQ failed: could not create filters array");
|
||||
send_notice_message(wsi, "error: failed to process filters");
|
||||
DEBUG_ERROR("REQ failed: could not create filters array");
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
int json_size = cJSON_GetArraySize(json);
|
||||
int filter_count = 0;
|
||||
for (int i = 2; i < json_size; i++) {
|
||||
cJSON* filter = cJSON_GetArrayItem(json, i);
|
||||
if (filter) {
|
||||
cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
|
||||
filter_count++;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_TRACE("REQ created %d filters from message", filter_count);
|
||||
|
||||
// Validate filters before processing
|
||||
char filter_error[512] = {0};
|
||||
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
|
||||
DEBUG_TRACE("REQ rejected: filter validation failed - %s", filter_error);
|
||||
send_notice_message(wsi, filter_error);
|
||||
DEBUG_WARN("REQ rejected: invalid filters");
|
||||
record_malformed_request(pss);
|
||||
cJSON_Delete(filters);
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("REQ filters validated successfully");
|
||||
|
||||
DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id);
|
||||
handle_req_message(subscription_id, filters, wsi, pss);
|
||||
DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id);
|
||||
|
||||
// Clean up the filters array we created
|
||||
cJSON_Delete(filters);
|
||||
|
||||
DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id);
|
||||
|
||||
// Send EOSE (End of Stored Events)
|
||||
cJSON* eose_response = cJSON_CreateArray();
|
||||
if (eose_response) {
|
||||
cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE"));
|
||||
cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id));
|
||||
|
||||
char *eose_str = cJSON_Print(eose_response);
|
||||
if (eose_str) {
|
||||
size_t eose_len = strlen(eose_str);
|
||||
|
||||
// DEBUG: Log WebSocket frame details before sending
|
||||
DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s",
|
||||
eose_len,
|
||||
eose_str,
|
||||
eose_len > 100 ? "..." : "");
|
||||
|
||||
// Queue message for proper libwebsockets pattern
|
||||
if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) {
|
||||
DEBUG_ERROR("Failed to queue EOSE message");
|
||||
}
|
||||
|
||||
free(eose_str);
|
||||
}
|
||||
cJSON_Delete(eose_response);
|
||||
}
|
||||
} else {
|
||||
send_notice_message(wsi, "error: missing or invalid subscription ID in REQ");
|
||||
DEBUG_WARN("REQ rejected: missing or invalid subscription ID");
|
||||
}
|
||||
} else if (strcmp(msg_type, "COUNT") == 0) {
|
||||
// Check NIP-42 authentication for COUNT requests if required
|
||||
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
|
||||
if (!pss->auth_challenge_sent) {
|
||||
send_nip42_auth_challenge(wsi, pss);
|
||||
} else {
|
||||
send_notice_message(wsi, "NIP-42 authentication required for count requests");
|
||||
DEBUG_WARN("COUNT rejected: NIP-42 authentication required");
|
||||
}
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle COUNT message
|
||||
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||
|
||||
if (sub_id && cJSON_IsString(sub_id)) {
|
||||
const char* subscription_id = cJSON_GetStringValue(sub_id);
|
||||
|
||||
// Create array of filter objects from position 2 onwards
|
||||
cJSON* filters = cJSON_CreateArray();
|
||||
int json_size = cJSON_GetArraySize(json);
|
||||
for (int i = 2; i < json_size; i++) {
|
||||
cJSON* filter = cJSON_GetArrayItem(json, i);
|
||||
if (filter) {
|
||||
cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate filters before processing
|
||||
char filter_error[512] = {0};
|
||||
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
|
||||
send_notice_message(wsi, filter_error);
|
||||
DEBUG_WARN("COUNT rejected: invalid filters");
|
||||
record_malformed_request(pss);
|
||||
cJSON_Delete(filters);
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
handle_count_message(subscription_id, filters, wsi, pss);
|
||||
|
||||
// Clean up the filters array we created
|
||||
cJSON_Delete(filters);
|
||||
}
|
||||
} else if (strcmp(msg_type, "CLOSE") == 0) {
|
||||
// Handle CLOSE message
|
||||
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||
if (sub_id && cJSON_IsString(sub_id)) {
|
||||
const char* subscription_id = cJSON_GetStringValue(sub_id);
|
||||
|
||||
// Validate subscription ID before processing
|
||||
if (!subscription_id) {
|
||||
send_notice_message(wsi, "error: invalid subscription ID in CLOSE");
|
||||
DEBUG_WARN("CLOSE rejected: NULL subscription ID");
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate subscription ID
|
||||
if (!validate_subscription_id(subscription_id)) {
|
||||
send_notice_message(wsi, "error: invalid subscription ID in CLOSE");
|
||||
DEBUG_WARN("CLOSE rejected: invalid subscription ID");
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||
return 0;
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Mark subscription as inactive in global manager FIRST
|
||||
// This prevents other threads from accessing it during removal
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
subscription_t* target_sub = g_subscription_manager.active_subscriptions;
|
||||
while (target_sub) {
|
||||
if (strcmp(target_sub->id, subscription_id) == 0 && target_sub->wsi == wsi) {
|
||||
target_sub->active = 0; // Mark as inactive immediately
|
||||
break;
|
||||
}
|
||||
target_sub = target_sub->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
// Now safe to remove from session list
|
||||
if (pss) {
|
||||
pthread_mutex_lock(&pss->session_lock);
|
||||
|
||||
struct subscription** current = &pss->subscriptions;
|
||||
while (*current) {
|
||||
if (strcmp((*current)->id, subscription_id) == 0) {
|
||||
struct subscription* to_remove = *current;
|
||||
*current = to_remove->session_next;
|
||||
pss->subscription_count--;
|
||||
break;
|
||||
}
|
||||
current = &((*current)->session_next);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
}
|
||||
|
||||
// Finally remove from global manager (which will free it)
|
||||
remove_subscription_from_manager(subscription_id, wsi);
|
||||
|
||||
// Subscription closed
|
||||
} else {
|
||||
send_notice_message(wsi, "error: missing or invalid subscription ID in CLOSE");
|
||||
DEBUG_WARN("CLOSE rejected: missing or invalid subscription ID");
|
||||
}
|
||||
} else if (strcmp(msg_type, "AUTH") == 0) {
|
||||
// Handle NIP-42 AUTH message
|
||||
if (cJSON_GetArraySize(json) >= 2) {
|
||||
cJSON* auth_payload = cJSON_GetArrayItem(json, 1);
|
||||
|
||||
if (cJSON_IsString(auth_payload)) {
|
||||
// AUTH challenge response: ["AUTH", <challenge>] (unusual)
|
||||
handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload));
|
||||
} else if (cJSON_IsObject(auth_payload)) {
|
||||
// AUTH signed event: ["AUTH", <event>] (standard NIP-42)
|
||||
handle_nip42_auth_signed_event(wsi, pss, auth_payload);
|
||||
} else {
|
||||
send_notice_message(wsi, "Invalid AUTH message format");
|
||||
DEBUG_WARN("Received AUTH message with invalid payload type");
|
||||
}
|
||||
} else {
|
||||
send_notice_message(wsi, "AUTH message requires payload");
|
||||
DEBUG_WARN("Received AUTH message without payload");
|
||||
}
|
||||
} else {
|
||||
// Unknown message type
|
||||
char unknown_msg[128];
|
||||
snprintf(unknown_msg, sizeof(unknown_msg), "Unknown message type: %.32s", msg_type);
|
||||
DEBUG_WARN(unknown_msg);
|
||||
send_notice_message(wsi, "Unknown message type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the reassembled message
|
||||
if (json) cJSON_Delete(json);
|
||||
// Free the reassembly buffer now that processing is complete
|
||||
free(complete_message);
|
||||
pss->reassembly_buffer = NULL;
|
||||
pss->reassembly_capacity = 0;
|
||||
|
||||
return 0; // Fragmented message processed
|
||||
} else {
|
||||
// Not the final fragment - continue accumulating
|
||||
DEBUG_TRACE("Accumulated %zu bytes so far, waiting for more fragments", pss->reassembly_size);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle non-fragmented messages (original code path)
|
||||
char *message = malloc(len + 1);
|
||||
if (message) {
|
||||
memcpy(message, in, len);
|
||||
message[len] = '\0';
|
||||
|
||||
// Parse JSON message (this is the normal program flow)
|
||||
DEBUG_TRACE("Parsing JSON message of length %zu", strlen(message));
|
||||
cJSON* json = cJSON_Parse(message);
|
||||
if (json && cJSON_IsArray(json)) {
|
||||
// Get message type
|
||||
@@ -682,7 +1384,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
if (kind_obj && cJSON_IsNumber(kind_obj)) {
|
||||
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||
|
||||
DEBUG_TRACE("Processing event kind %d", event_kind);
|
||||
DEBUG_TRACE("Processing event kind %d, message length: %zu", event_kind, strlen(message));
|
||||
|
||||
// Log reception of Kind 23456 events
|
||||
if (event_kind == 23456) {
|
||||
@@ -885,10 +1587,13 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
}
|
||||
}
|
||||
} else if (strcmp(msg_type, "REQ") == 0) {
|
||||
DEBUG_TRACE("REQ message received, starting processing");
|
||||
|
||||
// Log the full REQ message for debugging
|
||||
|
||||
// Check NIP-42 authentication for REQ subscriptions if required
|
||||
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
|
||||
DEBUG_TRACE("REQ rejected: NIP-42 authentication required");
|
||||
if (!pss->auth_challenge_sent) {
|
||||
send_nip42_auth_challenge(wsi, pss);
|
||||
} else {
|
||||
@@ -900,6 +1605,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("REQ message passed authentication check");
|
||||
|
||||
// Handle REQ message
|
||||
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||
|
||||
@@ -910,6 +1617,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
|
||||
// Validate subscription ID before processing
|
||||
if (!subscription_id) {
|
||||
DEBUG_TRACE("REQ rejected: NULL subscription ID");
|
||||
send_notice_message(wsi, "error: invalid subscription ID");
|
||||
DEBUG_WARN("REQ rejected: NULL subscription ID");
|
||||
record_malformed_request(pss);
|
||||
@@ -920,6 +1628,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
|
||||
// Validate subscription ID
|
||||
if (!validate_subscription_id(subscription_id)) {
|
||||
DEBUG_TRACE("REQ rejected: invalid subscription ID format");
|
||||
send_notice_message(wsi, "error: invalid subscription ID");
|
||||
DEBUG_WARN("REQ rejected: invalid subscription ID");
|
||||
cJSON_Delete(json);
|
||||
@@ -927,9 +1636,12 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("REQ subscription ID validated: %s", subscription_id);
|
||||
|
||||
// Create array of filter objects from position 2 onwards
|
||||
cJSON* filters = cJSON_CreateArray();
|
||||
if (!filters) {
|
||||
DEBUG_TRACE("REQ failed: could not create filters array");
|
||||
send_notice_message(wsi, "error: failed to process filters");
|
||||
DEBUG_ERROR("REQ failed: could not create filters array");
|
||||
cJSON_Delete(json);
|
||||
@@ -938,16 +1650,21 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
}
|
||||
|
||||
int json_size = cJSON_GetArraySize(json);
|
||||
int filter_count = 0;
|
||||
for (int i = 2; i < json_size; i++) {
|
||||
cJSON* filter = cJSON_GetArrayItem(json, i);
|
||||
if (filter) {
|
||||
cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
|
||||
filter_count++;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_TRACE("REQ created %d filters from message", filter_count);
|
||||
|
||||
// Validate filters before processing
|
||||
char filter_error[512] = {0};
|
||||
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
|
||||
DEBUG_TRACE("REQ rejected: filter validation failed - %s", filter_error);
|
||||
send_notice_message(wsi, filter_error);
|
||||
DEBUG_WARN("REQ rejected: invalid filters");
|
||||
record_malformed_request(pss);
|
||||
@@ -957,7 +1674,11 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("REQ filters validated successfully");
|
||||
|
||||
DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id);
|
||||
handle_req_message(subscription_id, filters, wsi, pss);
|
||||
DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id);
|
||||
|
||||
// Clean up the filters array we created
|
||||
cJSON_Delete(filters);
|
||||
@@ -1191,6 +1912,15 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
pss->message_queue_count = 0;
|
||||
pss->writeable_requested = 0;
|
||||
|
||||
// Clean up message reassembly buffer
|
||||
if (pss->reassembly_buffer) {
|
||||
free(pss->reassembly_buffer);
|
||||
pss->reassembly_buffer = NULL;
|
||||
}
|
||||
pss->reassembly_size = 0;
|
||||
pss->reassembly_capacity = 0;
|
||||
pss->reassembly_active = 0;
|
||||
|
||||
// Clean up session subscriptions - copy IDs first to avoid use-after-free
|
||||
pthread_mutex_lock(&pss->session_lock);
|
||||
|
||||
@@ -1254,13 +1984,80 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check and disconnect connections that have exceeded max age
|
||||
// This function works by checking connection age through the subscription system
|
||||
static void check_connection_age(int max_connection_seconds) {
|
||||
if (max_connection_seconds <= 0) {
|
||||
return; // Feature disabled
|
||||
}
|
||||
|
||||
time_t current_time = time(NULL);
|
||||
|
||||
// Lock the subscription manager to safely iterate through subscriptions
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
// Track unique WSI pointers we've already checked to avoid duplicate checks
|
||||
struct lws** checked_wsis = NULL;
|
||||
int checked_count = 0;
|
||||
int checked_capacity = 0;
|
||||
|
||||
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||
while (sub) {
|
||||
if (!sub->active || !sub->wsi) {
|
||||
sub = sub->next;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we've already processed this WSI
|
||||
int already_checked = 0;
|
||||
for (int i = 0; i < checked_count; i++) {
|
||||
if (checked_wsis[i] == sub->wsi) {
|
||||
already_checked = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!already_checked) {
|
||||
// Get per-session data to check connection age
|
||||
struct per_session_data *pss = (struct per_session_data *)lws_wsi_user(sub->wsi);
|
||||
|
||||
if (pss && pss->connection_established > 0) {
|
||||
time_t connection_age = current_time - pss->connection_established;
|
||||
|
||||
if (connection_age >= max_connection_seconds) {
|
||||
DEBUG_LOG("Disconnecting client %s: connection age %ld seconds exceeds limit %d seconds",
|
||||
pss->client_ip, connection_age, max_connection_seconds);
|
||||
|
||||
// Close connection with normal closure status
|
||||
lws_close_reason(sub->wsi, LWS_CLOSE_STATUS_NORMAL,
|
||||
(unsigned char*)"Connection age limit reached", 28);
|
||||
}
|
||||
}
|
||||
|
||||
// Add to checked list
|
||||
if (checked_count >= checked_capacity) {
|
||||
checked_capacity = checked_capacity == 0 ? 64 : checked_capacity * 2;
|
||||
checked_wsis = realloc(checked_wsis, checked_capacity * sizeof(struct lws*));
|
||||
}
|
||||
checked_wsis[checked_count++] = sub->wsi;
|
||||
}
|
||||
|
||||
sub = sub->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
// Cleanup
|
||||
free(checked_wsis);
|
||||
}
|
||||
|
||||
// WebSocket protocol definition
|
||||
static struct lws_protocols protocols[] = {
|
||||
{
|
||||
"nostr-relay-protocol",
|
||||
nostr_relay_callback,
|
||||
sizeof(struct per_session_data),
|
||||
4096, // rx buffer size
|
||||
65536, // rx buffer size
|
||||
0, NULL, 0
|
||||
},
|
||||
{ NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
|
||||
@@ -1422,6 +2219,9 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
|
||||
// Static variable for status post timing (initialize to 0 for immediate first post)
|
||||
static time_t last_status_post_time = 0;
|
||||
|
||||
// Static variable for connection age check timing
|
||||
static time_t last_connection_age_check = 0;
|
||||
|
||||
// Main event loop with proper signal handling
|
||||
while (g_server_running && !g_shutdown_flag) {
|
||||
@@ -1444,6 +2244,13 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
generate_and_post_status_event();
|
||||
}
|
||||
}
|
||||
|
||||
// Check connection age limits (every 60 seconds)
|
||||
int max_connection_seconds = get_config_int("max_connection_seconds", 86400);
|
||||
if (max_connection_seconds > 0 && (current_time - last_connection_age_check >= 60)) {
|
||||
last_connection_age_check = current_time;
|
||||
check_connection_age(max_connection_seconds);
|
||||
}
|
||||
}
|
||||
|
||||
lws_context_destroy(ws_context);
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
|
||||
// Filter validation constants
|
||||
#define MAX_FILTERS_PER_REQUEST 10
|
||||
#define MAX_AUTHORS_PER_FILTER 100
|
||||
#define MAX_IDS_PER_FILTER 100
|
||||
#define MAX_KINDS_PER_FILTER 50
|
||||
#define MAX_TAG_VALUES_PER_FILTER 100
|
||||
#define MAX_AUTHORS_PER_FILTER 1000
|
||||
#define MAX_IDS_PER_FILTER 1000
|
||||
#define MAX_KINDS_PER_FILTER 500
|
||||
#define MAX_TAG_VALUES_PER_FILTER 1000
|
||||
#define MAX_KIND_VALUE 65535
|
||||
#define MAX_TIMESTAMP_VALUE 2147483647 // Max 32-bit signed int
|
||||
#define MAX_LIMIT_VALUE 5000
|
||||
@@ -73,6 +73,12 @@ struct per_session_data {
|
||||
struct message_queue_node* message_queue_tail; // Tail of message queue
|
||||
int message_queue_count; // Number of messages in queue
|
||||
int writeable_requested; // Flag: 1 if writeable callback requested
|
||||
|
||||
// Message reassembly for handling fragmented WebSocket messages
|
||||
char* reassembly_buffer; // Buffer for accumulating message fragments (NULL when not reassembling)
|
||||
size_t reassembly_size; // Current size of accumulated data
|
||||
size_t reassembly_capacity; // Allocated capacity of reassembly buffer
|
||||
int reassembly_active; // Flag: 1 if currently reassembling a message
|
||||
};
|
||||
|
||||
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||
|
||||
5
tests/.test_keys.txt
Normal file
5
tests/.test_keys.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# Test key configuration (from make_and_restart_relay.sh -t)
|
||||
ADMIN_PRIVATE_KEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
ADMIN_PUBLIC_KEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
|
||||
RELAY_PUBLIC_KEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
|
||||
RELAY_URL="ws://localhost:8888"
|
||||
295
tests/subscription_cleanup_test.sh
Executable file
295
tests/subscription_cleanup_test.sh
Executable file
@@ -0,0 +1,295 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Subscription Cleanup Testing Suite for C-Relay
|
||||
# Tests startup cleanup and connection age limit features
|
||||
|
||||
set -e
|
||||
|
||||
# Load test keys
|
||||
source "$(dirname "$0")/.test_keys.txt"
|
||||
|
||||
# Configuration
|
||||
RELAY_HOST="127.0.0.1"
|
||||
RELAY_PORT="8888"
|
||||
RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test counters
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Subscription Cleanup Test Suite${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Function to print test header
|
||||
print_test_header() {
|
||||
echo -e "${BLUE}=== Test $1: $2 ===${NC}"
|
||||
}
|
||||
|
||||
# Function to print test result
|
||||
print_result() {
|
||||
local status=$1
|
||||
local message=$2
|
||||
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
|
||||
if [ "$status" = "PASS" ]; then
|
||||
echo -e "${GREEN}[PASS]${NC} $message"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
elif [ "$status" = "FAIL" ]; then
|
||||
echo -e "${RED}[FAIL]${NC} $message"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
else
|
||||
echo -e "${YELLOW}[WARN]${NC} $message"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check if relay is running
|
||||
check_relay_running() {
|
||||
# Send a simple REQ and check for EOSE response
|
||||
local response=$(echo '["REQ","ping",{}]' | timeout 2 websocat -n1 "$RELAY_URL" 2>/dev/null)
|
||||
if echo "$response" | grep -q "EOSE\|EVENT"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create a subscription
|
||||
create_subscription() {
|
||||
local sub_id=$1
|
||||
local filter=${2:-"{}"}
|
||||
|
||||
echo "[\"REQ\",\"$sub_id\",$filter]" | timeout 5 websocat -n1 "$RELAY_URL" 2>/dev/null || echo "TIMEOUT"
|
||||
}
|
||||
|
||||
# Function to close a subscription
|
||||
close_subscription() {
|
||||
local sub_id=$1
|
||||
|
||||
echo "[\"CLOSE\",\"$sub_id\"]" | timeout 5 websocat -n1 "$RELAY_URL" 2>/dev/null || echo "TIMEOUT"
|
||||
}
|
||||
|
||||
# Function to query subscription count from database
|
||||
get_subscription_count() {
|
||||
local db_file=$(find . -name "*.db" -type f 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "$db_file" ]; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
|
||||
sqlite3 "$db_file" "SELECT COUNT(*) FROM subscriptions WHERE event_type='created' AND ended_at IS NULL;" 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
# Test 1: Basic Connectivity
|
||||
print_test_header "1" "Basic Connectivity"
|
||||
|
||||
if check_relay_running; then
|
||||
print_result "PASS" "Relay is running and accepting connections"
|
||||
else
|
||||
print_result "FAIL" "Cannot connect to relay at $RELAY_URL"
|
||||
echo ""
|
||||
echo -e "${RED}ERROR: Relay must be running for tests to proceed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 2: Create Multiple Subscriptions
|
||||
print_test_header "2" "Create Multiple Subscriptions"
|
||||
|
||||
echo "[INFO] Creating 5 test subscriptions..."
|
||||
for i in {1..5}; do
|
||||
response=$(create_subscription "cleanup_test_$i")
|
||||
if echo "$response" | grep -q "EOSE"; then
|
||||
echo "[INFO] Subscription cleanup_test_$i created successfully"
|
||||
else
|
||||
print_result "WARN" "Subscription cleanup_test_$i may not have been created: $response"
|
||||
fi
|
||||
done
|
||||
|
||||
# Give subscriptions time to be logged
|
||||
sleep 2
|
||||
|
||||
# Check subscription count in database
|
||||
active_subs=$(get_subscription_count)
|
||||
echo "[INFO] Active subscriptions in database: $active_subs"
|
||||
|
||||
if [ "$active_subs" -ge 5 ]; then
|
||||
print_result "PASS" "Multiple subscriptions created and logged ($active_subs active)"
|
||||
else
|
||||
print_result "WARN" "Expected at least 5 subscriptions, found $active_subs"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 3: Simulate Orphaned Subscriptions (disconnect without CLOSE)
|
||||
print_test_header "3" "Simulate Orphaned Subscriptions"
|
||||
|
||||
echo "[INFO] Creating subscriptions and disconnecting abruptly..."
|
||||
|
||||
# Create subscriptions in background and kill the connection
|
||||
for i in {6..10}; do
|
||||
(echo "[\"REQ\",\"orphan_test_$i\",{}]" | timeout 2 websocat "$RELAY_URL" &>/dev/null) &
|
||||
pid=$!
|
||||
sleep 0.5
|
||||
kill -9 $pid 2>/dev/null || true
|
||||
done
|
||||
|
||||
sleep 2
|
||||
|
||||
orphaned_subs=$(get_subscription_count)
|
||||
echo "[INFO] Subscriptions after abrupt disconnects: $orphaned_subs"
|
||||
|
||||
if [ "$orphaned_subs" -gt "$active_subs" ]; then
|
||||
print_result "PASS" "Orphaned subscriptions detected ($orphaned_subs total, was $active_subs)"
|
||||
else
|
||||
print_result "WARN" "No increase in orphaned subscriptions detected"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 4: Startup Cleanup (requires relay restart)
|
||||
print_test_header "4" "Startup Cleanup Feature"
|
||||
|
||||
echo "[INFO] This test requires relay restart to verify startup cleanup"
|
||||
echo "[INFO] Current orphaned subscriptions: $orphaned_subs"
|
||||
echo ""
|
||||
echo -e "${YELLOW}[ACTION REQUIRED]${NC} Please restart the relay now with:"
|
||||
echo " ./make_and_restart_relay.sh"
|
||||
echo ""
|
||||
echo -n "Press Enter after relay has restarted to continue..."
|
||||
read
|
||||
|
||||
# Wait for relay to be ready
|
||||
echo "[INFO] Waiting for relay to be ready..."
|
||||
sleep 3
|
||||
|
||||
if ! check_relay_running; then
|
||||
print_result "FAIL" "Relay not responding after restart"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if orphaned subscriptions were cleaned up
|
||||
cleaned_subs=$(get_subscription_count)
|
||||
echo "[INFO] Active subscriptions after restart: $cleaned_subs"
|
||||
|
||||
if [ "$cleaned_subs" -eq 0 ]; then
|
||||
print_result "PASS" "Startup cleanup removed all orphaned subscriptions"
|
||||
elif [ "$cleaned_subs" -lt "$orphaned_subs" ]; then
|
||||
print_result "PASS" "Startup cleanup reduced orphaned subscriptions (from $orphaned_subs to $cleaned_subs)"
|
||||
else
|
||||
print_result "FAIL" "Startup cleanup did not reduce orphaned subscriptions"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 5: Connection Age Limit (requires configuration)
|
||||
print_test_header "5" "Connection Age Limit Feature"
|
||||
|
||||
echo "[INFO] Testing connection age limit feature..."
|
||||
echo "[INFO] Default max_connection_seconds is 86400 (24 hours)"
|
||||
echo ""
|
||||
echo -e "${YELLOW}[INFO]${NC} To test connection age limit with shorter timeout:"
|
||||
echo " 1. Set max_connection_seconds to 60 (1 minute) via admin event"
|
||||
echo " 2. Create a subscription and wait 61 seconds"
|
||||
echo " 3. Connection should be automatically closed"
|
||||
echo ""
|
||||
echo "[INFO] For this test, we'll verify the feature is enabled in config"
|
||||
|
||||
# Create a test subscription to verify connection works
|
||||
response=$(create_subscription "age_test_1")
|
||||
if echo "$response" | grep -q "EOSE"; then
|
||||
print_result "PASS" "Connection age limit feature is operational (subscription created)"
|
||||
else
|
||||
print_result "WARN" "Could not verify connection age limit feature"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 6: Verify Client Reconnection
|
||||
print_test_header "6" "Client Reconnection After Cleanup"
|
||||
|
||||
echo "[INFO] Testing that clients can reconnect after cleanup..."
|
||||
|
||||
# Create a subscription
|
||||
response=$(create_subscription "reconnect_test_1")
|
||||
if echo "$response" | grep -q "EOSE"; then
|
||||
echo "[INFO] First connection successful"
|
||||
|
||||
# Close and reconnect
|
||||
sleep 1
|
||||
response=$(create_subscription "reconnect_test_2")
|
||||
if echo "$response" | grep -q "EOSE"; then
|
||||
print_result "PASS" "Client can reconnect and create new subscriptions"
|
||||
else
|
||||
print_result "FAIL" "Client reconnection failed"
|
||||
fi
|
||||
else
|
||||
print_result "FAIL" "Initial connection failed"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 7: Verify Disabled State (max_connection_seconds = 0)
|
||||
print_test_header "7" "Verify Feature Can Be Disabled"
|
||||
|
||||
echo "[INFO] Connection age limit can be disabled by setting max_connection_seconds=0"
|
||||
echo "[INFO] When disabled, connections remain open indefinitely"
|
||||
echo "[INFO] This is the recommended setting for most relays"
|
||||
|
||||
# Create a long-lived subscription
|
||||
response=$(create_subscription "disabled_test_1")
|
||||
if echo "$response" | grep -q "EOSE"; then
|
||||
print_result "PASS" "Subscriptions work normally when feature is disabled/default"
|
||||
else
|
||||
print_result "WARN" "Could not verify disabled state"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 8: Database Integrity Check
|
||||
print_test_header "8" "Database Integrity After Cleanup"
|
||||
|
||||
echo "[INFO] Checking database integrity..."
|
||||
|
||||
db_file=$(find . -name "*.db" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$db_file" ]; then
|
||||
# Check if database is accessible
|
||||
if sqlite3 "$db_file" "PRAGMA integrity_check;" 2>/dev/null | grep -q "ok"; then
|
||||
print_result "PASS" "Database integrity check passed"
|
||||
else
|
||||
print_result "FAIL" "Database integrity check failed"
|
||||
fi
|
||||
|
||||
# Check subscription table structure
|
||||
if sqlite3 "$db_file" "SELECT COUNT(*) FROM subscriptions;" &>/dev/null; then
|
||||
print_result "PASS" "Subscription table is accessible"
|
||||
else
|
||||
print_result "FAIL" "Subscription table is not accessible"
|
||||
fi
|
||||
else
|
||||
print_result "WARN" "No database file found"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Final Summary
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Test Summary${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo "Total Tests: $TOTAL_TESTS"
|
||||
echo -e "${GREEN}Passed: $PASSED_TESTS${NC}"
|
||||
echo -e "${RED}Failed: $FAILED_TESTS${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $FAILED_TESTS -eq 0 ]; then
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some tests failed. Please review the output above.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
747
tests/test_requests.mjs
Normal file
747
tests/test_requests.mjs
Normal file
@@ -0,0 +1,747 @@
|
||||
|
||||
/**
|
||||
* Nostr Relay Pubkey Filter Test
|
||||
* Tests how many pubkeys different relays can handle in a single filter request
|
||||
*/
|
||||
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
// Configuration
|
||||
const RELAYS = [
|
||||
// "wss://relay.laantungir.net"
|
||||
"ws://127.0.0.1:8888"
|
||||
];
|
||||
|
||||
// Test parameters
|
||||
const STEP_SIZE = 25; // Increment pubkey count by 25 each test
|
||||
const MAX_PUBKEYS = 500; // Maximum pubkeys to test
|
||||
const EVENT_KIND = 1; // Kind 1 = text notes
|
||||
const EVENT_LIMIT = 2; // Only request 2 events per test
|
||||
|
||||
// Generate test pubkey arrays of increasing sizes
|
||||
function generateTestPubkeyArrays() {
|
||||
const testArrays = [];
|
||||
for (let count = STEP_SIZE; count <= MAX_PUBKEYS; count += STEP_SIZE) {
|
||||
testArrays.push(PUBKEYS.slice(0, count));
|
||||
}
|
||||
return testArrays;
|
||||
}
|
||||
|
||||
const PUBKEYS = [
|
||||
"85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204",
|
||||
"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
|
||||
"916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57",
|
||||
"2645caf5706a31767c921532975a079f85950e1006bd5065f5dd0213e6848a96",
|
||||
"8fe3f243e91121818107875d51bca4f3fcf543437aa9715150ec8036358939c5",
|
||||
"83e818dfbeccea56b0f551576b3fd39a7a50e1d8159343500368fa085ccd964b",
|
||||
"a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98",
|
||||
"e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411",
|
||||
"2cde0e02bda47eaeeed65e341619cc5f2afce990164669da4e1e5989180a96b9",
|
||||
"edcd20558f17d99327d841e4582f9b006331ac4010806efa020ef0d40078e6da",
|
||||
"34d2f5274f1958fcd2cb2463dabeaddf8a21f84ace4241da888023bf05cc8095",
|
||||
"c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5",
|
||||
"04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9",
|
||||
"e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42",
|
||||
"1306edd66f1da374adc417cf884bbcff57c6399656236c1f872ee10403c01b2d",
|
||||
"eaf27aa104833bcd16f671488b01d65f6da30163b5848aea99677cc947dd00aa",
|
||||
"472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e",
|
||||
"be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479",
|
||||
"c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15",
|
||||
"c037a6897df86bfd4df5496ca7e2318992b4766897fb18fbd1d347a4f4459f5e",
|
||||
"e41e883f1ef62485a074c1a1fa1d0a092a5d678ad49bedc2f955ab5e305ba94e",
|
||||
"020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e",
|
||||
"e2f28c1ac6dff5a7b755635af4c8436d2fec89b888a9d9548a51b2c63f779555",
|
||||
"29fbc05acee671fb579182ca33b0e41b455bb1f9564b90a3d8f2f39dee3f2779",
|
||||
"090254801a7e8e5085b02e711622f0dfa1a85503493af246aa42af08f5e4d2df",
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
||||
"6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964",
|
||||
"b0b8fbd9578ac23e782d97a32b7b3a72cda0760761359bd65661d42752b4090a",
|
||||
"b7996c183e036df27802945b80bbdc8b0bf5971b6621a86bf3569c332117f07d",
|
||||
"1833ee04459feb2ca4ae690d5f31269ad488c69e5fe903a42b532c677c4a8170",
|
||||
"4adb4ff2dc72bbf1f6da19fc109008a25013c837cf712016972fad015b19513f",
|
||||
"c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0",
|
||||
"368f4e0027fd223fdb69b6ec6e1c06d1f027a611b1ed38eeb32493eb2878bb35",
|
||||
"703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898",
|
||||
"50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63",
|
||||
"6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3",
|
||||
"a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f",
|
||||
"1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b",
|
||||
"19fefd7f39c96d2ff76f87f7627ae79145bc971d8ab23205005939a5a913bc2f",
|
||||
"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
|
||||
"a3b0ce5d70d0db22885706b2b1f144c6864a7e4828acff3f8f01ca6b3f54ad15",
|
||||
"aef0d6b212827f3ba1de6189613e6d4824f181f567b1205273c16895fdaf0b23",
|
||||
"826e9f895b81ab41a4522268b249e68d02ca81608def562a493cee35ffc5c759",
|
||||
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||
"e1055729d51e037b3c14e8c56e2c79c22183385d94aadb32e5dc88092cd0fef4",
|
||||
"27f211f4542fd89d673cfad15b6d838cc5d525615aae8695ed1dcebc39b2dadb",
|
||||
"eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f",
|
||||
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
|
||||
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
|
||||
"00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700",
|
||||
"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194",
|
||||
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954",
|
||||
"175f568d77fb0cb7400f0ddd8aed1738cd797532b314ef053a1669d4dba7433a",
|
||||
"6c535d95a8659b234d5a0805034f5f0a67e3c0ceffcc459f61f680fe944424bf",
|
||||
"9579444852221038dcba34512257b66a1c6e5bdb4339b6794826d4024b3e4ce9",
|
||||
"58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196",
|
||||
"b8e6bf46e109314616fe24e6c7e265791a5f2f4ec95ae8aa15d7107ad250dc63",
|
||||
"84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
|
||||
"387519cafd325668ecffe59577f37238638da4cf2d985b82f932fc81d33da1e8",
|
||||
"b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc",
|
||||
"4d62dd5e6ac55ae2405940f59f6f030a994ec2b3ecc5556c8dc542cce20e46dd",
|
||||
"9c612f8b770f0e3fd35cdac2bc57fcee8561e560504ea25c8b9eff8e03512b3e",
|
||||
"3eeb3de14ec5c48c6c4c9ff80908c4186170eabb74b2a6705a7db9f9922cd61e",
|
||||
"51d7f1b736d1958fa56f113e82a27987a3aca4f0e6d237fa8fc369cc1608c5c0",
|
||||
"c2622c916d9b90e10a81b2ba67b19bdfc5d6be26c25756d1f990d3785ce1361b",
|
||||
"b111d517452f9ef015e16d60ae623a6b66af63024eec941b0653bfee0dd667d4",
|
||||
"d897efcd971f8e5eae08c86b7da66f89b30e761a4a86ac41d907425d15b630fe",
|
||||
"9dea27855974a08fceb48c40fab8432c1a8e3a53a1da22a1ad568595d6010649",
|
||||
"47b630bbcdfa88b1c85f84aa3b68fe6c0102b651ba5d9a23cbd2d07b4f6eecc1",
|
||||
"eb0dc09a61fdfc0df5db1f20c7fc7d83f00c690580fea2e5bac8f99c13f65065",
|
||||
"5c0775b1ae0a5140da9599aa9cd1c5beea55c2d55a5d681808525eb5fce37b32",
|
||||
"b474e6999980aa9e8c9dd6e3720fb03136bfa05aba5fab1634dc0bd8767d412f",
|
||||
"759f7abf05ca710bf2c8da7ad7a9a7df6d0c85db7b2217da524e94e3627b2fbd",
|
||||
"060e7c6ed0dbeb9c8cdc61445ee38b9b08d899d6b617e28064b0916e243ddddb",
|
||||
"f728d9e6e7048358e70930f5ca64b097770d989ccd86854fe618eda9c8a38106",
|
||||
"bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce",
|
||||
"b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27",
|
||||
"c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6",
|
||||
"ad46db12ee250a108756ab4f0f3007b04d7e699f45eac3ab696077296219d207",
|
||||
"59fbee7369df7713dbbfa9bbdb0892c62eba929232615c6ff2787da384cb770f",
|
||||
"d7f0e3917c466f1e2233e9624fbd6d4bd1392dbcfcaf3574f457569d496cb731",
|
||||
"e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2",
|
||||
"6f35047caf7432fc0ab54a28fed6c82e7b58230bf98302bf18350ff71e10430a",
|
||||
"fdd5e8f6ae0db817be0b71da20498c1806968d8a6459559c249f322fa73464a7",
|
||||
"883fea4c071fda4406d2b66be21cb1edaf45a3e058050d6201ecf1d3596bbc39",
|
||||
"a1808558470389142e297d4729e081ab8bdff1ab50d0ebe22ffa78958f7a6ab7",
|
||||
"330fb1431ff9d8c250706bbcdc016d5495a3f744e047a408173e92ae7ee42dac",
|
||||
"a4cb51f4618cfcd16b2d3171c466179bed8e197c43b8598823b04de266cef110",
|
||||
"9c163c7351f8832b08b56cbb2e095960d1c5060dd6b0e461e813f0f07459119e",
|
||||
"0a722ca20e1ccff0adfdc8c2abb097957f0e0bf32db18c4281f031756d50eb8d",
|
||||
"5cad82c898ee66013711945d687f7d9549f645a0118467dae2f5e274c598d6ff",
|
||||
"03b593ef3d95102b54bdff77728131a7c3bdfe9007b0b92cd7c4ad4a0021de25",
|
||||
"d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c",
|
||||
"60d53675f07dee9e7d77910efa44682d87cb532313ba66b8f4449d649172296b",
|
||||
"d3ab33199eb48c6f785072b4a66a8e57814e35d31375cca8c3ceeecc171f30ba",
|
||||
"772bd267dffbff318d1a89f257c3371410111a8b89571dbbefa77af6bfa179f3",
|
||||
"11b9a89404dbf3034e7e1886ba9dc4c6d376f239a118271bd2ec567a889850ce",
|
||||
"0497384b57b43c107a778870462901bf68e0e8583b32e2816563543c059784a4",
|
||||
"5d9ba2c5ee0e86e2c4477b145eb301f2df06063a19f4c4ab9042bd347188ec8e",
|
||||
"5683ffc7ff8a732565135aad56cdff94ebacd9a616d1313aea8ad48a446bfe99",
|
||||
"3004d45a0ab6352c61a62586a57c50f11591416c29db1143367a4f0623b491ca",
|
||||
"b24e32ee9a1c18f2771b53345ed8dbc55b59cbe958e5a165dc01704c3aaa6196",
|
||||
"0a2df905acd5b5be3214a84cb2d4f61b0efb4d9bf05739d51112252504959688",
|
||||
"95361a2b42a26c22bac3b6b6ba4c5cac4d36906eb0cfb98268681c45a301c518",
|
||||
"b07d216f2f0422ec0252dd81a6513b8d0b0c7ef85291fbf5a85ef23f8df78fa7",
|
||||
"064de2497ce621aee2a5b4b926a08b1ca01bce9da85b0c714e883e119375140c",
|
||||
"5a8e581f16a012e24d2a640152ad562058cb065e1df28e907c1bfa82c150c8ba",
|
||||
"a36bdc7952e973b31cb32d4ce3ce21447db66c3149c1b7a3d2450f77f9c7e8f9",
|
||||
"e03cfe011d81424bb60a12e9eb0cb0c9c688c34712c3794c0752e0718b369ef2",
|
||||
"2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884",
|
||||
"b9003833fabff271d0782e030be61b7ec38ce7d45a1b9a869fbdb34b9e2d2000",
|
||||
"4379e76bfa76a80b8db9ea759211d90bb3e67b2202f8880cc4f5ffe2065061ad",
|
||||
"76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa",
|
||||
"d307643547703537dfdef811c3dea96f1f9e84c8249e200353425924a9908cf8",
|
||||
"da0cc82154bdf4ce8bf417eaa2d2fa99aa65c96c77867d6656fccdbf8e781b18",
|
||||
"3511ad63cd9ad760780044b7c815ee55e8e00722b5de271c47ff29367653456c",
|
||||
"f9acb0b034c4c1177e985f14639f317ef0fedee7657c060b146ee790024317ec",
|
||||
"0c371f5ed95076613443e8331c4b60828ed67bcdefaa1698fb5ce9d7b3285ffb",
|
||||
"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49",
|
||||
"053935081a69624466034446eda3374d905652ddbf8217c88708182687a33066",
|
||||
"a305cc8926861bdde5c71bbb6fd394bb4cea6ef5f5f86402b249fc5ceb0ce220",
|
||||
"03a6e50be223dbb49e282764388f6f2ca8826eae8c5a427aa82bb1b61e51d5e6",
|
||||
"a197639863cf175adc96348382a73b4a4a361c6b2e6fc1de61a14244a2f926a1",
|
||||
"3ca7ca157b5975ace02225caf99fdce43f11207c072cb4899c80a414a9c7539d",
|
||||
"02d9f5676fffc339ffe94dfab38bebe21ce117c6f1509d9922a82d454f420da2",
|
||||
"08634a74c9d14479b462389b307695815f9a189e8fb6e058b92e18bd3f537405",
|
||||
"ec7de4aa8758ba9e09a8c89d2757a1fa0e2cc61c20b757af52ae058931c1a33f",
|
||||
"2250f69694c2a43929e77e5de0f6a61ae5e37a1ee6d6a3baef1706ed9901248b",
|
||||
"a9434ee165ed01b286becfc2771ef1705d3537d051b387288898cc00d5c885be",
|
||||
"bd625f1b8c49a79f075f3ebd2d111ff625504cf2ad12442fd70d191dd2f4a562",
|
||||
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
|
||||
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
|
||||
"11d0b66747887ba9a6d34b23eb31287374b45b1a1b161eac54cb183c53e00ef7",
|
||||
"2544cfcb89d7c2f8d3a31ea2ed386ac5189a18f484672436580eec215f9b039c",
|
||||
"d4338b7c3306491cfdf54914d1a52b80a965685f7361311eae5f3eaff1d23a5b",
|
||||
"c43e382ee4835010b9fad18e0a6f50f1ae143b98e089b8bb974232fce4d1f295",
|
||||
"92de68b21302fa2137b1cbba7259b8ba967b535a05c6d2b0847d9f35ff3cf56a",
|
||||
"55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185",
|
||||
"2af01e0d6bd1b9fbb9e3d43157d64590fb27dcfbcabe28784a5832e17befb87b",
|
||||
"35b23cd02d2d75e55cee38fdee26bc82f1d15d3c9580800b04b0da2edb7517ea",
|
||||
"7e0c255fd3d0f9b48789a944baf19bf42c205a9c55199805eb13573b32137488",
|
||||
"ee0e01eb17fc6cb4cd2d9300d2f8945b51056514f156c6bc6d491b74496d161a",
|
||||
"cbc5ef6b01cbd1ffa2cb95a954f04c385a936c1a86e1bb9ccdf2cf0f4ebeaccb",
|
||||
"ec6e36d5c9eb874f1db4253ef02377f7cc70697cda40fbfb24ded6b5d14cce4c",
|
||||
"2779f3d9f42c7dee17f0e6bcdcf89a8f9d592d19e3b1bbd27ef1cffd1a7f98d1",
|
||||
"976713246c36db1a4364f917b98633cbe0805d46af880f6b50a505d4eb32ed47",
|
||||
"8766a54ef9a170b3860bc66fd655abb24b5fda75d7d7ff362f44442fbdeb47b9",
|
||||
"ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14",
|
||||
"e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb",
|
||||
"07eced8b63b883cedbd8520bdb3303bf9c2b37c2c7921ca5c59f64e0f79ad2a6",
|
||||
"532d830dffe09c13e75e8b145c825718fc12b0003f61d61e9077721c7fff93cb",
|
||||
"1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d",
|
||||
"c708943ea349519dcf56b2a5c138fd9ed064ad65ddecae6394eabd87a62f1770",
|
||||
"ccaa58e37c99c85bc5e754028a718bd46485e5d3cb3345691ecab83c755d48cc",
|
||||
"5b0e8da6fdfba663038690b37d216d8345a623cc33e111afd0f738ed7792bc54",
|
||||
"f2c96c97f6419a538f84cf3fa72e2194605e1848096e6e5170cce5b76799d400",
|
||||
"aa55a479ad6934d0fd78f3dbd88515cd1ca0d7a110812e711380d59df7598935",
|
||||
"bd9eb657c25b4f6cda68871ce26259d1f9bc62420487e3224905b674a710a45a",
|
||||
"69a80567e79b6b9bc7282ad595512df0b804784616bedb623c122fad420a2635",
|
||||
"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
|
||||
"df173277182f3155d37b330211ba1de4a81500c02d195e964f91be774ec96708",
|
||||
"675b84fe75e216ab947c7438ee519ca7775376ddf05dadfba6278bd012e1d728",
|
||||
"91c9a5e1a9744114c6fe2d61ae4de82629eaaa0fb52f48288093c7e7e036f832",
|
||||
"24ebb6b58d0b984a965b76f82ce9eff8795cc95085a4d09dedc56949ed596ada",
|
||||
"bb1cf5250435ff475cd8b32acb23e3ee7bbe8fc38f6951704b4798513947672c",
|
||||
"922945779f93fd0b3759f1157e3d9fa20f3fd24c4b8f2bcf520cacf649af776d",
|
||||
"3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d",
|
||||
"e8d67c435a4a59304e1414280e952efe17be4254fca27916bf63f9f73e54aba4",
|
||||
"c1fc7771f5fa418fd3ac49221a18f19b42ccb7a663da8f04cbbf6c08c80d20b1",
|
||||
"8eee8f5a002e533e9f9ffef14c713da449c23f56f4415e7995552075a02d1d37",
|
||||
"c998a5739f04f7fff202c54962aa5782b34ecb10d6f915bdfdd7582963bf9171",
|
||||
"a536ab1f7f3c0133baadbdf472b1ac7ad4b774ed432c1989284193572788bca0",
|
||||
"c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965",
|
||||
"e6ee5b449c220defea6373b8a7e147cabd67c2bdb5016886bf6096a3c7435a61",
|
||||
"a619cf1a888a73211bbf32e0c438319f23e91444d45d7bc88816ed5fcb7e8fa3",
|
||||
"56a6b75373c8f7b93c53bcae86d8ffbaba9f2a1b38122054fcdb7f3bf645b727",
|
||||
"89bfe407c647eb1888871f756516bb1906254fba3132d516ce9099614e37d10c",
|
||||
"b7ed68b062de6b4a12e51fd5285c1e1e0ed0e5128cda93ab11b4150b55ed32fc",
|
||||
"4657dfe8965be8980a93072bcfb5e59a65124406db0f819215ee78ba47934b3e",
|
||||
"d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a",
|
||||
"58ead82fa15b550094f7f5fe4804e0fe75b779dbef2e9b20511eccd69e6d08f9",
|
||||
"fcf6fee0e959c7195dadc5f36fe5a873003b389e7033293b06057c821fcbc9c5",
|
||||
"6681268ace4748d41a4cfcc1e64006fb935bbc359782b3d9611f64d51c6752d9",
|
||||
"e76450df94f84c1c0b71677a45d75b7918f0b786113c2d038e6ab8841b99f276",
|
||||
"a44dbc9aaa357176a7d4f5c3106846ea096b66de0b50ee39aff54baab6c4bf4b",
|
||||
"281e109d2a2899bb0555cf0c3a69b24b3debd61885ca29ef39b95b632be25fe7",
|
||||
"5be6446aa8a31c11b3b453bf8dafc9b346ff328d1fa11a0fa02a1e6461f6a9b1",
|
||||
"e1ff3bfdd4e40315959b08b4fcc8245eaa514637e1d4ec2ae166b743341be1af",
|
||||
"0d6c8388dcb049b8dd4fc8d3d8c3bb93de3da90ba828e4f09c8ad0f346488a33",
|
||||
"9be0be0e64d38a29a9cec9a5c8ef5d873c2bfa5362a4b558da5ff69bc3cbb81e",
|
||||
"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11",
|
||||
"4c7f826edf647462f744b3f16d485f53c797eabdb21cc8a7bb0713283b88e629",
|
||||
"d1621db4d91b23180707b8d4eb9b599fa8ec1dfc2453793a1f83878bd4bbc9d8",
|
||||
"4b74667f89358cd582ad82b16a2d24d5bfcb89ac4b1347ee80e5674a13ba78b2",
|
||||
"83d999a148625c3d2bb819af3064c0f6a12d7da88f68b2c69221f3a746171d19",
|
||||
"b6494a74d18a2dfa3f80ced9fadae35807716fce1071e4de19e2d746b6d87606",
|
||||
"b9c411db4036219e3dfcbe28d60e550b46cce86260fcf2c65d281258e437556f",
|
||||
"2590201e2919a8aa6568c88900192aa54ef00e6c0974a5b0432f52614a841ec8",
|
||||
"c15a5a65986e7ab4134dee3ab85254da5c5d4b04e78b4f16c82837192d355185",
|
||||
"dab6c6065c439b9bafb0b0f1ff5a0c68273bce5c1959a4158ad6a70851f507b6",
|
||||
"baf27a4cc4da49913e7fdecc951fd3b971c9279959af62b02b761a043c33384c",
|
||||
"6c237d8b3b120251c38c230c06d9e48f0d3017657c5b65c8c36112eb15c52aeb",
|
||||
"f173040998481bcb2534a53433eafb8d6ea4c7b0e1fc64572830471fe43fc77d",
|
||||
"36732cc35fe56185af1b11160a393d6c73a1fe41ddf1184c10394c28ca5d627b",
|
||||
"126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f",
|
||||
"457e17b7ea97a845a0d1fa8feda9976596678e3a8af46dc6671d40e050ce857d",
|
||||
"1739d937dc8c0c7370aa27585938c119e25c41f6c441a5d34c6d38503e3136ef",
|
||||
"b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0",
|
||||
"33bd77e5394520747faae1394a4af5fa47f404389676375b6dc7be865ed81452",
|
||||
"fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3",
|
||||
"4f83ef69228b3e09b0abc11ded9e6b85319c0b7fef1a044b8ee9970e38441817",
|
||||
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
|
||||
"d8f38b894b42f7008305cebf17b48925654f22b180c5861b81141f80ccf72848",
|
||||
"9c557e253213c127a86e333ff01c9f12f63091957efafd878d220a0e2cb1193e",
|
||||
"9eab64e92219ccedb15ea9b75ababaa4ae831451019394e0e3336390c3a742d8",
|
||||
"43dedbafef3748c3f9146b961c9b87a3f5cdb1ccb50b4f5890e408702a27a506",
|
||||
"17717ad4d20e2a425cda0a2195624a0a4a73c4f6975f16b1593fc87fa46f2d58",
|
||||
"ee6ea13ab9fe5c4a68eaf9b1a34fe014a66b40117c50ee2a614f4cda959b6e74",
|
||||
"4d023ce9dfd75a7f3075b8e8e084008be17a1f750c63b5de721e6ef883adc765",
|
||||
"d91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075",
|
||||
"4eb88310d6b4ed95c6d66a395b3d3cf559b85faec8f7691dafd405a92e055d6d",
|
||||
"0aeb0814c99a13df50643ca27b831a92aaae6366f54e9c276166347aa037d63a",
|
||||
"16f1a0100d4cfffbcc4230e8e0e4290cc5849c1adc64d6653fda07c031b1074b",
|
||||
"148d1366a5e4672b1321adf00321778f86a2371a4bdbe99133f28df0b3d32fa1",
|
||||
"7076f6592de184f9e912c617c46e5e83bad91d3b7f88b7b54cc73bf8ca493321",
|
||||
"dea51494fec5947d27ca659b73dd281ff5bdba3f89f5da1977a731ad0c22e725",
|
||||
"aa97e3d392b97c21327081cdb3cb674dfa8c9c663493db43799a4079555ad1b1",
|
||||
"b7dfbebf760efb2c756b3cae22baafdbbdf55abb782f85e93b4db804d5cba7e3",
|
||||
"ca696d7edb4a86be7c7d9872bd2f9a44440cf8e2de7853536cbb3a60ae89641f",
|
||||
"ff27d01cb1e56fb58580306c7ba76bb037bf211c5b573c56e4e70ca858755af0",
|
||||
"58dece9ff68d021afe76d549b9e48e6cb7b06a5c14cdf45332c8ed7321d6f211",
|
||||
"78688c1f371e7b923d95368c9298cca06c1ec0a89ea897aa181bd60091121fea",
|
||||
"9e8dd91d21e867dec464868a8d1f4a27c0e113c53e32f2bec0a7c6e25ad2e9d5",
|
||||
"6d028aa49aa1f584b3d35aee9fcee8e3c0d81108114289aa046a7969b21eb5f5",
|
||||
"2c470abbac95a49cd0ed5b3b9e628ffda1dbb03c14caba1a225a9b8bf1dc9d5f",
|
||||
"9d7af6946b320b3ba6b4d386de2b2cf3f8ac52fdcb63f3343d1a8362693a3ce5",
|
||||
"d7a4345c3ead1ea7a34bd6aae43c63cbd81941d9ba019fe972843e5ce78e3187",
|
||||
"00f471f6312ce408f7eb56592a2b6c6b5f54ac2967c77f4c1498903b598e1b16",
|
||||
"41e9e2c8398583b90204f8e35c2a4c036aeebac6d05dbdc3e7fb44a1d6bd65a2",
|
||||
"0d978064b9054c023111926050d983573dac2aff16bb8a7497fde8ad725357c0",
|
||||
"507f5bf8367c1883f115ddf9ee95f79ea693c720eb5a5a8718443a99fa308954",
|
||||
"9a090f86adf9fbdc37de2a14745e73d6e1fd096d3da0670b6795ce5ad3cfeea3",
|
||||
"f2b7c5787424c8f9cf6c4480eb99f4a3770cc06337a4f0d1b109ba849b464193",
|
||||
"6ffe93bb72d4ac788fd8be2dadf5bb4a2f14a330d530b0e68bd733c9744c6619",
|
||||
"3ebc74907d1f928f209ef210e872cac033eaf3ff89e6853286d45d91e351ef9e",
|
||||
"da56c54b5e6749d65ad038c196478794af94e4fa5a4efdc20b49981e4ec566c3",
|
||||
"266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
|
||||
"5b705e6cb602425c019202dd070a0c009b040ac19960eeef2d8a8fab25c1efe5",
|
||||
"9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437",
|
||||
"e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f",
|
||||
"40b9c85fffeafc1cadf8c30a4e5c88660ff6e4971a0dc723d5ab674b5e61b451",
|
||||
"efe5d120df0cc290fa748727fb45ac487caad346d4f2293ab069e8f01fc51981",
|
||||
"408f636bd26fcc5f29889033b447cb2411f60ab1b8a5fc8cb3842dab758fdeb5",
|
||||
"d8a2c33f2e2ff3a9d4ff2a5593f3d5a59e9167fa5ded063d0e49891776611e0c",
|
||||
"02a11d1545114ab63c29958093c91b9f88618e56fee037b9d2fabcff32f62ea9",
|
||||
"1bbd7fdf68eaf5c19446c3aaf63b39dd4a8e33548bc96f6bd239a4124d8f229e",
|
||||
"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11",
|
||||
"167e7fe01a76b6bec9d2a9b196b18c72e150e985fbeb46ee651869e7b4032785",
|
||||
"c88f94f0a391b9aaa1ffefd645253b1a968b0a422a876ea48920a95d45c33f47",
|
||||
"cd169bd8fbd5179e2a8d498ffc31d3ae0e40825ff2b8a85ea359c4455a107ca8",
|
||||
"fd38f135ef675eac5e93d5b2a738c41777c250188031caf1dcf07b1687a1fe49",
|
||||
"6b4c612991132cf4c6c390dceaae75041b9954ba4f9c465aca70beb350815a57",
|
||||
"8fec426247845bdd26f36ae4f737508c15dbec07d43ce18f8c136ab9e35ac212",
|
||||
"af551accea482000bdccb34bd3c521558e1f353773a3caed83a147921c369ea1",
|
||||
"a664a4973cd23e9f3b35a62429f7671aba2c2ae68c03313913b5c2d77269d906",
|
||||
"18f54af1e10c5bb7a35468b0f62b295d12347903c9f95738d065c84bef1402ef",
|
||||
"be39043cc12efbddfee564d95da751a71df6c139e2def45c431cadeb4a573ca3",
|
||||
"01ddee289b1a2e90874ca3428a7a414764a6cad1abfaa985c201e7aada16d38c",
|
||||
"da25cf7b457bddb6b7bc8e1b0146c0fa85373807d6efdac955199fd01fd53c1f",
|
||||
"ec380784d96b93d404166a6bf1a778227a94a02bdf499f94b5f48f61f5b1350f",
|
||||
"6538925ebfb661f418d8c7d074bee2e8afd778701dd89070c2da936d571e55c3",
|
||||
"9edd72eb23222c969379d90d60ec82891b7c827188bb28510a863f59cb697b0a",
|
||||
"09222857afceb23c66c99fc93d8e5ebda6d7aad901eb38af73c508f117685012",
|
||||
"744ecc9a119a92da88b1f448b4030cdbc2fec5c37ea06ebdd026e742b002af7f",
|
||||
"f531a8672baa2415b271e866dbe11fb08640f6c0e5d98f918bd0308e7169b5b7",
|
||||
"44dc1c2db9c3fbd7bee9257eceb52be3cf8c40baf7b63f46e56b58a131c74f0b",
|
||||
"89e14be49ed0073da83b678279cd29ba5ad86cf000b6a3d1a4c3dc4aa4fdd02c",
|
||||
"8fb140b4e8ddef97ce4b821d247278a1a4353362623f64021484b372f948000c",
|
||||
"72f9755501e1a4464f7277d86120f67e7f7ec3a84ef6813cc7606bf5e0870ff3",
|
||||
"3d99feac152027ede63326aa4f43d4ca88e4cd27296b96fe18c55d496a8f6340",
|
||||
"2540d50aeb9be889c3bd050c9cc849b57b156a2759b48084a83db59aa9056eb4",
|
||||
"b66be78da89991544a05c3a2b63da1d15eefe8e9a1bb6a4369f8616865bd6b7c",
|
||||
"2f5de0003db84ecd5449128350c66c7fb63e9d02b250d84af84f463e2f9bcef1",
|
||||
"2045369fc115b138d1438f98d3c29916986c9fde6b8203f7ff8699f0faee1c93",
|
||||
"ae1008d23930b776c18092f6eab41e4b09fcf3f03f3641b1b4e6ee3aa166d760",
|
||||
"1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139",
|
||||
"b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a",
|
||||
"ac3f6afe17593f61810513dac9a1e544e87b9ce91b27d37b88ec58fbaa9014aa",
|
||||
"d1f3c71639ae3bba17ffc6c8deb1fdb3a56506b3492213d033528cc291523704",
|
||||
"6b4a29bbd43d1d0eeead384f512dbb591ce9407d27dba48ad54b00d9d2e1972b",
|
||||
"84de08882b6e36705cf6592ee58e632dd6e092dd61c13192fc80cbbc0cbc82cc",
|
||||
"d3d74124ddfb5bdc61b8f18d17c3335bbb4f8c71182a35ee27314a49a4eb7b1d",
|
||||
"a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d",
|
||||
"eb7246eb8e26b0c48dd4f9c2a822a0f4d5c84138937195090932b61a2d756051",
|
||||
"683211bd155c7b764e4b99ba263a151d81209be7a566a2bb1971dc1bbd3b715e",
|
||||
"5468bceeb74ce35cb4173dcc9974bddac9e894a74bf3d44f9ca8b7554605c9ed",
|
||||
"78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d",
|
||||
"2754fc862d6bc0b7c3971046612d942563d181c187a391e180ed6b00f80e7e5b",
|
||||
"f1725586a402c06aec818d1478a45aaa0dc16c7a9c4869d97c350336d16f8e43",
|
||||
"6a359852238dc902aed19fbbf6a055f9abf21c1ca8915d1c4e27f50df2f290d9",
|
||||
"9e4954853fca260cecf983f098e5204c68b2bdfebde91f1f7b25c10b566d50f8",
|
||||
"3356de61b39647931ce8b2140b2bab837e0810c0ef515bbe92de0248040b8bdd",
|
||||
"3e294d2fd339bb16a5403a86e3664947dd408c4d87a0066524f8a573ae53ca8e",
|
||||
"21335073401a310cc9179fe3a77e9666710cfdf630dfd840f972c183a244b1ad",
|
||||
"987096ef8a2853fea1a31b0ed5276503da291536f167bbf7f3f991c9f05d6d7f",
|
||||
"7a78fbfec68c2b3ab6084f1f808321ba3b5ea47502c41115902013e648e76288",
|
||||
"c12a2bcb002fd74b4d342f9b942c24c44cc46d5ed39201245a8b6f4162e7efce",
|
||||
"8867bed93e89c93d0d8ac98b2443c5554799edb9190346946b12e03f13664450",
|
||||
"9b61cd02adac4b18fbcc06237e7469b07e276faf6ec4ecb34b030c2e385892a0",
|
||||
"0463223adf38df9a22a7fb07999a638fdd42d8437573e0bf19c43e013b14d673",
|
||||
"9989500413fb756d8437912cc32be0730dbe1bfc6b5d2eef759e1456c239f905",
|
||||
"17e2889fba01021d048a13fd0ba108ad31c38326295460c21e69c43fa8fbe515",
|
||||
"6c6c253fe26a5b2abf440124e35dcaa39e891cd28274431ba49da5c11d89747d",
|
||||
"9d065f84c0cba7b0ef86f5d2d155e6ce01178a8a33e194f9999b7497b1b2201b",
|
||||
"5ffb8e1b6b629c0e34a013f9298ebb0759b98a3d24029916321d5eb4255b6735",
|
||||
"3fc5f8553abd753ac47967c4c468cfd08e8cb9dee71b79e12d5adab205bc04d3",
|
||||
"ff82c8b53aa53a9705200690b91c572e2e4918f1a88de5d923ac06fa4560fa19",
|
||||
"4d4ab737e2fbb5af0fd590b4b7e8c6fe76d3a02a9791ef7fdacf601f9e50fad8",
|
||||
"5eca50a04afaefe55659fb74810b42654e2268c1acca6e53801b9862db74a83a",
|
||||
"d700fc10d457eeae4f02eb04d715a054837e68a2e2d010971382c5e1016dc99e",
|
||||
"af321973db865bb33fbc50a4de67fc0e6808d812c6e4dfd9cbc2fd50275b1dfd",
|
||||
"bbf923aa9246065f88c40c7d9bf61cccc0ff3fcff065a8cb2ff4cfbb62088f1e",
|
||||
"268b948b5aab4bab0e5430ee49e3cff11776cf183df93b32159f9670ed541495",
|
||||
"3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594",
|
||||
"4d4fb5ff0afb8c04e6c6e03f51281b664576f985e5bc34a3a7ee310a1e821f47",
|
||||
"9b12847f3d28bf8850ebc03f8d495a1ae8f9a2c86dbda295c90556619a3ee831",
|
||||
"733c5427f55ceba01a0f6607ab0fd11832bbb27d7db17b570e7eb7b68a081d9a",
|
||||
"4bc7982c4ee4078b2ada5340ae673f18d3b6a664b1f97e8d6799e6074cb5c39d",
|
||||
"afa0f26dbf3e674630d1cd6499e86c14f316cd4f78c6ab73bb85b00aa9c50a57",
|
||||
"c301f13372c8f0d9bc8186d874fa45fa33aede13e66f4187a3bd22ee41c95b2a",
|
||||
"548a29f145187fc97689f8ae67944627723c315c163b0dbb88842e50c681d7ca",
|
||||
"d0a1ffb8761b974cec4a3be8cbcb2e96a7090dcf465ffeac839aa4ca20c9a59e",
|
||||
"faaf47af27e3de06e83f346fc6ccea0aabfc7520d82ffe90c48dfcd740c69caa",
|
||||
"3eacaa768326d7dce80f6ee17ada199bebe7eb3c1a60b39b14e0a58bbac66fe4",
|
||||
"7f5237e9f77a22c4a89624c7ac31cae797d8ac4144b02493890d54fee7399bcd",
|
||||
"d84517802a434757c56ae8642bffb4d26e5ade0712053750215680f5896e579b",
|
||||
"bdb96ad31ac6af123c7683c55775ee2138da0f8f011e3994d56a27270e692575",
|
||||
"aab1b0caf13b9bd26a62cf8b3b20f9bfaa0e56f3ec42196a00fedf432e07d739",
|
||||
"c230edd34ca5c8318bf4592ac056cde37519d395c0904c37ea1c650b8ad4a712",
|
||||
"ce41c1698a8c042218bc586f0b9ec8d5bffa3dcbcea09bd59db9d0d92c3fc0b4",
|
||||
"b9a537523bba2fcdae857d90d8a760de4f2139c9f90d986f747ce7d0ec0d173d",
|
||||
"1a6e0aeff1dba7ba121fbeb33bf3162901495df3fcb4e4a40423e1c10edf0dca",
|
||||
"21b419102da8fc0ba90484aec934bf55b7abcf75eedb39124e8d75e491f41a5e",
|
||||
"2183e94758481d0f124fbd93c56ccaa45e7e545ceeb8d52848f98253f497b975",
|
||||
"e3f98bfb9cbeb7563a139983602e50f616cb7ebb06c3295b8ee377328f051206",
|
||||
"b5b9b84d1723994d06013606227fb5b91f9de8820b04cf848d1dccc23d054f39",
|
||||
"07adfda9c5adc80881bb2a5220f6e3181e0c043b90fa115c4f183464022968e6",
|
||||
"facdaf1ce758bdf04cdf1a1fa32a3564a608d4abc2481a286ffc178f86953ef0",
|
||||
"efc83f01c8fb309df2c8866b8c7924cc8b6f0580afdde1d6e16e2b6107c2862c",
|
||||
"52b4a076bcbbbdc3a1aefa3735816cf74993b1b8db202b01c883c58be7fad8bd",
|
||||
"c6f7077f1699d50cf92a9652bfebffac05fc6842b9ee391089d959b8ad5d48fd",
|
||||
"e7424ad457e512fdf4764a56bf6d428a06a13a1006af1fb8e0fe32f6d03265c7",
|
||||
"27797bd4e5ee52db0a197668c92b9a3e7e237e1f9fa73a10c38d731c294cfc9a",
|
||||
"7bdef7bdebb8721f77927d0e77c66059360fa62371fdf15f3add93923a613229",
|
||||
"3335d373e6c1b5bc669b4b1220c08728ea8ce622e5a7cfeeb4c0001d91ded1de",
|
||||
"645681b9d067b1a362c4bee8ddff987d2466d49905c26cb8fec5e6fb73af5c84",
|
||||
"06b7819d7f1c7f5472118266ed7bca8785dceae09e36ea3a4af665c6d1d8327c",
|
||||
"7a6b8c7de171955c214ded7e35cc782cd6dddfd141abb1929c632f69348e6f49",
|
||||
"eb882b0bb659bf72235020a0b884c4a7d817e0af3903715736b146188b1d0868",
|
||||
"2ae6c71323a225ecfa8cf655600ebbe12b1019ff36bf02726d82d095aab29729",
|
||||
"c2f85a06279a7bfa7f2477a3cee907990231a13d17b54524738504bd12e0c86c",
|
||||
"f1b911af1c7a56073e3b83ba7eaa681467040e0fbbdd265445aa80e65c274c22",
|
||||
"a54c2ae6ec6ac06b4d7b45c483eab86ac226b8ecfa99163ef7cc000da9b40895",
|
||||
"bbc73cae41502ddad7a4112586dcaf4422810d60aa4b57c637ccd1a746b07844",
|
||||
"218238431393959d6c8617a3bd899303a96609b44a644e973891038a7de8622d",
|
||||
"59cacbd83ad5c54ad91dacf51a49c06e0bef730ac0e7c235a6f6fa29b9230f02",
|
||||
"ba80990666ef0b6f4ba5059347beb13242921e54669e680064ca755256a1e3a6",
|
||||
"031db544f3158556508b321db59afd62c5bb1592021e5dfd9ff87dca0ad27d8c",
|
||||
"ee85604f8ec6e4e24f8eaf2a624d042ebd431dae448fe11779adcfb6bb78575e",
|
||||
"266ee74062e8dae0aeddfcd0f72725107598efaa80c1a7176d6ee6dd302bce4c",
|
||||
"b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450",
|
||||
"dbe0605a9c73172bad7523a327b236d55ea4b634e80e78a9013db791f8fd5b2c",
|
||||
"1e53e900c3bbc5ead295215efe27b2c8d5fbd15fb3dd810da3063674cb7213b2",
|
||||
"832a2b3cef4b1754c4a7572964a44db64d19edf627ec45179b519d0a5eae8199",
|
||||
"4c800257a588a82849d049817c2bdaad984b25a45ad9f6dad66e47d3b47e3b2f",
|
||||
"3743244390be53473a7e3b3b8d04dce83f6c9514b81a997fb3b123c072ef9f78",
|
||||
"f96c3d76497074c4c83a7b3823380e77dc73d5a9494fd2e053e4a1453e17824b",
|
||||
"d04ecf33a303a59852fdb681ed8b412201ba85d8d2199aec73cb62681d62aa90",
|
||||
"0cca6201658d5d98239c1511ef402562ff7d72446fb201a8d1857c39e369c9fa",
|
||||
"61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9",
|
||||
"7adb520c3ac7cb6dc8253508df0ce1d975da49fefda9b5c956744a049d230ace",
|
||||
"7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19",
|
||||
"9ec078ef9ca31e1bdbb97175dde1cb00bf9f7225e6f622ccc8d367302e220497",
|
||||
"93e174736c4719f80627854aca8b67efd0b59558c8ece267a8eccbbd2e7c5535",
|
||||
"e62f419a0e16607b96ff10ecb00249af7d4b69c7d121e4b04130c61cc998c32e",
|
||||
"171ddd43dab1af0d1fb14029287152a4c89296890e0607cf5e7ba73c73fdf1a5",
|
||||
"604e96e099936a104883958b040b47672e0f048c98ac793f37ffe4c720279eb2",
|
||||
"7726c437ccf791f6ded97dbac1846e62019e5fbd24f42e9db2f640f231c3c09a",
|
||||
"1bf9f239dca1636149bc2f3fc334077ae959ea9607cacf945ef8f8bb227dc5e1",
|
||||
"fcd818454002a6c47a980393f0549ac6e629d28d5688114bb60d831b5c1832a7",
|
||||
"56172b53f730750b40e63c501b16068dd96a245e7f0551675c0fec9817ee96e0",
|
||||
"260d3a820b7f8de20f4972725999b1af88b0cc5554ca38f9681c8d657e043cc3",
|
||||
"9ba8c688f091ca48de2b0f9bc998e3bc36a0092149f9201767da592849777f1c",
|
||||
"61594d714aa94fe551f604123578c4a6592145f4228ad8601224b1b89ce401b0",
|
||||
"416ca193aa5448b8cca1f09642807765cc0ee299609f972df0614cfb8ea2f2b1",
|
||||
"9b6d95b76a01191a4c778185681ed7f3bced2fffa8e41516ec78240b213285f5",
|
||||
"ee0304bae0d4679bb34347ce3b1b80482262b9812bd0c0d5e19a5e2445043b75",
|
||||
"8027a1877f39e603dafc63279e004b4ed9df861d18ce81d9c43c7d7135da8f65",
|
||||
"42b409ff9b261a985227b1ab92707e706777ac14de24654d7e05f0501b37e003",
|
||||
"99097983b74c70800b182abc6f64046ab70407e9cabcd6cf570a38ada9ef75d5",
|
||||
"de8ca7a6b3f7314e91921d4dc5e915fb7bc2bd32129ea6966322effa48050c4c",
|
||||
"dcb302978215f54f33c3d2d7157ef69fd5058cf488fc43dd75c32b5dcaf47e7a",
|
||||
"7c765d407d3a9d5ea117cb8b8699628560787fc084a0c76afaa449bfbd121d84",
|
||||
"06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71",
|
||||
"59f1b5faf29904fe94a6a042e2d82d80d68fc16ad7651eba90a8de39f63f8fe8",
|
||||
"174398550d1468a41b98a09f496c38d3694feadef0f0073fd557610384bafb10",
|
||||
"00d52016bd7e4aae8bf8eaa23f42276b649fe557483b5d7684702633dd0fd944",
|
||||
"9a39bf837c868d61ed8cce6a4c7a0eb96f5e5bcc082ad6afdd5496cb614a23fb",
|
||||
"74ffc51cc30150cf79b6cb316d3a15cf332ab29a38fec9eb484ab1551d6d1856",
|
||||
"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322",
|
||||
"6c6e3e05e1c9d2aae0ed2431544aea411771dd9d81017539af0fd818b2389f28",
|
||||
"23a2cf63ec81e65561acafc655898d2fd0ef190084653fa452413f75e5a3d5bc",
|
||||
"f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8",
|
||||
"e3aefda887252a72cee3578d33b2dcd90e9fe53b8bed6347ef5e26f74211adbb",
|
||||
"6b4ec98f02e647e01440b473bbd92a7fae21e01b6aa6c65e32db94a36092272e",
|
||||
"623ed218de81311783656783d6ce690b521a89c4dc09f28962e5bfd4fa549249",
|
||||
"9ce71f1506ccf4b99f234af49bd6202be883a80f95a155c6e9a1c36fd7e780c7",
|
||||
"139fcc6bb304b2879974c59cda61d86d7816ad4ac0f38ee7a724df488060e65d",
|
||||
"0e8c41eb946657188ea6f2aac36c25e393fff4d4149a83679220d66595ff0faa",
|
||||
"59ffbe1fc829decf90655438bd2df3a7b746ef4a04634d4ee9e280bb6ce5f14e",
|
||||
"e4f695f05bb05b231255ccce3d471b8d79c64a65bccc014662d27f0f7e921092",
|
||||
"39cc53c9e3f7d4980b21bea5ebc8a5b9cdf7fa6539430b5a826e8ad527168656",
|
||||
"685fb49563864326e78df461468795b7e47849a27e713281cd8bb75c0547936d",
|
||||
"05e4649832dfb8d1bfa81ea7cbf1c92c4f1cd5052bfc8d5465ba744aa6fa5eb8",
|
||||
"e5cece49ae3fc2c81f50c8e7a93a5fb1e1585380c467e4822234b64a94add617",
|
||||
"dda028cd1b806b4d494cc7f2789b6c2bd7e3c28ff6a267d03acc5ac6e69a05e0",
|
||||
"046c436b2a525059867b40c81e469b6d83001442fc65312c87a7ce7abeb022ff",
|
||||
"676ffea2ec31426a906d7795d7ebae2ba5e61f0b9fa815995b4a299dd085d510",
|
||||
"15b5cf6cdf4fd1c02f28bcce0f197cafae4c8c7c66a3e2e23af9fe610875315e",
|
||||
"c0fb5367cfcb803c5383f98e26524bed9176e6211588f53ec63fe6079cbfd3df",
|
||||
"7ab00f596b0286b77f78af567ee1be2536feee41daee67bd872f1480b7aa65b9",
|
||||
"e8d66519e43b1214ac68f9f2bdbc4386d41ac66b20c5a260b9b04102784074e9",
|
||||
"e6618db6961dc7b91478e0fa78c4c1b6699009981526693bd5e273972550860c",
|
||||
"b1e1185884a6d14bbfce3899cb53e8183adde642f264d0ff4f1745371e06134c",
|
||||
"cae5b7ea348afefc4c102bb7b125c4928f114739a27b877c6bcfbe5a79280384",
|
||||
"ecbe372132a9323b813eeb48f8dfcedaeca00e2887af181b063c6cfa13ed8ea1",
|
||||
"52387c6b99cc42aac51916b08b7b51d2baddfc19f2ba08d82a48432849dbdfb2",
|
||||
"3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da",
|
||||
"f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9",
|
||||
"fd0266485777bd73e97c7c37f520c83c82e362fe4c25a6be93f3380083d4646b",
|
||||
"433e80c14ff7b8e16e179ccec35f55833df7dd5a5a063d23117b4b01b6f97170",
|
||||
"b7c6f6915cfa9a62fff6a1f02604de88c23c6c6c6d1b8f62c7cc10749f307e81",
|
||||
"ddf03aca85ade039e6742d5bef3df352df199d0d31e22b9858e7eda85cb3bbbe",
|
||||
"d36e8083fa7b36daee646cb8b3f99feaa3d89e5a396508741f003e21ac0b6bec",
|
||||
"ec79b568bdea63ca6091f5b84b0c639c10a0919e175fa09a4de3154f82906f25",
|
||||
"8cd2d0f8310f7009e94f50231870756cb39ba68f37506044910e2f71482b1788",
|
||||
"0eef96197f5c6be3859b6817e6a5736685856c416e29a2925bd5a15b2a57c8b1",
|
||||
"04918dfc36c93e7db6cc0d60f37e1522f1c36b64d3f4b424c532d7c595febbc5",
|
||||
"c8383d81dd24406745b68409be40d6721c301029464067fcc50a25ddf9139549",
|
||||
"3b3a42d34cf0a1402d18d536c9d2ac2eb1c6019a9153be57084c8165d192e325",
|
||||
"da18e9860040f3bf493876fc16b1a912ae5a6f6fa8d5159c3de2b8233a0d9851",
|
||||
"e3fc673fc5f99cc554d0ff47756795647d25cb6e6658f912d114ae6429d35d35",
|
||||
"3aa5817273c3b2f94f491840e0472f049d0f10009e23de63006166bca9b36ea3",
|
||||
"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747",
|
||||
"1096f6be0a4d7f0ecc2df4ed2c8683f143efc81eeba3ece6daadd2fca74c7ecc",
|
||||
"d76726da1b64e8679d8b6e66facf551ba96f2612de5a171fac818ee85ce3e5fe",
|
||||
"27487c9600b16b24a1bfb0519cfe4a5d1ad84959e3cce5d6d7a99d48660a1f78",
|
||||
"5d3ab876c206a37ad3b094e20bfc3941df3fa21a15ac8ea76d6918473789669a",
|
||||
"6b1b8dac34ffc61d464dfeef00e4a84a604e172ef6391fb629293d6f1666148c",
|
||||
"6fb266012c3008303e54ae55140b46957e9978098401dda34f4d921a275bf8bb",
|
||||
"53a91e3a64d1f658e983ac1e4f9e0c697f8f33e01d8debe439f4c1a92113f592",
|
||||
"5082984480f3b27891840a2037512739149678efc2ac981ca8cd016d02304efd",
|
||||
"7b849efa5604b58d50c419637b9873847dbf957081d526136c3a49b7357cd617",
|
||||
"f53b9d91a8cd177fb4a1cf081a1b6d58759a381ef120a7c5a18c0e70cae80983",
|
||||
"cfd7df62799a22e384a4ab5da8c4026c875b119d0f47c2716b20cdac9cc1f1a6",
|
||||
"d83b5ef189df7e884627294b752969547814c3cfe38995cf207c040e03bbe7a4",
|
||||
"96f652249b0946e1575d78a8bc7450123c8e64f1c56f6b2f93bc23fb249ed85a",
|
||||
"d60bdad03468f5f8c85b1b10db977e310a5aafab33750dfadb37488b02bfc8d7",
|
||||
"9839f160d893daae661c84168e07f46f0e1e9746feb8439a6d76738b4ad32eaa",
|
||||
"453a656903a031395d450f318211a6ec54cd79049a851f92cd6702c65ff5f5bd",
|
||||
"1634b87b5fcfd4a6c4ff2f2de17450ccce46f9abe0b02a71876c596ec165bfed",
|
||||
"24480686b56234a240fd9827209b584847f3d4f9657f0d9a97aec5320a264acb",
|
||||
"f4d1866e8599563c52ceeedf11c28b8567e465c6e9a91df92add535d57f02ab0",
|
||||
"805b34f708837dfb3e7f05815ac5760564628b58d5a0ce839ccbb6ef3620fac3",
|
||||
"659a74f6cfbc7c252c58d93452b9d9575e36c464aa6544c6375227c9166a6ed9",
|
||||
"75d737c3472471029c44876b330d2284288a42779b591a2ed4daa1c6c07efaf7",
|
||||
"dac1d8c5a9fe94f224e095b52577c33c2cc2b8f3a2d6ad9cbd46845af8c987f0",
|
||||
"be7358c4fe50148cccafc02ea205d80145e253889aa3958daafa8637047c840e",
|
||||
"30e8cbf1427c137fa60674a639431c19a9d6f4c07fd2959df83158e674fccbaa",
|
||||
"7f573f55d875ce8edc528edf822949fd2ab9f9c65d914a40225663b0a697be07",
|
||||
"781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5",
|
||||
"d82a91e1013170b10ca7fa0ec800fd0dc6e9335b70c303dadba603fc36802b5f",
|
||||
"a4237e420cdb0b3231d171fe879bcae37a2db7abf2f12a337b975337618c3ac2",
|
||||
"7ff4d89f90845ac4d7a50a259163798e0f446e61d4c943cc89637beff567ad02",
|
||||
"48dbb5e717a6221d64fd13ba12794bc28e5067ac1d7632ee9437d533772750df",
|
||||
"efba340bd479176486e5a2281c97ac4a90fdcf86ec9c13a78c3182ab877cd19b",
|
||||
"1021c8921548fa89abb4cc7e8668a3a8dcebae0a4c323ffeaf570438832d6993",
|
||||
"c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345",
|
||||
"4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8",
|
||||
"e568a76a4f8836be296d405eb41034260d55e2361e4b2ef88350a4003bbd5f9b",
|
||||
"ebdee92945ef05283be0ac3de25787c81a6a58a10f568f9c6b61d9dd513adbad",
|
||||
"6e8f3edfa28bfc8057d735794f76b697bcf18fb894a5a37a132693ebda31a464",
|
||||
"576d23dc3db2056d208849462fee358cf9f0f3310a2c63cb6c267a4b9f5848f9",
|
||||
"18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08",
|
||||
"a9046cc9175dc5a45fb93a2c890f9a8b18c707fa6d695771aab9300081d3e21a",
|
||||
"7a69e5f62fcc20e81beea7461a945e6531f8c7944200d0b3cb4cc63556c44106",
|
||||
"fd0266485777bd73e97c7c37f520c83c82e362fe4c25a6be93f3380083d4646b",
|
||||
"4b29db7a76f3b4fbc0a4fffc092e41c14f1a1a975a462d87e82827af03719cb2",
|
||||
"df1a6cb6c95a5bdd2a69e4fa921061da950fc0bb0b3529d04ca75e0c11f871df",
|
||||
"08bfc00b7f72e015f45c326f486bec16e4d5236b70e44543f1c5e86a8e21c76a",
|
||||
"1e908fbc1d131c17a87f32069f53f64f45c75f91a2f6d43f8aa6410974da5562",
|
||||
"b3a737d014a7e75f44b0f5afbd752f9bcc2abe54f60dbbebc3681b6e16611967",
|
||||
"d3052ca3e3d523b1ec80671eb1bba0517a2f522e195778dc83dd03a8d84a170e",
|
||||
"b98ded4ceaea20790dbcb3c31400692009d34c7f9927c286835a99b7481a5c22",
|
||||
"9e1e498420bc7c35f3e3a78d20045b4c8343986dae48739759bccb2a27e88c53",
|
||||
"141d2053cb29535ad45aa9e865cdec492524f0ec0066496b98b7099daab5d658",
|
||||
"8722c3843c85ddd6162a5cb506e1cb4d6ab0cafb966034f426e55a2ef89a345e",
|
||||
"52dfd21724329920c5c95f5361464e468584136d30030eb29247a7fe6c2c6e36",
|
||||
"d82a91e1013170b10ca7fa0ec800fd0dc6e9335b70c303dadba603fc36802b5f",
|
||||
"6bbb7d71eaa2544215a877e136cd7f490f4625eb56459a0da856cc8296d5df30",
|
||||
"1ebb28301aa1a48248d3723a0ea434bb7d4612ec920fa749583e3f41ce25849f",
|
||||
"00000001505e7e48927046e9bbaa728b1f3b511227e2200c578d6e6bb0c77eb9",
|
||||
"a01b5ba26421374250442e0d23f96e6a4bce429e0175cd0769ad8c585dd5a892",
|
||||
"26d6a946675e603f8de4bf6f9cef442037b70c7eee170ff06ed7673fc34c98f1",
|
||||
"1dd7992ea0ecbda7480ceed35748c3655691133c8c78af947fd24299db8f481f",
|
||||
"cdee943cbb19c51ab847a66d5d774373aa9f63d287246bb59b0827fa5e637400",
|
||||
"3d03c53608415b1d718c7786ee10bdb4e67bced32207e32880ee9e44301a19ec",
|
||||
"170dc4045d6c06275b40bd39f68ca31dbb962094e9763ee460f8341bd40bebca",
|
||||
"db1abbff170320730e5ace672ad7217161b8935afc1a896ae2fecf903c159932",
|
||||
"b0ac2c26eabdb0e0a9b0d10fd1458ca73c575b19d65e13f0e7484cbee84038b3",
|
||||
"c1e7fc21b4f9c199e6086e095639f0f16a4e4884544547ce8a653ed7b5b6c4a7",
|
||||
"813c2662366a12f6337b951c048552fd3c4894e403cab701634dcd803786dc09",
|
||||
"fd0bcf8cd1aee83fe75e6c0fdfc543845e5bc3f50d26d2d5e5c6d3fa521f98c0",
|
||||
"45fd1955f590da87c1fd2edb99d17accf235ec3ebf0afe1d3306ade42693c6e9",
|
||||
"27938497226683c701e2843c6db580e2f0e25f5a198f4c3397e3a0a27764215d",
|
||||
"2321edfd415f9558605b4d7a7083c52624e8922ae86bb2ae359fbf829724111a",
|
||||
"0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd",
|
||||
"1f8e182bf72d61cb78dcd6b96dd3be8b874b8881da6630757b6508970f67230c",
|
||||
"c6603b0f1ccfec625d9c08b753e4f774eaf7d1cf2769223125b5fd4da728019e",
|
||||
"296842eaaed9be5ae0668da09fe48aac0521c4af859ad547d93145e5ac34c17e",
|
||||
"88f8707a45e825a13ed383332abe6e2f104ab44d877918be22550083a2b59e60",
|
||||
"27a20b41a66b35d442302a50ca1baad72c2ed844c8d1224c9f6d50a12752084e",
|
||||
"280e847ef0c82a2a7c4e877c91cd7567474c1431b815d27bbc1017e147d9d77c",
|
||||
"ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d",
|
||||
"bb0174ae21a6cac1a0a9c8b4ac6ebfda56ce51605c315b1824970bc275f7239a",
|
||||
"edb470271297ac5a61f277f3cd14de54c67eb5ccd20ef0d9df29be18685bb004",
|
||||
"9609b093450dd7e0afb389619acdaf2e6a0d6817c93552f3911e05b50ae73e3d",
|
||||
"3e33fd7124f174fc535151937f8718634dd9d856143d4cefb5a10ddaf2f615c0",
|
||||
"463555bb4b0f80fd1376fae628fabfaf7e5e31cd2741d80aa4d225c926bc287e",
|
||||
"916cb5ff07d3b51cef7f6b6b7f5479b1001b401c0e82558ee1a22504c7d507c9",
|
||||
"cc5f259f036683798e4a52071dbb97238702ffb6f0c85af6d273c8ddbe5c0afb",
|
||||
"2ad91f1dca2dcd5fc89e7208d1e5059f0bac0870d63fc3bac21c7a9388fa18fd",
|
||||
"8bf629b3d519a0f8a8390137a445c0eb2f5f2b4a8ed71151de898051e8006f13",
|
||||
"94215f42a96335c87fcb9e881a0bbb62b9a795519e109cf5f9d2ef617681f622",
|
||||
"bcbf9644d3f475d00eb9c6e467385ce16d4546c1a24222ccfa542bf776eaba95",
|
||||
"ba5115c37b0f911e530ed6c487ccbd9b737da33fd4b88a9f590860378c06af62",
|
||||
"609f186ca023d658c0fe019570472f59565c8be1dc163b1541fac9d90aa4e8af",
|
||||
"4e5622b575cdbb4d5ded093e48c68cd3f724fad547142f0c1b0aec2f2b2a0b2e",
|
||||
"4df7b43b3a4db4b99e3dbad6bd0f84226726efd63ae7e027f91acbd91b4dba48",
|
||||
"bcbf9644d3f475d00eb9c6e467385ce16d4546c1a24222ccfa542bf776eaba95",
|
||||
"3a06add309fd8419ea4d4e475e9c0dff5909c635d9769bf0728232f3a0683a84",
|
||||
"d2384c4ac623eb9f15ae4cb32ee7a9b03e0202802d52f2395f2ee8f6433151aa",
|
||||
"1d4cc828b657da8c7a101e8657365459b9dc74139bed5d35bd8295b00be2a1ae",
|
||||
"76fcec0e0638351f1d0e0dc4ebaf6dd3d67404126d664547674070f3175273d9",
|
||||
"6707c39e6c53ef945c5df29af78667dc941ed80094994bd264fd806a6e0a3230",
|
||||
"80482e60178c2ce996da6d67577f56a2b2c47ccb1c84c81f2b7960637cb71b78",
|
||||
"147784df719c09fad62bff0493a60b4f5dbbe8579e73f00d207350e8ffdfd65f",
|
||||
"afc0295d2c6e0a1820c214c07312070a4070d52083163d7fe410fa02bf85d9d2",
|
||||
"6a5e3cc17279cbdf051c06d96e3f843cdb296f351d8ca35a6a190c0ab90dbf9a",
|
||||
"3b7fc823611f1aeaea63ee3bf69b25b8aa16ec6e81d1afc39026808fe194354f",
|
||||
"d96fe9c5478d1bb36e9ec40cc678b0bf7ff67e017922a151f925640a8884f291",
|
||||
"1d80e5588de010d137a67c42b03717595f5f510e73e42cfc48f31bae91844d59",
|
||||
"06dde95f0268ce40128bf73ca6e85567b8567688ea52f24dcd5734e77c50f2d9",
|
||||
"f683e87035f7ad4f44e0b98cfbd9537e16455a92cd38cefc4cb31db7557f5ef2",
|
||||
"036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58",
|
||||
"7cc328a08ddb2afdf9f9be77beff4c83489ff979721827d628a542f32a247c0e",
|
||||
"f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba",
|
||||
"fb61b93d864e4f0eba766bb8556f2dc0262e8e985012e29ba28508dd52067d98",
|
||||
"0e52122d1eb95cdd8ba5f65815f7d1c9125a8c14d82989eae52ab369eea6c7e4",
|
||||
"04dcaf2552801937d1c20b69adf89646f21b53c17906271d22c7be9bcadb96c0",
|
||||
"0ee827a36e8bb0cfc483cf1872781182c4a16c58acba3ae2d7b155e0370e93b8",
|
||||
"adc14fa3ad590856dd8b80815d367f7c1e6735ad00fd98a86d002fbe9fb535e1",
|
||||
"2bc9be7569515701581d5d765422a17ee3500d8e1f4e7aa53f6be86ae6ba274d",
|
||||
"9a21569255d0a3a9e75f1de2e4c883c9be2e5615887f22b2ecf6b1813bcd587d",
|
||||
"3f3ff7adb39159c42c0aa16d53c0483bfbfad57df22a9e9e9364a306741eb2cf",
|
||||
"e9aa50decff01f2cec1ec2b2e0b34332cf9c92cafdac5a7cc0881a6d26b59854",
|
||||
"c7eda660a6bc8270530e82b4a7712acdea2e31dc0a56f8dc955ac009efd97c86",
|
||||
"787338757fc25d65cd929394d5e7713cf43638e8d259e8dcf5c73b834eb851f2",
|
||||
"7e8ffe414a53ce18a85536eda74d3cbda0da6a98d4fe6c514948002472178c95",
|
||||
"6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3",
|
||||
"0c371f5ed95076613443e8331c4b60828ed67bcdefaa1698fb5ce9d7b3285ffb",
|
||||
"daa41bedb68591363bf4407f687cb9789cc543ed024bb77c22d2c84d88f54153",
|
||||
"12ee03d11684a125dd87be879c28190415be3f3b1eca6b4ed743bd74ffd880e6",
|
||||
"00dfb20815a71e24572394efdfbf772e56a507921b8287201ab8937496ee8e6d",
|
||||
"94a6a78a5aebbba40bd1aaa2234810132c2d8004bb9177616c413d3c0ddf320e"
|
||||
];
|
||||
const PUBKEY_ARRAYS = generateTestPubkeyArrays();
|
||||
|
||||
// Main testing function
|
||||
async function testRelayPubkeyLimits() {
|
||||
console.log('Starting Nostr Relay Pubkey Filter Test');
|
||||
console.log(`Testing ${RELAYS.length} relays with up to ${MAX_PUBKEYS} pubkeys`);
|
||||
console.log(`Incrementing by ${STEP_SIZE} pubkeys per test, requesting ${EVENT_LIMIT} events per test\n`);
|
||||
|
||||
const results = {};
|
||||
|
||||
// Initialize results for each relay
|
||||
for (const relayUrl of RELAYS) {
|
||||
results[relayUrl] = {
|
||||
maxPubkeys: 0,
|
||||
failures: 0,
|
||||
lastSuccess: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Test each pubkey array size
|
||||
for (const pubkeyArray of PUBKEY_ARRAYS) {
|
||||
const pubkeyCount = pubkeyArray.length;
|
||||
console.log(`\nTesting with ${pubkeyCount} pubkeys...`);
|
||||
|
||||
// Test each relay with this pubkey count
|
||||
for (const relayUrl of RELAYS) {
|
||||
if (results[relayUrl].failures >= 2) {
|
||||
console.log(` ${relayUrl}: Skipping (already failed 2 times)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await testRelayWithPubkeys(relayUrl, pubkeyArray);
|
||||
if (success) {
|
||||
results[relayUrl].maxPubkeys = pubkeyCount;
|
||||
results[relayUrl].lastSuccess = pubkeyCount;
|
||||
results[relayUrl].failures = 0; // Reset failures on success
|
||||
console.log(` ${relayUrl}: ✓ Success`);
|
||||
} else {
|
||||
results[relayUrl].failures++;
|
||||
console.log(` ${relayUrl}: ✗ Failed (${results[relayUrl].failures}/2)`);
|
||||
}
|
||||
} catch (error) {
|
||||
results[relayUrl].failures++;
|
||||
console.log(` ${relayUrl}: ✗ Error (${results[relayUrl].failures}/2): ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print final results
|
||||
console.log('\n=== FINAL RESULTS ===');
|
||||
for (const relayUrl of RELAYS) {
|
||||
const result = results[relayUrl];
|
||||
console.log(`${relayUrl}: ${result.maxPubkeys} pubkeys (last success: ${result.lastSuccess})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test a single relay with a specific pubkey array
|
||||
async function testRelayWithPubkeys(relayUrl, pubkeys) {
|
||||
return new Promise((resolve) => {
|
||||
const ws = new WebSocket(relayUrl);
|
||||
let receivedEOSE = false;
|
||||
let subscriptionId = 'test_' + Math.random().toString(36).substr(2, 9);
|
||||
let timeoutId;
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeoutId);
|
||||
ws.close();
|
||||
};
|
||||
|
||||
ws.on('open', () => {
|
||||
console.log(`Connected to ${relayUrl}`);
|
||||
// Send subscription request
|
||||
const filter = {
|
||||
kinds: [EVENT_KIND],
|
||||
authors: pubkeys,
|
||||
limit: EVENT_LIMIT
|
||||
};
|
||||
const subscriptionMessage = ['REQ', subscriptionId, filter];
|
||||
const messageString = JSON.stringify(subscriptionMessage);
|
||||
const messageSize = Buffer.byteLength(messageString, 'utf8');
|
||||
console.log(`Sending request with ${pubkeys.length} pubkeys (${messageSize} bytes)`);
|
||||
ws.send(messageString);
|
||||
|
||||
// Set timeout for EOSE response
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!receivedEOSE) {
|
||||
console.log('Timeout waiting for EOSE');
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
}, 10000); // 10 second timeout
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
if (message[0] === 'EVENT' && message[1] === subscriptionId) {
|
||||
// Skip event content, just log that we received an event
|
||||
console.log(`Received EVENT for subscription ${subscriptionId}`);
|
||||
} else if (message[0] === 'EOSE' && message[1] === subscriptionId) {
|
||||
console.log(`Received EOSE: ${JSON.stringify(message)}`);
|
||||
receivedEOSE = true;
|
||||
cleanup();
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log(`Received other message: ${JSON.stringify(message)}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Received non-JSON: ${data.toString()}`);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.log(`WebSocket error: ${error.message}`);
|
||||
cleanup();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
// Overall connection timeout
|
||||
setTimeout(() => {
|
||||
if (!receivedEOSE) {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
}, 15000); // 15 second total timeout
|
||||
});
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testRelayPubkeyLimits().catch(console.error);
|
||||
Reference in New Issue
Block a user