Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f49aae8ab0 | ||
|
|
f6debcf799 | ||
|
|
edbc4f1359 | ||
|
|
5242f066e7 | ||
|
|
af186800fa | ||
|
|
2bff4a5f44 | ||
|
|
edb73d50cf | ||
|
|
3dc09d55fd | ||
|
|
079fb1b0f5 | ||
|
|
17b2aa8111 | ||
|
|
78d484cfe0 | ||
|
|
182e12817d | ||
|
|
9179d57cc9 | ||
|
|
9cb9b746d8 | ||
|
|
57a0089664 | ||
|
|
53f7608872 | ||
|
|
838ce5b45a | ||
|
|
e878b9557e | ||
|
|
6638d37d6f | ||
|
|
4c29e15329 | ||
|
|
48890a2121 | ||
|
|
e312d7e18c | ||
|
|
6c38aaebf3 | ||
|
|
18b0ac44bf | ||
|
|
b6749eff2f | ||
|
|
c73a103280 | ||
|
|
a5d194f730 | ||
|
|
6320436b88 | ||
|
|
87325927ed | ||
|
|
4435cdf5b6 | ||
|
|
b041654611 | ||
|
|
e833dcefd4 | ||
|
|
29680f0ee8 | ||
|
|
670329700c | ||
|
|
62e17af311 | ||
|
|
e3938a2c85 | ||
|
|
49ffc3d99e | ||
|
|
34bb1c34a2 |
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,3 +1,9 @@
|
||||
[submodule "nostr_core_lib"]
|
||||
path = nostr_core_lib
|
||||
url = https://git.laantungir.net/laantungir/nostr_core_lib.git
|
||||
[submodule "c_utils_lib"]
|
||||
path = c_utils_lib
|
||||
url = ssh://git@git.laantungir.net:2222/laantungir/c_utils_lib.git
|
||||
[submodule "text_graph"]
|
||||
path = text_graph
|
||||
url = ssh://git@git.laantungir.net:2222/laantungir/text_graph.git
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
description: "Brief description of what this command does"
|
||||
---
|
||||
|
||||
Run build_and_push.sh, and supply a good git commit message. For example:
|
||||
Run increment_and_push.sh, and supply a good git commit message. For example:
|
||||
|
||||
./build_and_push.sh "Fixed the bug with nip05 implementation"
|
||||
./increment_and_push.sh "Fixed the bug with nip05 implementation"
|
||||
@@ -1 +1 @@
|
||||
src/embedded_web_content.c
|
||||
src/embedded_web_content.c
|
||||
|
||||
@@ -121,8 +121,8 @@ fuser -k 8888/tcp
|
||||
- Event filtering done at C level, not SQL level for NIP-40 expiration
|
||||
|
||||
### Configuration Override Behavior
|
||||
- CLI port override only affects first-time startup
|
||||
- After database creation, all config comes from events
|
||||
- CLI port override applies during first-time startup and existing relay restarts
|
||||
- After database creation, all config comes from events (but CLI overrides can still be applied)
|
||||
- Database path cannot be changed after initialization
|
||||
|
||||
## Non-Obvious Pitfalls
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
# Alpine-based MUSL static binary builder for C-Relay
|
||||
# Produces truly portable binaries with zero runtime dependencies
|
||||
|
||||
ARG DEBUG_BUILD=false
|
||||
|
||||
FROM alpine:3.19 AS builder
|
||||
|
||||
# Re-declare build argument in this stage
|
||||
ARG DEBUG_BUILD=false
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
@@ -76,6 +81,15 @@ RUN git submodule update --init --recursive
|
||||
# Copy nostr_core_lib source files (cached unless nostr_core_lib changes)
|
||||
COPY nostr_core_lib /build/nostr_core_lib/
|
||||
|
||||
# Copy c_utils_lib source files (cached unless c_utils_lib changes)
|
||||
COPY c_utils_lib /build/c_utils_lib/
|
||||
|
||||
# Build c_utils_lib with MUSL-compatible flags (cached unless c_utils_lib changes)
|
||||
RUN cd c_utils_lib && \
|
||||
sed -i 's/CFLAGS = -Wall -Wextra -std=c99 -O2 -g/CFLAGS = -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -O2 -g/' Makefile && \
|
||||
make clean && \
|
||||
make
|
||||
|
||||
# Build nostr_core_lib with required NIPs (cached unless nostr_core_lib changes)
|
||||
# Disable fortification in build.sh to prevent __*_chk symbol issues
|
||||
# NIPs: 001(Basic), 006(Keys), 013(PoW), 017(DMs), 019(Bech32), 044(Encryption), 059(Gift Wrap - required by NIP-17)
|
||||
@@ -91,20 +105,29 @@ COPY Makefile /build/Makefile
|
||||
|
||||
# Build c-relay with full static linking (only rebuilds when src/ changes)
|
||||
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
|
||||
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
|
||||
# Use conditional compilation flags based on DEBUG_BUILD argument
|
||||
RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
CFLAGS="-g -O0 -DDEBUG"; \
|
||||
STRIP_CMD=""; \
|
||||
echo "Building with DEBUG symbols enabled"; \
|
||||
else \
|
||||
CFLAGS="-O2"; \
|
||||
STRIP_CMD="strip /build/c_relay_static"; \
|
||||
echo "Building optimized production binary"; \
|
||||
fi && \
|
||||
gcc -static $CFLAGS -Wall -Wextra -std=c99 \
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||
-I. -Inostr_core_lib -Inostr_core_lib/nostr_core \
|
||||
-I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core \
|
||||
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
|
||||
src/main.c src/config.c src/dm_admin.c src/request_validator.c \
|
||||
src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c \
|
||||
src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c \
|
||||
-o /build/c_relay_static \
|
||||
c_utils_lib/libc_utils.a \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
|
||||
-lcurl -lz -lpthread -lm -ldl
|
||||
|
||||
# Strip binary to reduce size
|
||||
RUN strip /build/c_relay_static
|
||||
-lcurl -lz -lpthread -lm -ldl && \
|
||||
eval "$STRIP_CMD"
|
||||
|
||||
# Verify it's truly static
|
||||
RUN echo "=== Binary Information ===" && \
|
||||
|
||||
31
Makefile
31
Makefile
@@ -2,8 +2,8 @@
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -std=c99 -g -O2
|
||||
INCLUDES = -I. -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket
|
||||
LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl
|
||||
INCLUDES = -I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket
|
||||
LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl -Lc_utils_lib -lc_utils
|
||||
|
||||
# Build directory
|
||||
BUILD_DIR = build
|
||||
@@ -11,6 +11,7 @@ BUILD_DIR = build
|
||||
# Source files
|
||||
MAIN_SRC = src/main.c src/config.c src/dm_admin.c src/request_validator.c src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c
|
||||
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
||||
C_UTILS_LIB = c_utils_lib/libc_utils.a
|
||||
|
||||
# Architecture detection
|
||||
ARCH = $(shell uname -m)
|
||||
@@ -32,9 +33,16 @@ $(BUILD_DIR):
|
||||
mkdir -p $(BUILD_DIR)
|
||||
|
||||
# Check if nostr_core_lib is built
|
||||
# Explicitly specify NIPs to ensure NIP-44 (encryption) is included
|
||||
# NIPs: 1 (basic), 6 (keys), 13 (PoW), 17 (DMs), 19 (bech32), 44 (encryption), 59 (gift wrap)
|
||||
$(NOSTR_CORE_LIB):
|
||||
@echo "Building nostr_core_lib..."
|
||||
cd nostr_core_lib && ./build.sh
|
||||
@echo "Building nostr_core_lib with required NIPs (including NIP-44 for encryption)..."
|
||||
cd nostr_core_lib && ./build.sh --nips=1,6,13,17,19,44,59
|
||||
|
||||
# Check if c_utils_lib is built
|
||||
$(C_UTILS_LIB):
|
||||
@echo "Building c_utils_lib..."
|
||||
cd c_utils_lib && ./build.sh lib
|
||||
|
||||
# Update main.h version information (requires main.h to exist)
|
||||
src/main.h:
|
||||
@@ -73,18 +81,18 @@ force-version:
|
||||
@$(MAKE) src/main.h
|
||||
|
||||
# Build the relay
|
||||
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
||||
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
|
||||
@echo "Compiling C-Relay for architecture: $(ARCH)"
|
||||
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS)
|
||||
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
|
||||
@echo "Build complete: $(TARGET)"
|
||||
|
||||
# Build for specific architectures
|
||||
x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
||||
x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
|
||||
@echo "Building C-Relay for x86_64..."
|
||||
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(LIBS)
|
||||
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
|
||||
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
|
||||
|
||||
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
||||
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
|
||||
@echo "Cross-compiling C-Relay for ARM64..."
|
||||
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
|
||||
echo "ERROR: ARM64 cross-compiler not found."; \
|
||||
@@ -108,7 +116,7 @@ arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
||||
fi
|
||||
@echo "Using aarch64-linux-gnu-gcc with ARM64 libraries..."
|
||||
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig \
|
||||
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_arm64 $(NOSTR_CORE_LIB) \
|
||||
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_arm64 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) \
|
||||
-L/usr/lib/aarch64-linux-gnu $(LIBS)
|
||||
@echo "Build complete: $(BUILD_DIR)/c_relay_arm64"
|
||||
|
||||
@@ -159,9 +167,10 @@ clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
@echo "Clean complete"
|
||||
|
||||
# Clean everything including nostr_core_lib
|
||||
# Clean everything including nostr_core_lib and c_utils_lib
|
||||
clean-all: clean
|
||||
cd nostr_core_lib && make clean 2>/dev/null || true
|
||||
cd c_utils_lib && make clean 2>/dev/null || true
|
||||
|
||||
# Install dependencies (Ubuntu/Debian)
|
||||
install-deps:
|
||||
|
||||
511
NOSTR_RELEASE.md
Normal file
511
NOSTR_RELEASE.md
Normal file
@@ -0,0 +1,511 @@
|
||||
# Relay
|
||||
|
||||
I am releasing the code for the nostr relay that I wrote use myself. The code is free for anyone to use in any way that they wish.
|
||||
|
||||
Some of the features of this relay are conventional, and some are unconventional.
|
||||
|
||||
## The conventional
|
||||
|
||||
This relay is written in C99 with a sqlite database.
|
||||
|
||||
It implements the following NIPs.
|
||||
|
||||
- [X] NIP-01: Basic protocol flow implementation
|
||||
- [X] NIP-09: Event deletion
|
||||
- [X] NIP-11: Relay information document
|
||||
- [X] NIP-13: Proof of Work
|
||||
- [X] NIP-15: End of Stored Events Notice
|
||||
- [X] NIP-20: Command Results
|
||||
- [X] NIP-33: Parameterized Replaceable Events
|
||||
- [X] NIP-40: Expiration Timestamp
|
||||
- [X] NIP-42: Authentication of clients to relays
|
||||
- [X] NIP-45: Counting results
|
||||
- [X] NIP-50: Keywords filter
|
||||
- [X] NIP-70: Protected Events
|
||||
|
||||
## The unconventional
|
||||
|
||||
### The binaries are fully self contained.
|
||||
|
||||
It should just run in linux without having to worry about what you have on your system. I want to download and run. No docker. No dependency hell.
|
||||
|
||||
### The relay is a full nostr citizen with it's own public and private keys.
|
||||
|
||||
For example, you can see my implementation running here:
|
||||
|
||||
[https://primal.net/p/nprofile1qqswn2jsmm8lq8evas0v9vhqkdpn9nuujt90mtz60nqgsxndy66es4qjjnhr7](https://)
|
||||
|
||||
What this means in practice is that when you start the program, it generates keys for itself, and for it's administrator (You can specify these if you wish)
|
||||
|
||||
Now the program and the administrator can have verifed communication between the two. For example, the administrator can send DMs to the relay, asking it's status, and changing it's configuration through any client that can handle nip17 DMs. The relay can also send notifications to the administrator about it's current status, or it can publish it's status directly to NOSTR as kind-1 notes.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
Get your C-Relay up and running in minutes with a static binary (no dependencies required):
|
||||
|
||||
### 1. Download Static Binary
|
||||
|
||||
Download the latest static release from the [releases page](https://git.laantungir.net/laantungir/c-relay/releases):
|
||||
|
||||
```bash
|
||||
# Static binary - works on all Linux distributions (no dependencies)
|
||||
wget https://git.laantungir.net/laantungir/c-relay/releases/download/v0.6.0/c-relay-v0.6.0-linux-x86_64-static
|
||||
chmod +x c-relay-v0.6.0-linux-x86_64-static
|
||||
mv c-relay-v0.6.0-linux-x86_64-static c-relay
|
||||
```
|
||||
|
||||
### 2. Start the Relay
|
||||
|
||||
Simply run the binary - no configuration files needed:
|
||||
|
||||
```bash
|
||||
./c-relay
|
||||
```
|
||||
|
||||
On first startup, you'll see:
|
||||
|
||||
- **Admin Private Key**: Save this securely! You'll need it for administration
|
||||
- **Relay Public Key**: Your relay's identity on the Nostr network
|
||||
- **Port Information**: Default is 8888, or the next available port
|
||||
|
||||
### 3. Access the Web Interface
|
||||
|
||||
Open your browser and navigate to:
|
||||
|
||||
```
|
||||
http://localhost:8888/api/
|
||||
```
|
||||
|
||||
The web interface provides:
|
||||
|
||||
- Real-time configuration management
|
||||
- Database statistics dashboard
|
||||
- Auth rules management
|
||||
- Secure admin authentication with your Nostr identity
|
||||
|
||||
### 4. Test Your Relay
|
||||
|
||||
Test basic connectivity:
|
||||
|
||||
```bash
|
||||
# Test WebSocket connection
|
||||
curl -H "Accept: application/nostr+json" http://localhost:8888
|
||||
|
||||
# Test with a Nostr client
|
||||
# Add ws://localhost:8888 to your client's relay list
|
||||
```
|
||||
|
||||
### 5. Configure Your Relay (Optional)
|
||||
|
||||
Use the web interface or send admin commands to customize:
|
||||
|
||||
- Relay name and description
|
||||
- Authentication rules (whitelist/blacklist)
|
||||
- Connection limits
|
||||
- Proof-of-work requirements
|
||||
|
||||
**That's it!** Your relay is now running with zero configuration required. The event-based configuration system means you can adjust all settings through the web interface or admin API without editing config files.
|
||||
|
||||
## Web Admin Interface
|
||||
|
||||
C-Relay includes a **built-in web-based administration interface** accessible at `http://localhost:8888/api/`. The interface provides:
|
||||
|
||||
- **Real-time Configuration Management**: View and edit all relay settings through a web UI
|
||||
- **Database Statistics Dashboard**: Monitor event counts, storage usage, and performance metrics
|
||||
- **Auth Rules Management**: Configure whitelist/blacklist rules for pubkeys
|
||||
- **NIP-42 Authentication**: Secure access using your Nostr identity
|
||||
- **Event-Based Updates**: All changes are applied as cryptographically signed Nostr events
|
||||
|
||||
The web interface serves embedded static files with no external dependencies and includes proper CORS headers for browser compatibility.
|
||||
|
||||
## Administrator API
|
||||
|
||||
C-Relay uses an innovative **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key generated during first startup. All admin commands use **NIP-44 encrypted command arrays** for security and compatibility.
|
||||
|
||||
### Authentication
|
||||
|
||||
All admin commands require signing with the admin private key displayed during first-time startup. **Save this key securely** - it cannot be recovered and is needed for all administrative operations.
|
||||
|
||||
### Event Structure
|
||||
|
||||
All admin commands use the same unified event structure with NIP-44 encrypted content:
|
||||
|
||||
**Admin Command Event:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "event_id",
|
||||
"pubkey": "admin_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23456,
|
||||
"content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
|
||||
"tags": [
|
||||
["p", "relay_public_key"]
|
||||
],
|
||||
"sig": "event_signature"
|
||||
}
|
||||
```
|
||||
|
||||
The `content` field contains a NIP-44 encrypted JSON array representing the command.
|
||||
|
||||
**Admin Response Event:**
|
||||
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "BpKCVhfN8eYtRmPqSvWxZnMkL2gHjUiOp3rTyEwQaS5dFg...",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
The `content` field contains a NIP-44 encrypted JSON response object.
|
||||
|
||||
### Admin Commands
|
||||
|
||||
All commands are sent as NIP-44 encrypted JSON arrays in the event content. The following table lists all available commands:
|
||||
|
||||
|
||||
| Command Type | Command Format | Description |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| **Configuration Management** | | |
|
||||
| `config_update` | `["config_update", [{"key": "auth_enabled", "value": "true", "data_type": "boolean", "category": "auth"}, {"key": "relay_description", "value": "My Relay", "data_type": "string", "category": "relay"}, ...]]` | Update relay configuration parameters (supports multiple updates) |
|
||||
| `config_query` | `["config_query", "all"]` | Query all configuration parameters |
|
||||
| **Auth Rules Management** | | |
|
||||
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
|
||||
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
|
||||
| `auth_delete_rule` | `["delete_auth_rule", "blacklist", "pubkey", "abc123..."]` | Delete specific auth rule |
|
||||
| `auth_query_all` | `["auth_query", "all"]` | Query all auth rules |
|
||||
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
|
||||
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |
|
||||
| **System Commands** | | |
|
||||
| `system_clear_auth` | `["system_command", "clear_all_auth_rules"]` | Clear all auth rules |
|
||||
| `system_status` | `["system_command", "system_status"]` | Get system status |
|
||||
| `stats_query` | `["stats_query"]` | Get comprehensive database statistics |
|
||||
| **Database Queries** | | |
|
||||
| `sql_query` | `["sql_query", "SELECT * FROM events LIMIT 10"]` | Execute read-only SQL query against relay database |
|
||||
|
||||
### Available Configuration Keys
|
||||
|
||||
**Basic Relay Settings:**
|
||||
|
||||
- `relay_name`: Relay name (displayed in NIP-11)
|
||||
- `relay_description`: Relay description text
|
||||
- `relay_contact`: Contact information
|
||||
- `relay_software`: Software URL
|
||||
- `relay_version`: Software version
|
||||
- `supported_nips`: Comma-separated list of supported NIP numbers (e.g., "1,2,4,9,11,12,13,15,16,20,22,33,40,42")
|
||||
- `language_tags`: Comma-separated list of supported language tags (e.g., "en,es,fr" or "*" for all)
|
||||
- `relay_countries`: Comma-separated list of supported country codes (e.g., "US,CA,MX" or "*" for all)
|
||||
- `posting_policy`: Posting policy URL or text
|
||||
- `payments_url`: Payment URL for premium features
|
||||
- `max_connections`: Maximum concurrent connections
|
||||
- `max_subscriptions_per_client`: Max subscriptions per client
|
||||
- `max_event_tags`: Maximum tags per event
|
||||
- `max_content_length`: Maximum event content length
|
||||
|
||||
**Authentication & Access Control:**
|
||||
|
||||
- `auth_enabled`: Enable whitelist/blacklist auth rules (`true`/`false`)
|
||||
- `nip42_auth_required`: Enable NIP-42 cryptographic authentication (`true`/`false`)
|
||||
- `nip42_auth_required_kinds`: Event kinds requiring NIP-42 auth (comma-separated)
|
||||
- `nip42_challenge_timeout`: NIP-42 challenge expiration seconds
|
||||
|
||||
**Proof of Work & Validation:**
|
||||
|
||||
- `pow_min_difficulty`: Minimum proof-of-work difficulty
|
||||
- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`)
|
||||
|
||||
**Monitoring Settings:**
|
||||
|
||||
- `kind_24567_reporting_throttle_sec`: Minimum seconds between monitoring events (default: 5)
|
||||
|
||||
### Dynamic Configuration Updates
|
||||
|
||||
C-Relay supports **dynamic configuration updates** without requiring a restart for most settings. Configuration parameters are categorized as either **dynamic** (can be updated immediately) or **restart-required** (require relay restart to take effect).
|
||||
|
||||
**Dynamic Configuration Parameters (No Restart Required):**
|
||||
|
||||
- All relay information (NIP-11) settings: `relay_name`, `relay_description`, `relay_contact`, `relay_software`, `relay_version`, `supported_nips`, `language_tags`, `relay_countries`, `posting_policy`, `payments_url`
|
||||
- Authentication settings: `auth_enabled`, `nip42_auth_required`, `nip42_auth_required_kinds`, `nip42_challenge_timeout`
|
||||
- Subscription limits: `max_subscriptions_per_client`, `max_total_subscriptions`
|
||||
- Event validation limits: `max_event_tags`, `max_content_length`, `max_message_length`
|
||||
- Proof of Work settings: `pow_min_difficulty`, `pow_mode`
|
||||
- Event expiration settings: `nip40_expiration_enabled`, `nip40_expiration_strict`, `nip40_expiration_filter`, `nip40_expiration_grace_period`
|
||||
|
||||
**Restart-Required Configuration Parameters:**
|
||||
|
||||
- Connection settings: `max_connections`, `relay_port`
|
||||
- Database and core system settings
|
||||
|
||||
When updating configuration, the admin API response will indicate whether a restart is required for each parameter. Dynamic updates take effect immediately and are reflected in NIP-11 relay information documents without restart.
|
||||
|
||||
### Response Format
|
||||
|
||||
All admin commands return **signed EVENT responses** via WebSocket following standard Nostr protocol. Responses use JSON content with structured data.
|
||||
|
||||
#### Response Examples
|
||||
|
||||
**Success Response:**
|
||||
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"success\", \"message\": \"Operation completed successfully\", \"timestamp\": 1234567890}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Error Response:**
|
||||
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"invalid configuration value\", \"timestamp\": 1234567890}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Auth Rules Query Response:**
|
||||
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"auth_rules_all\", \"total_results\": 2, \"timestamp\": 1234567890, \"data\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"abc123...\", \"action\": \"allow\"}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration Query Response:**
|
||||
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_all\", \"total_results\": 27, \"timestamp\": 1234567890, \"data\": [{\"key\": \"auth_enabled\", \"value\": \"false\", \"data_type\": \"boolean\", \"category\": \"auth\", \"description\": \"Enable NIP-42 authentication\"}, {\"key\": \"relay_description\", \"value\": \"My Relay\", \"data_type\": \"string\", \"category\": \"relay\", \"description\": \"Relay description text\"}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration Update Success Response:**
|
||||
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"total_results\": 2, \"timestamp\": 1234567890, \"status\": \"success\", \"data\": [{\"key\": \"auth_enabled\", \"value\": \"true\", \"status\": \"updated\"}, {\"key\": \"relay_description\", \"value\": \"My Updated Relay\", \"status\": \"updated\"}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration Update Error Response:**
|
||||
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"field validation failed: invalid port number '99999' (must be 1-65535)\", \"timestamp\": 1234567890}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Database Statistics Query Response:**
|
||||
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"stats_query\", \"timestamp\": 1234567890, \"database_size_bytes\": 1048576, \"total_events\": 15432, \"database_created_at\": 1234567800, \"latest_event_at\": 1234567890, \"event_kinds\": [{\"kind\": 1, \"count\": 12000, \"percentage\": 77.8}, {\"kind\": 0, \"count\": 2500, \"percentage\": 16.2}], \"time_stats\": {\"total\": 15432, \"last_24h\": 234, \"last_7d\": 1456, \"last_30d\": 5432}, \"top_pubkeys\": [{\"pubkey\": \"abc123...\", \"event_count\": 1234, \"percentage\": 8.0}, {\"pubkey\": \"def456...\", \"event_count\": 987, \"percentage\": 6.4}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**SQL Query Response:**
|
||||
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"sql_query\", \"request_id\": \"request_event_id\", \"timestamp\": 1234567890, \"query\": \"SELECT * FROM events LIMIT 10\", \"execution_time_ms\": 45, \"row_count\": 10, \"columns\": [\"id\", \"pubkey\", \"created_at\", \"kind\", \"content\"], \"rows\": [[\"abc123...\", \"def456...\", 1234567890, 1, \"Hello world\"], ...]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"],
|
||||
["e", "request_event_id"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
### SQL Query Command
|
||||
|
||||
The `sql_query` command allows administrators to execute read-only SQL queries against the relay database. This provides powerful analytics and debugging capabilities through the admin API.
|
||||
|
||||
**Request/Response Correlation:**
|
||||
|
||||
- Each response includes the request event ID in both the `tags` array (`["e", "request_event_id"]`) and the decrypted content (`"request_id": "request_event_id"`)
|
||||
- This allows proper correlation when multiple queries are submitted concurrently
|
||||
- Frontend can track pending queries and match responses to requests
|
||||
|
||||
**Security Features:**
|
||||
|
||||
- Only SELECT statements allowed (INSERT, UPDATE, DELETE, DROP, etc. are blocked)
|
||||
- Query timeout: 5 seconds (configurable)
|
||||
- Result row limit: 1000 rows (configurable)
|
||||
- All queries logged with execution time
|
||||
|
||||
**Available Tables and Views:**
|
||||
|
||||
- `events` - All Nostr events
|
||||
- `config` - Configuration parameters
|
||||
- `auth_rules` - Authentication rules
|
||||
- `subscription_events` - Subscription lifecycle log
|
||||
- `event_broadcasts` - Event broadcast log
|
||||
- `recent_events` - Last 1000 events (view)
|
||||
- `event_stats` - Event statistics by type (view)
|
||||
- `subscription_analytics` - Subscription metrics (view)
|
||||
- `active_subscriptions_log` - Currently active subscriptions (view)
|
||||
- `event_kinds_view` - Event distribution by kind (view)
|
||||
- `top_pubkeys_view` - Top 10 pubkeys by event count (view)
|
||||
- `time_stats_view` - Time-based statistics (view)
|
||||
|
||||
**Example Queries:**
|
||||
|
||||
```sql
|
||||
-- Recent events
|
||||
SELECT id, pubkey, created_at, kind FROM events ORDER BY created_at DESC LIMIT 20
|
||||
|
||||
-- Event distribution by kind
|
||||
SELECT * FROM event_kinds_view ORDER BY count DESC
|
||||
|
||||
-- Active subscriptions
|
||||
SELECT * FROM active_subscriptions_log ORDER BY created_at DESC
|
||||
|
||||
-- Database statistics
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM events) as total_events,
|
||||
(SELECT COUNT(*) FROM subscription_events) as total_subscriptions
|
||||
```
|
||||
|
||||
## Real-time Monitoring System
|
||||
|
||||
C-Relay includes a subscription-based monitoring system that broadcasts real-time relay statistics using ephemeral events (kind 24567).
|
||||
|
||||
### Activation
|
||||
|
||||
The monitoring system activates automatically when clients subscribe to kind 24567 events:
|
||||
|
||||
```json
|
||||
["REQ", "monitoring-sub", {"kinds": [24567]}]
|
||||
```
|
||||
|
||||
For specific monitoring types, use d-tag filters:
|
||||
|
||||
```json
|
||||
["REQ", "event-kinds-sub", {"kinds": [24567], "#d": ["event_kinds"]}]
|
||||
["REQ", "time-stats-sub", {"kinds": [24567], "#d": ["time_stats"]}]
|
||||
["REQ", "top-pubkeys-sub", {"kinds": [24567], "#d": ["top_pubkeys"]}]
|
||||
```
|
||||
|
||||
When no subscriptions exist, monitoring is dormant to conserve resources.
|
||||
|
||||
### Monitoring Event Types
|
||||
|
||||
|
||||
| Type | d Tag | Description |
|
||||
| ---------------------- | ------------------------ | ------------------------------------------- |
|
||||
| Event Distribution | `event_kinds` | Event count by kind with percentages |
|
||||
| Time Statistics | `time_stats` | Events in last 24h, 7d, 30d |
|
||||
| Top Publishers | `top_pubkeys` | Top 10 pubkeys by event count |
|
||||
| Active Subscriptions | `active_subscriptions` | Current subscription details (admin only) |
|
||||
| Subscription Details | `subscription_details` | Detailed subscription info (admin only) |
|
||||
| CPU Metrics | `cpu_metrics` | Process CPU and memory usage |
|
||||
|
||||
### Event Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 24567,
|
||||
"pubkey": "<relay_pubkey>",
|
||||
"created_at": <timestamp>,
|
||||
"content": "{\"data_type\":\"event_kinds\",\"timestamp\":1234567890,...}",
|
||||
"tags": [
|
||||
["d", "event_kinds"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
- `kind_24567_reporting_throttle_sec`: Minimum seconds between monitoring events (default: 5)
|
||||
|
||||
### Web Dashboard Integration
|
||||
|
||||
The built-in web dashboard (`/api/`) automatically subscribes to monitoring events and displays real-time statistics.
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Monitoring events are ephemeral (not stored in database)
|
||||
- Throttling prevents excessive event generation
|
||||
- Automatic activation/deactivation based on subscriptions
|
||||
- Minimal overhead when no clients are monitoring
|
||||
|
||||
## Direct Messaging Admin System
|
||||
|
||||
In addition to the above admin API, c-relay allows the administrator to direct message the relay to get information or control some settings. As long as the administrator is signed in with any nostr client that allows sending nip-17 direct messages (DMs), they can control the relay.
|
||||
|
||||
The is possible because the relay is a full nostr citizen with it's own private and public key, and it knows the administrator's public key.
|
||||
|
||||
**Available DM commands**
|
||||
|
||||
The intent is not to be strict in the formatting of the DM. So for example if the relay receives any DM from the administrator with the words "stats" or "statistics" in it, it will respond to the administrator with a reply DM with the current relay statistics.
|
||||
|
||||
- `stats`|`statistics`: Relay statistics
|
||||
- `config`|`configuration`: Relay configuration
|
||||
127
README.md
127
README.md
@@ -164,6 +164,8 @@ All commands are sent as NIP-44 encrypted JSON arrays in the event content. The
|
||||
| `system_clear_auth` | `["system_command", "clear_all_auth_rules"]` | Clear all auth rules |
|
||||
| `system_status` | `["system_command", "system_status"]` | Get system status |
|
||||
| `stats_query` | `["stats_query"]` | Get comprehensive database statistics |
|
||||
| **Database Queries** |
|
||||
| `sql_query` | `["sql_query", "SELECT * FROM events LIMIT 10"]` | Execute read-only SQL query against relay database |
|
||||
|
||||
### Available Configuration Keys
|
||||
|
||||
@@ -193,6 +195,9 @@ All commands are sent as NIP-44 encrypted JSON arrays in the event content. The
|
||||
- `pow_min_difficulty`: Minimum proof-of-work difficulty
|
||||
- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`)
|
||||
|
||||
**Monitoring Settings:**
|
||||
- `kind_24567_reporting_throttle_sec`: Minimum seconds between monitoring events (default: 5)
|
||||
|
||||
### Dynamic Configuration Updates
|
||||
|
||||
C-Relay supports **dynamic configuration updates** without requiring a restart for most settings. Configuration parameters are categorized as either **dynamic** (can be updated immediately) or **restart-required** (require relay restart to take effect).
|
||||
@@ -320,8 +325,68 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**SQL Query Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"sql_query\", \"request_id\": \"request_event_id\", \"timestamp\": 1234567890, \"query\": \"SELECT * FROM events LIMIT 10\", \"execution_time_ms\": 45, \"row_count\": 10, \"columns\": [\"id\", \"pubkey\", \"created_at\", \"kind\", \"content\"], \"rows\": [[\"abc123...\", \"def456...\", 1234567890, 1, \"Hello world\"], ...]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"],
|
||||
["e", "request_event_id"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
### SQL Query Command
|
||||
|
||||
The `sql_query` command allows administrators to execute read-only SQL queries against the relay database. This provides powerful analytics and debugging capabilities through the admin API.
|
||||
|
||||
**Request/Response Correlation:**
|
||||
- Each response includes the request event ID in both the `tags` array (`["e", "request_event_id"]`) and the decrypted content (`"request_id": "request_event_id"`)
|
||||
- This allows proper correlation when multiple queries are submitted concurrently
|
||||
- Frontend can track pending queries and match responses to requests
|
||||
|
||||
**Security Features:**
|
||||
- Only SELECT statements allowed (INSERT, UPDATE, DELETE, DROP, etc. are blocked)
|
||||
- Query timeout: 5 seconds (configurable)
|
||||
- Result row limit: 1000 rows (configurable)
|
||||
- All queries logged with execution time
|
||||
|
||||
**Available Tables and Views:**
|
||||
- `events` - All Nostr events
|
||||
- `config` - Configuration parameters
|
||||
- `auth_rules` - Authentication rules
|
||||
- `subscription_events` - Subscription lifecycle log
|
||||
- `event_broadcasts` - Event broadcast log
|
||||
- `recent_events` - Last 1000 events (view)
|
||||
- `event_stats` - Event statistics by type (view)
|
||||
- `subscription_analytics` - Subscription metrics (view)
|
||||
- `active_subscriptions_log` - Currently active subscriptions (view)
|
||||
- `event_kinds_view` - Event distribution by kind (view)
|
||||
- `top_pubkeys_view` - Top 10 pubkeys by event count (view)
|
||||
- `time_stats_view` - Time-based statistics (view)
|
||||
|
||||
**Example Queries:**
|
||||
```sql
|
||||
-- Recent events
|
||||
SELECT id, pubkey, created_at, kind FROM events ORDER BY created_at DESC LIMIT 20
|
||||
|
||||
-- Event distribution by kind
|
||||
SELECT * FROM event_kinds_view ORDER BY count DESC
|
||||
|
||||
-- Active subscriptions
|
||||
SELECT * FROM active_subscriptions_log ORDER BY created_at DESC
|
||||
|
||||
-- Database statistics
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM events) as total_events,
|
||||
(SELECT COUNT(*) FROM subscription_events) as total_subscriptions
|
||||
```
|
||||
|
||||
|
||||
@@ -329,6 +394,68 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
|
||||
|
||||
|
||||
|
||||
## Real-time Monitoring System
|
||||
|
||||
C-Relay includes a subscription-based monitoring system that broadcasts real-time relay statistics using ephemeral events (kind 24567).
|
||||
|
||||
### Activation
|
||||
|
||||
The monitoring system activates automatically when clients subscribe to kind 24567 events:
|
||||
|
||||
```json
|
||||
["REQ", "monitoring-sub", {"kinds": [24567]}]
|
||||
```
|
||||
|
||||
For specific monitoring types, use d-tag filters:
|
||||
|
||||
```json
|
||||
["REQ", "event-kinds-sub", {"kinds": [24567], "#d": ["event_kinds"]}]
|
||||
["REQ", "time-stats-sub", {"kinds": [24567], "#d": ["time_stats"]}]
|
||||
["REQ", "top-pubkeys-sub", {"kinds": [24567], "#d": ["top_pubkeys"]}]
|
||||
```
|
||||
|
||||
When no subscriptions exist, monitoring is dormant to conserve resources.
|
||||
|
||||
### Monitoring Event Types
|
||||
|
||||
| Type | d Tag | Description |
|
||||
|------|-------|-------------|
|
||||
| Event Distribution | `event_kinds` | Event count by kind with percentages |
|
||||
| Time Statistics | `time_stats` | Events in last 24h, 7d, 30d |
|
||||
| Top Publishers | `top_pubkeys` | Top 10 pubkeys by event count |
|
||||
| Active Subscriptions | `active_subscriptions` | Current subscription details (admin only) |
|
||||
| Subscription Details | `subscription_details` | Detailed subscription info (admin only) |
|
||||
| CPU Metrics | `cpu_metrics` | Process CPU and memory usage |
|
||||
|
||||
### Event Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 24567,
|
||||
"pubkey": "<relay_pubkey>",
|
||||
"created_at": <timestamp>,
|
||||
"content": "{\"data_type\":\"event_kinds\",\"timestamp\":1234567890,...}",
|
||||
"tags": [
|
||||
["d", "event_kinds"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
- `kind_24567_reporting_throttle_sec`: Minimum seconds between monitoring events (default: 5)
|
||||
|
||||
### Web Dashboard Integration
|
||||
|
||||
The built-in web dashboard (`/api/`) automatically subscribes to monitoring events and displays real-time statistics.
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Monitoring events are ephemeral (not stored in database)
|
||||
- Throttling prevents excessive event generation
|
||||
- Automatic activation/deactivation based on subscriptions
|
||||
- Minimal overhead when no clients are monitoring
|
||||
|
||||
## Direct Messaging Admin System
|
||||
|
||||
In addition to the above admin API, c-relay allows the administrator to direct message the relay to get information or control some settings. As long as the administrator is signed in with any nostr client that allows sending nip-17 direct messages (DMs), they can control the relay.
|
||||
|
||||
58
api/embedded.html
Normal file
58
api/embedded.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Embedded NOSTR_LOGIN_LITE</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#login-container {
|
||||
/* No styling - let embedded modal blend seamlessly */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="login-container"></div>
|
||||
</div>
|
||||
|
||||
<script src="../lite/nostr.bundle.js"></script>
|
||||
<script src="../lite/nostr-lite.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
theme:'default',
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
seedphrase: true,
|
||||
readonly: true,
|
||||
connect: true,
|
||||
remote: true,
|
||||
otp: true
|
||||
}
|
||||
});
|
||||
|
||||
window.NOSTR_LOGIN_LITE.embed('#login-container', {
|
||||
seamless: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
875
api/index.css
875
api/index.css
File diff suppressed because it is too large
Load Diff
475
api/index.html
475
api/index.html
@@ -4,214 +4,125 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>C-Relay Admin API</title>
|
||||
<title>C-Relay Admin</title>
|
||||
<link rel="stylesheet" href="/api/index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>C-RELAY ADMIN API</h1>
|
||||
<!-- Side Navigation Menu -->
|
||||
<nav class="side-nav" id="side-nav">
|
||||
<ul class="nav-menu">
|
||||
<li><button class="nav-item" data-page="statistics">Statistics</button></li>
|
||||
<li><button class="nav-item" data-page="subscriptions">Subscriptions</button></li>
|
||||
<li><button class="nav-item" data-page="configuration">Configuration</button></li>
|
||||
<li><button class="nav-item" data-page="authorization">Authorization</button></li>
|
||||
<li><button class="nav-item" data-page="relay-events">Relay Events</button></li>
|
||||
<li><button class="nav-item" data-page="dm">DM</button></li>
|
||||
<li><button class="nav-item" data-page="database">Database Query</button></li>
|
||||
</ul>
|
||||
<div class="nav-footer">
|
||||
<button class="nav-footer-btn" id="nav-dark-mode-btn">DARK MODE</button>
|
||||
<button class="nav-footer-btn" id="nav-logout-btn">LOGOUT</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Sections Wrapper -->
|
||||
<div class="main-sections-wrapper">
|
||||
<!-- Side Navigation Overlay -->
|
||||
<div class="side-nav-overlay" id="side-nav-overlay"></div>
|
||||
|
||||
<!-- Persistent Authentication Header - Always Visible -->
|
||||
<div id="persistent-auth-container" class="section flex-section">
|
||||
<div class="user-info-container">
|
||||
<button type="button" id="login-logout-btn" class="login-logout-btn">LOGIN</button>
|
||||
<div class="user-details" id="persistent-user-details" style="display: none;">
|
||||
<div><strong>Name:</strong> <span id="persistent-user-name">Loading...</span></div>
|
||||
<div><strong>Public Key:</strong>
|
||||
<div class="user-pubkey" id="persistent-user-pubkey">Loading...</div>
|
||||
</div>
|
||||
<div><strong>About:</strong> <span id="persistent-user-about">Loading...</span></div>
|
||||
<!-- Header with title and profile display -->
|
||||
<div class="section">
|
||||
|
||||
<div class="header-content">
|
||||
<div class="header-title clickable" id="header-title">
|
||||
<span class="relay-letter" data-letter="R">R</span>
|
||||
<span class="relay-letter" data-letter="E">E</span>
|
||||
<span class="relay-letter" data-letter="L">L</span>
|
||||
<span class="relay-letter" data-letter="A">A</span>
|
||||
<span class="relay-letter" data-letter="Y">Y</span>
|
||||
</div>
|
||||
<div class="relay-info">
|
||||
<div id="relay-name" class="relay-name">C-Relay</div>
|
||||
<div id="relay-description" class="relay-description">Loading...</div>
|
||||
<div id="relay-pubkey-container" class="relay-pubkey-container">
|
||||
<div id="relay-pubkey" class="relay-pubkey">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Section -->
|
||||
<div id="login-section" class="flex-section">
|
||||
<div class="section">
|
||||
<h2>NOSTR AUTHENTICATION</h2>
|
||||
<p id="login-instructions">Please login with your Nostr identity to access the admin interface.</p>
|
||||
<!-- nostr-lite login UI will be injected here -->
|
||||
<div class="profile-area" id="profile-area" style="display: none;">
|
||||
<div class="admin-label">admin</div>
|
||||
<div class="profile-container">
|
||||
<img id="header-user-image" class="header-user-image" alt="Profile" style="display: none;">
|
||||
<span id="header-user-name" class="header-user-name">Loading...</span>
|
||||
</div>
|
||||
<!-- Logout dropdown -->
|
||||
<!-- Dropdown menu removed - buttons moved to sidebar -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Relay Connection Section -->
|
||||
<div id="relay-connection-section" class="flex-section">
|
||||
<div class="section">
|
||||
<h2>RELAY CONNECTION</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="relay-connection-url">Relay URL:</label>
|
||||
<input type="text" id="relay-connection-url" value="ws://localhost:8888"
|
||||
placeholder="ws://localhost:8888 or wss://relay.example.com">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="relay-pubkey-manual">Relay Pubkey (if not available via NIP-11):</label>
|
||||
<input type="text" id="relay-pubkey-manual" placeholder="64-character hex pubkey"
|
||||
pattern="[0-9a-fA-F]{64}" title="64-character hexadecimal public key">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
|
||||
<button type="button" id="disconnect-relay-btn" disabled>DISCONNECT</button>
|
||||
<button type="button" id="restart-relay-btn" disabled>RESTART RELAY</button>
|
||||
</div>
|
||||
|
||||
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>
|
||||
|
||||
<!-- Relay Information Display -->
|
||||
<div id="relay-info-display" class="hidden">
|
||||
<h3>Relay Information (NIP-11)</h3>
|
||||
<table class="config-table" id="relay-info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="relay-info-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div> <!-- End Main Sections Wrapper -->
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Testing Section -->
|
||||
<div id="div_config" class="section flex-section" style="display: none;">
|
||||
<h2>RELAY CONFIGURATION</h2>
|
||||
<div id="config-display" class="hidden">
|
||||
<div class="config-table-container">
|
||||
<table class="config-table" id="config-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="config-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="fetch-config-btn">REFRESH</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth Rules Management - Moved after configuration -->
|
||||
<div class="section flex-section" id="authRulesSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2>AUTH RULES MANAGEMENT</h2>
|
||||
</div>
|
||||
|
||||
<!-- Auth Rules Table -->
|
||||
<div id="authRulesTableContainer" style="display: none;">
|
||||
<table class="config-table" id="authRulesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rule Type</th>
|
||||
<th>Pattern Type</th>
|
||||
<th>Pattern Value</th>
|
||||
<th>Action</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="authRulesTableBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Simplified Auth Rule Input Section -->
|
||||
<div id="authRuleInputSections" style="display: block;">
|
||||
|
||||
<!-- Combined Pubkey Auth Rule Section -->
|
||||
|
||||
|
||||
<div class="input-group">
|
||||
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
|
||||
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
|
||||
|
||||
</div>
|
||||
<div id="whitelistWarning" class="warning-box" style="display: none;">
|
||||
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
|
||||
mode.
|
||||
Only whitelisted users will be able to interact with the relay.
|
||||
</div>
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
|
||||
WHITELIST</button>
|
||||
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
|
||||
BLACKLIST</button>
|
||||
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Login Modal Overlay -->
|
||||
<div id="login-modal" class="login-modal-overlay" style="display: none;">
|
||||
<div class="login-modal-content">
|
||||
<div id="login-modal-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DATABASE STATISTICS Section -->
|
||||
<div class="section">
|
||||
<!-- Subscribe to kind 24567 events to receive real-time monitoring data -->
|
||||
<div class="section flex-section" id="databaseStatisticsSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2>DATABASE STATISTICS</h2>
|
||||
DATABASE STATISTICS
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Event Rate Graph Container -->
|
||||
<div id="event-rate-chart"></div>
|
||||
|
||||
<!-- Database Overview Table -->
|
||||
<div class="input-group">
|
||||
<label>Database Overview:</label>
|
||||
<div class="config-table-container">
|
||||
<table class="config-table" id="stats-overview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Value</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="stats-overview-table-body">
|
||||
<tr>
|
||||
<td>Database Size</td>
|
||||
<td id="db-size">-</td>
|
||||
<td>Current database file size</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Events</td>
|
||||
<td id="total-events">-</td>
|
||||
<td>Total number of events stored</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Process ID</td>
|
||||
<td id="process-id">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Active Subscriptions</td>
|
||||
<td id="active-subscriptions">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory Usage</td>
|
||||
<td id="memory-usage">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU Core</td>
|
||||
<td id="cpu-core">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU Usage</td>
|
||||
<td id="cpu-usage">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Oldest Event</td>
|
||||
<td id="oldest-event">-</td>
|
||||
<td>Timestamp of oldest event</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Newest Event</td>
|
||||
<td id="newest-event">-</td>
|
||||
<td>Timestamp of newest event</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -248,24 +159,20 @@
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th>Events</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="stats-time-table-body">
|
||||
<tr>
|
||||
<td>Last 24 Hours</td>
|
||||
<td id="events-24h">-</td>
|
||||
<td>Events in the last day</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last 7 Days</td>
|
||||
<td id="events-7d">-</td>
|
||||
<td>Events in the last week</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last 30 Days</td>
|
||||
<td id="events-30d">-</td>
|
||||
<td>Events in the last month</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -294,12 +201,122 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
</div>
|
||||
|
||||
<!-- SUBSCRIPTION DETAILS Section (Admin Only) -->
|
||||
<div class="section flex-section" id="subscriptionDetailsSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
ACTIVE SUBSCRIPTION DETAILS
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<button type="button" id="refresh-stats-btn">REFRESH STATISTICS</button>
|
||||
<div class="config-table-container">
|
||||
<table class="config-table" id="subscription-details-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Subscription ID</th>
|
||||
<th>Client IP</th>
|
||||
<th>WSI Pointer</th>
|
||||
<th>Duration</th>
|
||||
<th>Filters</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subscription-details-table-body">
|
||||
<tr>
|
||||
<td colspan="5" style="text-align: center; font-style: italic;">No subscriptions active</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testing Section -->
|
||||
<div id="div_config" class="section flex-section" style="display: none;">
|
||||
<div class="section-header">
|
||||
RELAY CONFIGURATION
|
||||
</div>
|
||||
<div id="config-display" class="hidden">
|
||||
<div class="config-table-container">
|
||||
<table class="config-table" id="config-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="config-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="fetch-config-btn">REFRESH</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth Rules Management - Moved after configuration -->
|
||||
<div class="section flex-section" id="authRulesSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
AUTH RULES MANAGEMENT
|
||||
</div>
|
||||
|
||||
<!-- Auth Rules Table -->
|
||||
<div id="authRulesTableContainer" style="display: none;">
|
||||
<table class="config-table" id="authRulesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rule Type</th>
|
||||
<th>Pattern Type</th>
|
||||
<th>Pattern Value</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="authRulesTableBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Simplified Auth Rule Input Section -->
|
||||
<div id="authRuleInputSections" style="display: block;">
|
||||
|
||||
<!-- Combined Pubkey Auth Rule Section -->
|
||||
|
||||
|
||||
<div class="input-group">
|
||||
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
|
||||
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
|
||||
|
||||
</div>
|
||||
<div id="whitelistWarning" class="warning-box" style="display: none;">
|
||||
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
|
||||
mode.
|
||||
Only whitelisted users will be able to interact with the relay.
|
||||
</div>
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
|
||||
WHITELIST</button>
|
||||
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
|
||||
BLACKLIST</button>
|
||||
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- NIP-17 DIRECT MESSAGES Section -->
|
||||
<div class="section" id="nip17DMSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
@@ -307,7 +324,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Outbox -->
|
||||
<div class="input-group">
|
||||
<div>
|
||||
<label for="dm-outbox">Send Message to Relay:</label>
|
||||
<textarea id="dm-outbox" rows="4" placeholder="Enter your message to send to the relay..."></textarea>
|
||||
</div>
|
||||
@@ -326,6 +343,120 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RELAY EVENTS Section -->
|
||||
<div class="section" id="relayEventsSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
RELAY EVENTS MANAGEMENT
|
||||
</div>
|
||||
|
||||
<!-- Kind 0: User Metadata -->
|
||||
<div class="input-group">
|
||||
<h3>Kind 0: User Metadata</h3>
|
||||
<div class="form-group">
|
||||
<label for="kind0-name">Name:</label>
|
||||
<input type="text" id="kind0-name" placeholder="Relay Name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="kind0-about">About:</label>
|
||||
<textarea id="kind0-about" rows="3" placeholder="Relay Description"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="kind0-picture">Picture URL:</label>
|
||||
<input type="url" id="kind0-picture" placeholder="https://example.com/logo.png">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="kind0-banner">Banner URL:</label>
|
||||
<input type="url" id="kind0-banner" placeholder="https://example.com/banner.png">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="kind0-nip05">NIP-05:</label>
|
||||
<input type="text" id="kind0-nip05" placeholder="relay@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="kind0-website">Website:</label>
|
||||
<input type="url" id="kind0-website" placeholder="https://example.com">
|
||||
</div>
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="submit-kind0-btn">UPDATE METADATA</button>
|
||||
</div>
|
||||
<div id="kind0-status" class="status-message"></div>
|
||||
</div>
|
||||
|
||||
<!-- Kind 10050: DM Relay List -->
|
||||
<div class="input-group">
|
||||
<h3>Kind 10050: DM Relay List</h3>
|
||||
<div class="form-group">
|
||||
<label for="kind10050-relays">Relay URLs (one per line):</label>
|
||||
<textarea id="kind10050-relays" rows="4" placeholder="wss://relay1.com wss://relay2.com"></textarea>
|
||||
</div>
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="submit-kind10050-btn">UPDATE DM RELAYS</button>
|
||||
</div>
|
||||
<div id="kind10050-status" class="status-message"></div>
|
||||
</div>
|
||||
|
||||
<!-- Kind 10002: Relay List -->
|
||||
<div class="input-group">
|
||||
<h3>Kind 10002: Relay List</h3>
|
||||
<div id="kind10002-relay-entries">
|
||||
<!-- Dynamic relay entries will be added here -->
|
||||
</div>
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="add-relay-entry-btn">ADD RELAY</button>
|
||||
<button type="button" id="submit-kind10002-btn">UPDATE RELAYS</button>
|
||||
</div>
|
||||
<div id="kind10002-status" class="status-message"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SQL QUERY Section -->
|
||||
<div class="section" id="sqlQuerySection" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2>SQL QUERY CONSOLE</h2>
|
||||
</div>
|
||||
|
||||
<!-- Query Selector -->
|
||||
<div class="input-group">
|
||||
<label for="query-dropdown">Quick Queries & History:</label>
|
||||
<select id="query-dropdown" onchange="loadSelectedQuery()">
|
||||
<option value="">-- Select a query --</option>
|
||||
<optgroup label="Common Queries">
|
||||
<option value="recent_events">Recent Events</option>
|
||||
<option value="event_stats">Event Statistics</option>
|
||||
<option value="subscriptions">Active Subscriptions</option>
|
||||
<option value="top_pubkeys">Top Pubkeys</option>
|
||||
<option value="event_kinds">Event Kinds Distribution</option>
|
||||
<option value="time_stats">Time-based Statistics</option>
|
||||
</optgroup>
|
||||
<optgroup label="Query History" id="history-group">
|
||||
<!-- Dynamically populated from localStorage -->
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Query Editor -->
|
||||
<div class="input-group">
|
||||
<label for="sql-input">SQL Query:</label>
|
||||
<textarea id="sql-input" rows="5" placeholder="SELECT * FROM events LIMIT 10"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Query Actions -->
|
||||
<div class="input-group">
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="execute-sql-btn">EXECUTE QUERY</button>
|
||||
<button type="button" id="clear-sql-btn">CLEAR</button>
|
||||
<button type="button" id="clear-history-btn">CLEAR HISTORY</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Query Results -->
|
||||
<div class="input-group">
|
||||
<label>Query Results:</label>
|
||||
<div id="query-info" class="info-box"></div>
|
||||
<div id="query-table" class="config-table-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load the official nostr-tools bundle first -->
|
||||
<!-- <script src="https://laantungir.net/nostr-login-lite/nostr.bundle.js"></script> -->
|
||||
<script src="/api/nostr.bundle.js"></script>
|
||||
@@ -333,6 +464,8 @@
|
||||
<!-- Load NOSTR_LOGIN_LITE main library -->
|
||||
<!-- <script src="https://laantungir.net/nostr-login-lite/nostr-lite.js"></script> -->
|
||||
<script src="/api/nostr-lite.js"></script>
|
||||
<!-- Load text_graph library -->
|
||||
<script src="/api/text_graph.js"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
8409
api/index.js
8409
api/index.js
File diff suppressed because it is too large
Load Diff
463
api/text_graph.js
Normal file
463
api/text_graph.js
Normal file
@@ -0,0 +1,463 @@
|
||||
/**
|
||||
* ASCIIBarChart - A dynamic ASCII-based vertical bar chart renderer
|
||||
*
|
||||
* Creates real-time animated bar charts using monospaced characters (X)
|
||||
* with automatic scaling, labels, and responsive font sizing.
|
||||
*/
|
||||
class ASCIIBarChart {
|
||||
/**
|
||||
* Create a new ASCII bar chart
|
||||
* @param {string} containerId - The ID of the HTML element to render the chart in
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {number} [options.maxHeight=20] - Maximum height of the chart in rows
|
||||
* @param {number} [options.maxDataPoints=30] - Maximum number of data columns before scrolling
|
||||
* @param {string} [options.title=''] - Chart title (displayed centered at top)
|
||||
* @param {string} [options.xAxisLabel=''] - X-axis label (displayed centered at bottom)
|
||||
* @param {string} [options.yAxisLabel=''] - Y-axis label (displayed vertically on left)
|
||||
* @param {boolean} [options.autoFitWidth=true] - Automatically adjust font size to fit container width
|
||||
* @param {boolean} [options.useBinMode=false] - Enable time bin mode for data aggregation
|
||||
* @param {number} [options.binDuration=10000] - Duration of each time bin in milliseconds (10 seconds default)
|
||||
* @param {string} [options.xAxisLabelFormat='elapsed'] - X-axis label format: 'elapsed', 'bins', 'timestamps', 'ranges'
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
*/
|
||||
constructor(containerId, options = {}) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.data = [];
|
||||
this.maxHeight = options.maxHeight || 20;
|
||||
this.maxDataPoints = options.maxDataPoints || 30;
|
||||
this.totalDataPoints = 0; // Track total number of data points added
|
||||
this.title = options.title || '';
|
||||
this.xAxisLabel = options.xAxisLabel || '';
|
||||
this.yAxisLabel = options.yAxisLabel || '';
|
||||
this.autoFitWidth = options.autoFitWidth !== false; // Default to true
|
||||
this.debug = options.debug || false; // Debug logging option
|
||||
|
||||
// Time bin configuration
|
||||
this.useBinMode = options.useBinMode !== false; // Default to true
|
||||
this.binDuration = options.binDuration || 4000; // 4 seconds default
|
||||
this.xAxisLabelFormat = options.xAxisLabelFormat || 'elapsed';
|
||||
|
||||
// Time bin data structures
|
||||
this.bins = [];
|
||||
this.currentBinIndex = -1;
|
||||
this.binStartTime = null;
|
||||
this.binCheckInterval = null;
|
||||
this.chartStartTime = Date.now();
|
||||
|
||||
// Set up resize observer if auto-fit is enabled
|
||||
if (this.autoFitWidth) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.adjustFontSize();
|
||||
});
|
||||
this.resizeObserver.observe(this.container);
|
||||
}
|
||||
|
||||
// Initialize first bin if bin mode is enabled
|
||||
if (this.useBinMode) {
|
||||
this.initializeBins();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new data point to the chart
|
||||
* @param {number} value - The numeric value to add
|
||||
*/
|
||||
addValue(value) {
|
||||
// Time bin mode: add value to current active bin count
|
||||
this.checkBinRotation(); // Ensure we have an active bin
|
||||
this.bins[this.currentBinIndex].count += value; // Changed from ++ to += value
|
||||
this.totalDataPoints++;
|
||||
|
||||
this.render();
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data from the chart
|
||||
*/
|
||||
clear() {
|
||||
this.data = [];
|
||||
this.totalDataPoints = 0;
|
||||
|
||||
if (this.useBinMode) {
|
||||
this.bins = [];
|
||||
this.currentBinIndex = -1;
|
||||
this.binStartTime = null;
|
||||
this.initializeBins();
|
||||
}
|
||||
|
||||
this.render();
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the width of the chart in characters
|
||||
* @returns {number} The chart width in characters
|
||||
* @private
|
||||
*/
|
||||
getChartWidth() {
|
||||
let dataLength = this.maxDataPoints; // Always use maxDataPoints for consistent width
|
||||
|
||||
if (dataLength === 0) return 50; // Default width for empty chart
|
||||
|
||||
const yAxisPadding = this.yAxisLabel ? 2 : 0;
|
||||
const yAxisNumbers = 3; // Width of Y-axis numbers
|
||||
const separator = 1; // The '|' character
|
||||
// const dataWidth = dataLength * 2; // Each column is 2 characters wide // TEMP: commented for no-space test
|
||||
const dataWidth = dataLength; // Each column is 1 character wide // TEMP: adjusted for no-space columns
|
||||
const padding = 1; // Extra padding
|
||||
|
||||
const totalWidth = yAxisPadding + yAxisNumbers + separator + dataWidth + padding;
|
||||
|
||||
// Only log when width changes
|
||||
if (this.debug && this.lastChartWidth !== totalWidth) {
|
||||
console.log('getChartWidth changed:', { dataLength, totalWidth, previous: this.lastChartWidth });
|
||||
this.lastChartWidth = totalWidth;
|
||||
}
|
||||
|
||||
return totalWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust font size to fit container width
|
||||
* @private
|
||||
*/
|
||||
adjustFontSize() {
|
||||
if (!this.autoFitWidth) return;
|
||||
|
||||
const containerWidth = this.container.clientWidth;
|
||||
const chartWidth = this.getChartWidth();
|
||||
|
||||
if (chartWidth === 0) return;
|
||||
|
||||
// Calculate optimal font size
|
||||
// For monospace fonts, character width is approximately 0.6 * font size
|
||||
// Use a slightly smaller ratio to fit more content
|
||||
const charWidthRatio = 0.7;
|
||||
const padding = 30; // Reduce padding to fit more content
|
||||
const availableWidth = containerWidth - padding;
|
||||
const optimalFontSize = Math.floor((availableWidth / chartWidth) / charWidthRatio);
|
||||
|
||||
// Set reasonable bounds (min 4px, max 20px)
|
||||
const fontSize = Math.max(4, Math.min(20, optimalFontSize));
|
||||
|
||||
// Only log when font size changes
|
||||
if (this.debug && this.lastFontSize !== fontSize) {
|
||||
console.log('fontSize changed:', { containerWidth, chartWidth, fontSize, previous: this.lastFontSize });
|
||||
this.lastFontSize = fontSize;
|
||||
}
|
||||
|
||||
this.container.style.fontSize = fontSize + 'px';
|
||||
this.container.style.lineHeight = '1.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the chart to the container
|
||||
* @private
|
||||
*/
|
||||
render() {
|
||||
let dataToRender = [];
|
||||
let maxValue = 0;
|
||||
let minValue = 0;
|
||||
let valueRange = 0;
|
||||
|
||||
if (this.useBinMode) {
|
||||
// Bin mode: render bin counts
|
||||
if (this.bins.length === 0) {
|
||||
this.container.textContent = 'No data yet. Click Start to begin.';
|
||||
return;
|
||||
}
|
||||
// Always create a fixed-length array filled with 0s, then overlay actual bin data
|
||||
dataToRender = new Array(this.maxDataPoints).fill(0);
|
||||
|
||||
// Overlay actual bin data (most recent bins, reversed for left-to-right display)
|
||||
const startIndex = Math.max(0, this.bins.length - this.maxDataPoints);
|
||||
const recentBins = this.bins.slice(startIndex);
|
||||
|
||||
// Reverse the bins so most recent is on the left, and overlay onto the fixed array
|
||||
recentBins.reverse().forEach((bin, index) => {
|
||||
if (index < this.maxDataPoints) {
|
||||
dataToRender[index] = bin.count;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.debug) {
|
||||
console.log('render() dataToRender:', dataToRender, 'bins length:', this.bins.length);
|
||||
}
|
||||
maxValue = Math.max(...dataToRender);
|
||||
minValue = Math.min(...dataToRender);
|
||||
valueRange = maxValue - minValue;
|
||||
} else {
|
||||
// Legacy mode: render individual values
|
||||
if (this.data.length === 0) {
|
||||
this.container.textContent = 'No data yet. Click Start to begin.';
|
||||
return;
|
||||
}
|
||||
dataToRender = this.data;
|
||||
maxValue = Math.max(...this.data);
|
||||
minValue = Math.min(...this.data);
|
||||
valueRange = maxValue - minValue;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const scale = this.maxHeight;
|
||||
|
||||
// Calculate scaling factor: each X represents at least 1 count
|
||||
const maxCount = Math.max(...dataToRender);
|
||||
const scaleFactor = Math.max(1, Math.ceil(maxCount / scale)); // 1 X = scaleFactor counts
|
||||
const scaledMax = Math.ceil(maxCount / scaleFactor) * scaleFactor;
|
||||
|
||||
// Calculate Y-axis label width (for vertical text)
|
||||
const yLabelWidth = this.yAxisLabel ? 2 : 0;
|
||||
const yAxisPadding = this.yAxisLabel ? ' ' : '';
|
||||
|
||||
// Add title if provided (centered)
|
||||
if (this.title) {
|
||||
// const chartWidth = 4 + this.maxDataPoints * 2; // Y-axis numbers + data columns // TEMP: commented for no-space test
|
||||
const chartWidth = 4 + this.maxDataPoints; // Y-axis numbers + data columns // TEMP: adjusted for no-space columns
|
||||
const titlePadding = Math.floor((chartWidth - this.title.length) / 2);
|
||||
output += yAxisPadding + ' '.repeat(Math.max(0, titlePadding)) + this.title + '\n\n';
|
||||
}
|
||||
|
||||
// Draw from top to bottom
|
||||
for (let row = scale; row > 0; row--) {
|
||||
let line = '';
|
||||
|
||||
// Add vertical Y-axis label character
|
||||
if (this.yAxisLabel) {
|
||||
const L = this.yAxisLabel.length;
|
||||
const startRow = Math.floor((scale - L) / 2) + 1;
|
||||
const relativeRow = scale - row + 1; // 1 at top, scale at bottom
|
||||
if (relativeRow >= startRow && relativeRow < startRow + L) {
|
||||
const labelIndex = relativeRow - startRow;
|
||||
line += this.yAxisLabel[labelIndex] + ' ';
|
||||
} else {
|
||||
line += ' ';
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the actual count value this row represents (1 at bottom, increasing upward)
|
||||
const rowCount = (row - 1) * scaleFactor + 1;
|
||||
|
||||
// Add Y-axis label (show actual count values)
|
||||
line += String(rowCount).padStart(3, ' ') + ' |';
|
||||
|
||||
// Draw each column
|
||||
for (let i = 0; i < dataToRender.length; i++) {
|
||||
const count = dataToRender[i];
|
||||
const scaledHeight = Math.ceil(count / scaleFactor);
|
||||
|
||||
if (scaledHeight >= row) {
|
||||
// line += ' X'; // TEMP: commented out space between columns
|
||||
line += 'X'; // TEMP: no space between columns
|
||||
} else {
|
||||
// line += ' '; // TEMP: commented out space between columns
|
||||
line += ' '; // TEMP: single space for empty columns
|
||||
}
|
||||
}
|
||||
|
||||
output += line + '\n';
|
||||
}
|
||||
|
||||
// Draw X-axis
|
||||
// output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints * 2) + '\n'; // TEMP: commented out for no-space test
|
||||
output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints) + '\n'; // TEMP: back to original length
|
||||
|
||||
// Draw X-axis labels based on mode and format
|
||||
let xAxisLabels = yAxisPadding + ' '; // Initial padding to align with X-axis
|
||||
|
||||
// Determine label interval (every 5 columns)
|
||||
const labelInterval = 5;
|
||||
|
||||
// Generate all labels first and store in array
|
||||
let labels = [];
|
||||
for (let i = 0; i < this.maxDataPoints; i++) {
|
||||
if (i % labelInterval === 0) {
|
||||
let label = '';
|
||||
if (this.useBinMode) {
|
||||
// For bin mode, show labels for all possible positions
|
||||
// i=0 is leftmost (most recent), i=maxDataPoints-1 is rightmost (oldest)
|
||||
const elapsedSec = (i * this.binDuration) / 1000;
|
||||
// Format with appropriate precision for sub-second bins
|
||||
if (this.binDuration < 1000) {
|
||||
// Show decimal seconds for sub-second bins
|
||||
label = elapsedSec.toFixed(1) + 's';
|
||||
} else {
|
||||
// Show whole seconds for 1+ second bins
|
||||
label = String(Math.round(elapsedSec)) + 's';
|
||||
}
|
||||
} else {
|
||||
// For legacy mode, show data point numbers
|
||||
const startIndex = Math.max(1, this.totalDataPoints - this.maxDataPoints + 1);
|
||||
label = String(startIndex + i);
|
||||
}
|
||||
labels.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
// Build the label string with calculated spacing
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
const label = labels[i];
|
||||
xAxisLabels += label;
|
||||
|
||||
// Add spacing: labelInterval - label.length (except for last label)
|
||||
if (i < labels.length - 1) {
|
||||
const spacing = labelInterval - label.length;
|
||||
xAxisLabels += ' '.repeat(spacing);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the label line extends to match the X-axis dash line length
|
||||
// The dash line is this.maxDataPoints characters long, starting after " +"
|
||||
const dashLineLength = this.maxDataPoints;
|
||||
const minLabelLineLength = yAxisPadding.length + 4 + dashLineLength; // 4 for " "
|
||||
if (xAxisLabels.length < minLabelLineLength) {
|
||||
xAxisLabels += ' '.repeat(minLabelLineLength - xAxisLabels.length);
|
||||
}
|
||||
output += xAxisLabels + '\n';
|
||||
|
||||
// Add X-axis label if provided
|
||||
if (this.xAxisLabel) {
|
||||
// const labelPadding = Math.floor((this.maxDataPoints * 2 - this.xAxisLabel.length) / 2); // TEMP: commented for no-space test
|
||||
const labelPadding = Math.floor((this.maxDataPoints - this.xAxisLabel.length) / 2); // TEMP: adjusted for no-space columns
|
||||
output += '\n' + yAxisPadding + ' ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n';
|
||||
}
|
||||
|
||||
this.container.textContent = output;
|
||||
|
||||
// Adjust font size to fit width (only once at initialization)
|
||||
if (this.autoFitWidth) {
|
||||
this.adjustFontSize();
|
||||
}
|
||||
|
||||
// Update the external info display
|
||||
if (this.useBinMode) {
|
||||
const binCounts = this.bins.map(bin => bin.count);
|
||||
const scaleFactor = Math.max(1, Math.ceil(maxValue / scale));
|
||||
document.getElementById('values').textContent = `[${dataToRender.join(', ')}]`;
|
||||
document.getElementById('max-value').textContent = maxValue;
|
||||
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, 1X=${scaleFactor} counts`;
|
||||
} else {
|
||||
document.getElementById('values').textContent = `[${this.data.join(', ')}]`;
|
||||
document.getElementById('max-value').textContent = maxValue;
|
||||
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, Height: ${scale}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the info display
|
||||
* @private
|
||||
*/
|
||||
updateInfo() {
|
||||
if (this.useBinMode) {
|
||||
const totalCount = this.bins.reduce((sum, bin) => sum + bin.count, 0);
|
||||
document.getElementById('count').textContent = totalCount;
|
||||
} else {
|
||||
document.getElementById('count').textContent = this.data.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the bin system
|
||||
* @private
|
||||
*/
|
||||
initializeBins() {
|
||||
this.bins = [];
|
||||
this.currentBinIndex = -1;
|
||||
this.binStartTime = null;
|
||||
this.chartStartTime = Date.now();
|
||||
|
||||
// Create first bin
|
||||
this.rotateBin();
|
||||
|
||||
// Set up automatic bin rotation check
|
||||
this.binCheckInterval = setInterval(() => {
|
||||
this.checkBinRotation();
|
||||
}, 100); // Check every 100ms for responsiveness
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current bin should rotate and create new bin if needed
|
||||
* @private
|
||||
*/
|
||||
checkBinRotation() {
|
||||
if (!this.useBinMode || !this.binStartTime) return;
|
||||
|
||||
const now = Date.now();
|
||||
if ((now - this.binStartTime) >= this.binDuration) {
|
||||
this.rotateBin();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate to a new bin, finalizing the current one
|
||||
*/
|
||||
rotateBin() {
|
||||
// Finalize current bin if it exists
|
||||
if (this.currentBinIndex >= 0) {
|
||||
this.bins[this.currentBinIndex].isActive = false;
|
||||
}
|
||||
|
||||
// Create new bin
|
||||
const newBin = {
|
||||
startTime: Date.now(),
|
||||
count: 0,
|
||||
isActive: true
|
||||
};
|
||||
|
||||
this.bins.push(newBin);
|
||||
this.currentBinIndex = this.bins.length - 1;
|
||||
this.binStartTime = newBin.startTime;
|
||||
|
||||
// Keep only the most recent bins
|
||||
if (this.bins.length > this.maxDataPoints) {
|
||||
this.bins.shift();
|
||||
this.currentBinIndex--;
|
||||
}
|
||||
|
||||
// Ensure currentBinIndex points to the last bin (the active one)
|
||||
this.currentBinIndex = this.bins.length - 1;
|
||||
|
||||
// Force a render to update the display immediately
|
||||
this.render();
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format X-axis label for a bin based on the configured format
|
||||
* @param {number} binIndex - Index of the bin
|
||||
* @returns {string} Formatted label
|
||||
* @private
|
||||
*/
|
||||
formatBinLabel(binIndex) {
|
||||
const bin = this.bins[binIndex];
|
||||
if (!bin) return ' ';
|
||||
|
||||
switch (this.xAxisLabelFormat) {
|
||||
case 'bins':
|
||||
return String(binIndex + 1).padStart(2, ' ');
|
||||
|
||||
case 'timestamps':
|
||||
const time = new Date(bin.startTime);
|
||||
return time.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).replace(/:/g, '');
|
||||
|
||||
case 'ranges':
|
||||
const startSec = Math.floor((bin.startTime - this.chartStartTime) / 1000);
|
||||
const endSec = startSec + Math.floor(this.binDuration / 1000);
|
||||
return `${startSec}-${endSec}`;
|
||||
|
||||
case 'elapsed':
|
||||
default:
|
||||
// For elapsed time, always show time relative to the first bin (index 0)
|
||||
// This keeps the leftmost label as 0s and increases to the right
|
||||
const firstBinTime = this.bins[0] ? this.bins[0].startTime : this.chartStartTime;
|
||||
const elapsedSec = Math.floor((bin.startTime - firstBinTime) / 1000);
|
||||
return String(elapsedSec).padStart(2, ' ') + 's';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,616 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Global variables
|
||||
COMMIT_MESSAGE=""
|
||||
RELEASE_MODE=false
|
||||
|
||||
show_usage() {
|
||||
echo "C-Relay Build and Push Script"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " $0 \"commit message\" - Default: compile, increment patch, commit & push"
|
||||
echo " $0 -r \"commit message\" - Release: compile x86+arm64, increment minor, create release"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 \"Fixed event validation bug\""
|
||||
echo " $0 --release \"Major release with new features\""
|
||||
echo ""
|
||||
echo "Default Mode (patch increment):"
|
||||
echo " - Compile C-Relay"
|
||||
echo " - Increment patch version (v1.2.3 → v1.2.4)"
|
||||
echo " - Git add, commit with message, and push"
|
||||
echo ""
|
||||
echo "Release Mode (-r flag):"
|
||||
echo " - Compile C-Relay for x86_64 and arm64 (dynamic and static versions)"
|
||||
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
|
||||
echo " - Git add, commit, push, and create Gitea release"
|
||||
echo ""
|
||||
echo "Requirements for Release Mode:"
|
||||
echo " - For ARM64 builds: make install-arm64-deps (optional - will build x86_64 only if missing)"
|
||||
echo " - For static builds: sudo apt-get install musl-dev libcap-dev libuv1-dev libev-dev"
|
||||
echo " - Gitea token in ~/.gitea_token for release uploads"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-r|--release)
|
||||
RELEASE_MODE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# First non-flag argument is the commit message
|
||||
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
||||
COMMIT_MESSAGE="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate inputs
|
||||
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
||||
print_error "Commit message is required"
|
||||
echo ""
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we're in a git repository
|
||||
check_git_repo() {
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_error "Not in a git repository"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get current version and increment appropriately
|
||||
increment_version() {
|
||||
local increment_type="$1" # "patch" or "minor"
|
||||
|
||||
print_status "Getting current version..."
|
||||
|
||||
# Get the highest version tag (not chronologically latest)
|
||||
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "")
|
||||
if [[ -z "$LATEST_TAG" ]]; then
|
||||
LATEST_TAG="v0.0.0"
|
||||
print_warning "No version tags found, starting from $LATEST_TAG"
|
||||
fi
|
||||
|
||||
# Extract version components (remove 'v' prefix)
|
||||
VERSION=${LATEST_TAG#v}
|
||||
|
||||
# Parse major.minor.patch using regex
|
||||
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
MAJOR=${BASH_REMATCH[1]}
|
||||
MINOR=${BASH_REMATCH[2]}
|
||||
PATCH=${BASH_REMATCH[3]}
|
||||
else
|
||||
print_error "Invalid version format in tag: $LATEST_TAG"
|
||||
print_error "Expected format: v0.1.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Increment version based on type
|
||||
if [[ "$increment_type" == "minor" ]]; then
|
||||
# Minor release: increment minor, zero patch
|
||||
NEW_MINOR=$((MINOR + 1))
|
||||
NEW_PATCH=0
|
||||
NEW_VERSION="v${MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
print_status "Release mode: incrementing minor version"
|
||||
else
|
||||
# Default: increment patch
|
||||
NEW_PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||
print_status "Default mode: incrementing patch version"
|
||||
fi
|
||||
|
||||
print_status "Current version: $LATEST_TAG"
|
||||
print_status "New version: $NEW_VERSION"
|
||||
|
||||
# Export for use in other functions
|
||||
export NEW_VERSION
|
||||
}
|
||||
|
||||
# Function to compile the C-Relay project
|
||||
compile_project() {
|
||||
print_status "Compiling C-Relay..."
|
||||
|
||||
# Clean previous build
|
||||
if make clean > /dev/null 2>&1; then
|
||||
print_success "Cleaned previous build"
|
||||
else
|
||||
print_warning "Clean failed or no Makefile found"
|
||||
fi
|
||||
|
||||
# Force regenerate main.h to pick up new tags
|
||||
if make force-version > /dev/null 2>&1; then
|
||||
print_success "Regenerated main.h"
|
||||
else
|
||||
print_warning "Failed to regenerate main.h"
|
||||
fi
|
||||
|
||||
# Compile the project
|
||||
if make > /dev/null 2>&1; then
|
||||
print_success "C-Relay compiled successfully"
|
||||
else
|
||||
print_error "Compilation failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to build release binaries
|
||||
build_release_binaries() {
|
||||
print_status "Building release binaries..."
|
||||
|
||||
# Build x86_64 version
|
||||
print_status "Building x86_64 version..."
|
||||
make clean > /dev/null 2>&1
|
||||
if make x86 > /dev/null 2>&1; then
|
||||
if [[ -f "build/c_relay_x86" ]]; then
|
||||
cp build/c_relay_x86 c-relay-x86_64
|
||||
print_success "x86_64 binary created: c-relay-x86_64"
|
||||
else
|
||||
print_error "x86_64 binary not found after compilation"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "x86_64 build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Try to build ARM64 version
|
||||
print_status "Attempting ARM64 build..."
|
||||
make clean > /dev/null 2>&1
|
||||
if make arm64 > /dev/null 2>&1; then
|
||||
if [[ -f "build/c_relay_arm64" ]]; then
|
||||
cp build/c_relay_arm64 c-relay-arm64
|
||||
print_success "ARM64 binary created: c-relay-arm64"
|
||||
else
|
||||
print_warning "ARM64 binary not found after compilation"
|
||||
fi
|
||||
else
|
||||
print_warning "ARM64 build failed - ARM64 cross-compilation not properly set up"
|
||||
print_status "Only x86_64 binary will be included in release"
|
||||
fi
|
||||
|
||||
# Build static x86_64 version
|
||||
print_status "Building static x86_64 version..."
|
||||
make clean > /dev/null 2>&1
|
||||
if make static-musl-x86_64 > /dev/null 2>&1; then
|
||||
if [[ -f "build/c_relay_static_musl_x86_64" ]]; then
|
||||
cp build/c_relay_static_musl_x86_64 c-relay-static-x86_64
|
||||
print_success "Static x86_64 binary created: c-relay-static-x86_64"
|
||||
else
|
||||
print_warning "Static x86_64 binary not found after compilation"
|
||||
fi
|
||||
else
|
||||
print_warning "Static x86_64 build failed - MUSL development packages may not be installed"
|
||||
print_status "Run 'sudo apt-get install musl-dev libcap-dev libuv1-dev libev-dev' to enable static builds"
|
||||
fi
|
||||
|
||||
# Try to build static ARM64 version
|
||||
print_status "Attempting static ARM64 build..."
|
||||
make clean > /dev/null 2>&1
|
||||
if make static-musl-arm64 > /dev/null 2>&1; then
|
||||
if [[ -f "build/c_relay_static_musl_arm64" ]]; then
|
||||
cp build/c_relay_static_musl_arm64 c-relay-static-arm64
|
||||
print_success "Static ARM64 binary created: c-relay-static-arm64"
|
||||
else
|
||||
print_warning "Static ARM64 binary not found after compilation"
|
||||
fi
|
||||
else
|
||||
print_warning "Static ARM64 build failed - ARM64 cross-compilation or MUSL ARM64 packages not set up"
|
||||
fi
|
||||
|
||||
# Restore normal build
|
||||
make clean > /dev/null 2>&1
|
||||
make > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to commit and push changes
|
||||
git_commit_and_push() {
|
||||
print_status "Preparing git commit..."
|
||||
|
||||
# Stage all changes
|
||||
if git add . > /dev/null 2>&1; then
|
||||
print_success "Staged all changes"
|
||||
else
|
||||
print_error "Failed to stage changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --staged --quiet; then
|
||||
print_warning "No changes to commit"
|
||||
else
|
||||
# Commit changes
|
||||
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
|
||||
print_success "Committed changes"
|
||||
else
|
||||
print_error "Failed to commit changes"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create new git tag
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists"
|
||||
fi
|
||||
|
||||
# Push changes and tags
|
||||
print_status "Pushing to remote repository..."
|
||||
if git push > /dev/null 2>&1; then
|
||||
print_success "Pushed changes"
|
||||
else
|
||||
print_error "Failed to push changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Push only the new tag to avoid conflicts with existing tags
|
||||
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Pushed tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag push failed, trying force push..."
|
||||
if git push --force origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Force-pushed updated tag: $NEW_VERSION"
|
||||
else
|
||||
print_error "Failed to push tag: $NEW_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to commit and push changes without creating a tag (tag already created)
|
||||
git_commit_and_push_no_tag() {
|
||||
print_status "Preparing git commit..."
|
||||
|
||||
# Stage all changes
|
||||
if git add . > /dev/null 2>&1; then
|
||||
print_success "Staged all changes"
|
||||
else
|
||||
print_error "Failed to stage changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --staged --quiet; then
|
||||
print_warning "No changes to commit"
|
||||
else
|
||||
# Commit changes
|
||||
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
|
||||
print_success "Committed changes"
|
||||
else
|
||||
print_error "Failed to commit changes"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Push changes and tags
|
||||
print_status "Pushing to remote repository..."
|
||||
if git push > /dev/null 2>&1; then
|
||||
print_success "Pushed changes"
|
||||
else
|
||||
print_error "Failed to push changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Push only the new tag to avoid conflicts with existing tags
|
||||
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Pushed tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag push failed, trying force push..."
|
||||
if git push --force origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Force-pushed updated tag: $NEW_VERSION"
|
||||
else
|
||||
print_error "Failed to push tag: $NEW_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create Gitea release
|
||||
create_gitea_release() {
|
||||
print_status "Creating Gitea release..."
|
||||
|
||||
# Check for Gitea token
|
||||
if [[ ! -f "$HOME/.gitea_token" ]]; then
|
||||
print_warning "No ~/.gitea_token found. Skipping release creation."
|
||||
print_warning "Create ~/.gitea_token with your Gitea access token to enable releases."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
|
||||
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/c-relay"
|
||||
|
||||
# Create release
|
||||
print_status "Creating release $NEW_VERSION..."
|
||||
local response=$(curl -s -X POST "$api_url/releases" \
|
||||
-H "Authorization: token $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
|
||||
|
||||
local upload_result=false
|
||||
|
||||
if echo "$response" | grep -q '"id"'; then
|
||||
print_success "Created release $NEW_VERSION"
|
||||
if upload_release_binaries "$api_url" "$token"; then
|
||||
upload_result=true
|
||||
fi
|
||||
elif echo "$response" | grep -q "already exists"; then
|
||||
print_warning "Release $NEW_VERSION already exists"
|
||||
if upload_release_binaries "$api_url" "$token"; then
|
||||
upload_result=true
|
||||
fi
|
||||
else
|
||||
print_error "Failed to create release $NEW_VERSION"
|
||||
print_error "Response: $response"
|
||||
|
||||
# Try to check if the release exists anyway
|
||||
print_status "Checking if release exists..."
|
||||
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
|
||||
if echo "$check_response" | grep -q '"id"'; then
|
||||
print_warning "Release exists but creation response was unexpected"
|
||||
if upload_release_binaries "$api_url" "$token"; then
|
||||
upload_result=true
|
||||
fi
|
||||
else
|
||||
print_error "Release does not exist and creation failed"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Return based on upload success
|
||||
if [[ "$upload_result" == true ]]; then
|
||||
return 0
|
||||
else
|
||||
print_error "Binary upload failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to upload release binaries
|
||||
upload_release_binaries() {
|
||||
local api_url="$1"
|
||||
local token="$2"
|
||||
local upload_success=true
|
||||
|
||||
# Get release ID with more robust parsing
|
||||
print_status "Getting release ID for $NEW_VERSION..."
|
||||
local response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
|
||||
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
||||
|
||||
if [[ -z "$release_id" ]]; then
|
||||
print_error "Could not get release ID for $NEW_VERSION"
|
||||
print_error "API Response: $response"
|
||||
|
||||
# Try to list all releases to debug
|
||||
print_status "Available releases:"
|
||||
curl -s -H "Authorization: token $token" "$api_url/releases" | grep -o '"tag_name":"[^"]*"' | head -5
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_success "Found release ID: $release_id"
|
||||
|
||||
# Upload x86_64 binary
|
||||
if [[ -f "c-relay-x86_64" ]]; then
|
||||
print_status "Uploading x86_64 binary..."
|
||||
local upload_response=$(curl -s -w "\n%{http_code}" -X POST "$api_url/releases/$release_id/assets" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@c-relay-x86_64;filename=c-relay-${NEW_VERSION}-linux-x86_64")
|
||||
|
||||
local http_code=$(echo "$upload_response" | tail -n1)
|
||||
local response_body=$(echo "$upload_response" | head -n -1)
|
||||
|
||||
if [[ "$http_code" == "201" ]]; then
|
||||
print_success "Uploaded x86_64 binary successfully"
|
||||
else
|
||||
print_error "Failed to upload x86_64 binary (HTTP $http_code)"
|
||||
print_error "Response: $response_body"
|
||||
upload_success=false
|
||||
fi
|
||||
else
|
||||
print_warning "x86_64 binary not found: c-relay-x86_64"
|
||||
fi
|
||||
|
||||
# Upload ARM64 binary
|
||||
if [[ -f "c-relay-arm64" ]]; then
|
||||
print_status "Uploading ARM64 binary..."
|
||||
local upload_response=$(curl -s -w "\n%{http_code}" -X POST "$api_url/releases/$release_id/assets" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@c-relay-arm64;filename=c-relay-${NEW_VERSION}-linux-arm64")
|
||||
|
||||
local http_code=$(echo "$upload_response" | tail -n1)
|
||||
local response_body=$(echo "$upload_response" | head -n -1)
|
||||
|
||||
if [[ "$http_code" == "201" ]]; then
|
||||
print_success "Uploaded ARM64 binary successfully"
|
||||
else
|
||||
print_error "Failed to upload ARM64 binary (HTTP $http_code)"
|
||||
print_error "Response: $response_body"
|
||||
upload_success=false
|
||||
fi
|
||||
else
|
||||
print_warning "ARM64 binary not found: c-relay-arm64"
|
||||
fi
|
||||
|
||||
# Upload static x86_64 binary
|
||||
if [[ -f "c-relay-static-x86_64" ]]; then
|
||||
print_status "Uploading static x86_64 binary..."
|
||||
local upload_response=$(curl -s -w "\n%{http_code}" -X POST "$api_url/releases/$release_id/assets" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@c-relay-static-x86_64;filename=c-relay-${NEW_VERSION}-linux-x86_64-static")
|
||||
|
||||
local http_code=$(echo "$upload_response" | tail -n1)
|
||||
local response_body=$(echo "$upload_response" | head -n -1)
|
||||
|
||||
if [[ "$http_code" == "201" ]]; then
|
||||
print_success "Uploaded static x86_64 binary successfully"
|
||||
else
|
||||
print_error "Failed to upload static x86_64 binary (HTTP $http_code)"
|
||||
print_error "Response: $response_body"
|
||||
upload_success=false
|
||||
fi
|
||||
else
|
||||
print_warning "Static x86_64 binary not found: c-relay-static-x86_64"
|
||||
fi
|
||||
|
||||
# Upload static ARM64 binary
|
||||
if [[ -f "c-relay-static-arm64" ]]; then
|
||||
print_status "Uploading static ARM64 binary..."
|
||||
local upload_response=$(curl -s -w "\n%{http_code}" -X POST "$api_url/releases/$release_id/assets" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@c-relay-static-arm64;filename=c-relay-${NEW_VERSION}-linux-arm64-static")
|
||||
|
||||
local http_code=$(echo "$upload_response" | tail -n1)
|
||||
local response_body=$(echo "$upload_response" | head -n -1)
|
||||
|
||||
if [[ "$http_code" == "201" ]]; then
|
||||
print_success "Uploaded static ARM64 binary successfully"
|
||||
else
|
||||
print_error "Failed to upload static ARM64 binary (HTTP $http_code)"
|
||||
print_error "Response: $response_body"
|
||||
upload_success=false
|
||||
fi
|
||||
else
|
||||
print_warning "Static ARM64 binary not found: c-relay-static-arm64"
|
||||
fi
|
||||
|
||||
# Return success/failure status
|
||||
if [[ "$upload_success" == true ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to clean up release binaries
|
||||
cleanup_release_binaries() {
|
||||
local force_cleanup="$1" # Optional parameter to force cleanup even on failure
|
||||
|
||||
if [[ "$force_cleanup" == "force" ]] || [[ "$upload_success" == true ]]; then
|
||||
if [[ -f "c-relay-x86_64" ]]; then
|
||||
rm -f c-relay-x86_64
|
||||
print_status "Cleaned up x86_64 binary"
|
||||
fi
|
||||
if [[ -f "c-relay-arm64" ]]; then
|
||||
rm -f c-relay-arm64
|
||||
print_status "Cleaned up ARM64 binary"
|
||||
fi
|
||||
if [[ -f "c-relay-static-x86_64" ]]; then
|
||||
rm -f c-relay-static-x86_64
|
||||
print_status "Cleaned up static x86_64 binary"
|
||||
fi
|
||||
if [[ -f "c-relay-static-arm64" ]]; then
|
||||
rm -f c-relay-static-arm64
|
||||
print_status "Cleaned up static ARM64 binary"
|
||||
fi
|
||||
else
|
||||
print_warning "Keeping binary files due to upload failures"
|
||||
print_status "Files available for manual upload:"
|
||||
if [[ -f "c-relay-x86_64" ]]; then
|
||||
print_status " - c-relay-x86_64"
|
||||
fi
|
||||
if [[ -f "c-relay-arm64" ]]; then
|
||||
print_status " - c-relay-arm64"
|
||||
fi
|
||||
if [[ -f "c-relay-static-x86_64" ]]; then
|
||||
print_status " - c-relay-static-x86_64"
|
||||
fi
|
||||
if [[ -f "c-relay-static-arm64" ]]; then
|
||||
print_status " - c-relay-static-arm64"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_status "C-Relay Build and Push Script"
|
||||
|
||||
# Check prerequisites
|
||||
check_git_repo
|
||||
|
||||
if [[ "$RELEASE_MODE" == true ]]; then
|
||||
print_status "=== RELEASE MODE ==="
|
||||
|
||||
# Increment minor version for releases
|
||||
increment_version "minor"
|
||||
|
||||
# Create new git tag BEFORE compilation so version.h picks it up
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
|
||||
git tag -d "$NEW_VERSION" > /dev/null 2>&1
|
||||
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Compile project first (will now pick up the new tag)
|
||||
compile_project
|
||||
|
||||
# Build release binaries
|
||||
build_release_binaries
|
||||
|
||||
# Commit and push (but skip tag creation since we already did it)
|
||||
git_commit_and_push_no_tag
|
||||
|
||||
# Create Gitea release with binaries
|
||||
if create_gitea_release; then
|
||||
print_success "Release $NEW_VERSION completed successfully!"
|
||||
print_status "Binaries uploaded to Gitea release"
|
||||
upload_success=true
|
||||
else
|
||||
print_error "Release creation or binary upload failed"
|
||||
upload_success=false
|
||||
fi
|
||||
|
||||
# Cleanup (only if upload was successful)
|
||||
cleanup_release_binaries
|
||||
|
||||
else
|
||||
print_status "=== DEFAULT MODE ==="
|
||||
|
||||
# Increment patch version for regular commits
|
||||
increment_version "patch"
|
||||
|
||||
# Create new git tag BEFORE compilation so version.h picks it up
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
|
||||
git tag -d "$NEW_VERSION" > /dev/null 2>&1
|
||||
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Compile project (will now pick up the new tag)
|
||||
compile_project
|
||||
|
||||
# Commit and push (but skip tag creation since we already did it)
|
||||
git_commit_and_push_no_tag
|
||||
|
||||
print_success "Build and push completed successfully!"
|
||||
print_status "Version $NEW_VERSION pushed to repository"
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute main function
|
||||
main
|
||||
@@ -9,11 +9,21 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
|
||||
|
||||
echo "=========================================="
|
||||
echo "C-Relay MUSL Static Binary Builder"
|
||||
echo "=========================================="
|
||||
# Parse command line arguments
|
||||
DEBUG_BUILD=false
|
||||
if [[ "$1" == "--debug" ]]; then
|
||||
DEBUG_BUILD=true
|
||||
echo "=========================================="
|
||||
echo "C-Relay MUSL Static Binary Builder (DEBUG MODE)"
|
||||
echo "=========================================="
|
||||
else
|
||||
echo "=========================================="
|
||||
echo "C-Relay MUSL Static Binary Builder (PRODUCTION MODE)"
|
||||
echo "=========================================="
|
||||
fi
|
||||
echo "Project directory: $SCRIPT_DIR"
|
||||
echo "Build directory: $BUILD_DIR"
|
||||
echo "Debug build: $DEBUG_BUILD"
|
||||
echo ""
|
||||
|
||||
# Create build directory
|
||||
@@ -83,6 +93,7 @@ echo ""
|
||||
|
||||
$DOCKER_CMD build \
|
||||
--platform "$PLATFORM" \
|
||||
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||
-f "$DOCKERFILE" \
|
||||
-t c-relay-musl-builder:latest \
|
||||
--progress=plain \
|
||||
@@ -105,6 +116,7 @@ echo "=========================================="
|
||||
# Build the builder stage to extract the binary
|
||||
$DOCKER_CMD build \
|
||||
--platform "$PLATFORM" \
|
||||
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||
--target builder \
|
||||
-f "$DOCKERFILE" \
|
||||
-t c-relay-static-builder-stage:latest \
|
||||
@@ -179,11 +191,16 @@ echo "=========================================="
|
||||
echo "Binary: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||
echo "Platform: $PLATFORM"
|
||||
if [ "$DEBUG_BUILD" = true ]; then
|
||||
echo "Build Type: DEBUG (with symbols, no optimization)"
|
||||
else
|
||||
echo "Build Type: PRODUCTION (optimized, stripped)"
|
||||
fi
|
||||
if [ "$TRULY_STATIC" = true ]; then
|
||||
echo "Type: Fully static binary (Alpine MUSL-based)"
|
||||
echo "Linkage: Fully static binary (Alpine MUSL-based)"
|
||||
echo "Portability: Works on ANY Linux distribution"
|
||||
else
|
||||
echo "Type: Static binary (may have minimal dependencies)"
|
||||
echo "Linkage: Static binary (may have minimal dependencies)"
|
||||
fi
|
||||
echo ""
|
||||
echo "✓ Build complete!"
|
||||
|
||||
1
c_utils_lib
Submodule
1
c_utils_lib
Submodule
Submodule c_utils_lib added at 442facd7e3
72
debug.log
Normal file
72
debug.log
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
=== NOSTR WebSocket Debug Log Started ===
|
||||
[14:16:28.243] SEND localhost:8888: ["EVENT", {
|
||||
"pubkey": "193279d1459ba1399aadb954422bf8595aa77367dccf482c682f5f208e435844",
|
||||
"created_at": 1761499411,
|
||||
"kind": 1059,
|
||||
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
|
||||
"content": "AmWNi4P5J126kk69XH2o5mYvGj+69+Fjfr/nZx892I2z8edkwtp2IH7XAnPUqdGPu7x1xiZF1sNfr21VKThOhE54K/uQHLFydZN3acgUfX13sCeWhrvnQD0EvjvZC6QzW9DfFayYoYl+rEPYcra1/N68a+N1R7XnNcf1K/ZFh5Grcnln0H5YdXKRBhQI9aai4iFp1VGy2V0IR+6gDJGbJ7TbAbD3wgGWv1i77C03skH3RgzH+f2b7VBtm+vjKX6q7v6v8j3w1lRFE5Qh0Tqgedh3+UsnwqQta7OCzF9OyAVPK7EqKQBss5LzYSRUpcCE1vw5b7I7yeBFwU9WfnGLUW+uZxMJ2C3P4NBBrVO8UFIkBrPL2cqkoD5c8DgMLJjXGmc4EWfB4ZWb3KjbfLbgi6DVQ++cDjBbnCOPhX+/4qOnWq+gI28e/xk3cBvQtgUOkvWX3oGl3/Q33u4UGtxkFEXGfzdKHVDkR86kqf7RMZjIwTjLGpx4uov0cNmzj07hYEdoG/lJ4yA1v/GyF7viJdnnz3tE0hCZaViqSCev0rfUHWRDDMXJzJ9SS+OwpVswSG4NKvYsDhDM89BjhFs08HshTFdIh2AY45jR/16CsZM9JudH5BwqcX23wToYdZ+lrerOA0EkYb0DJUzGVe4lMpdJZoB8qXLHxMAKwKu0UEWEkeBnnZbvTGwCRbfGorxwPrnyqUCy9tzJx0GOLhRIzBmt6lki607VLDYjK97VIz0dff3fyWPAfy/yBlO2nHhVubUgpPaAjcaYNkO/iZwuP8oJkClWWmKwAQoNoxt+Ly2llrkz+Ne8oXMQdJSq416x6MLHo2JbKH8uwjx0yKG0oldLyWaz3A8OHYkJuxOi7HPVTlOOJrsjG4kMn2g97rVUXLs5v9F/StOjzxtiQWmCBtCsvK2LEEK/DzfavcJstEMxQztJjhiYRO3MJanL7lN2zu1ZHO149FJrgqGV6RQ8DDXf55yuabqHilBuUSDKpI0gl0+Efuor1my+L9J7MjJQ83aSwGizX7uXedMsGQRcvU3++Uvbw7sd2l67fb7IoYU04TPGZkIm120qwf7GAUpnDL7Lhulu/9LFMFs3UnGl9cLzY6EAJtDANHjMAoXbGbYclnoSiNW4yr3X9PBHO5o2YhIxfpTyEgLebJLOkzoziuCTpX8/MdhOhFtlIyo5B8Mbt5GDOHh4x1ZMKOl02J00Vvgui0hLw4Vri8Lz/ErPIRSlrEOB+8K5zPzJy/bD8XrOKlOwSbF5j9dsqs+8uCTC/v9YNQ0cC9wP7gVAxErQ3suJVeV7pzY+eGR051AcW7ppTs1gShhxDDaSaKdMlrkBdFDZcCJ+tomSgW56bOi45erpmk8Lcv4RrBzjBtq1hz+XSaTBAtEnGtHNH2uOn7KP/NNaD38dYkpb3N1VR3zuV67RcuPZeB+5WR9jhnLoSMGox2s=",
|
||||
"id": "c6c18d902744fc0aaa4ca9172b3bcd0dde3fd7d943b41b2a39a16927ede67804",
|
||||
"sig": "d67e0e914aa361c528510efd216548b6734a5fa68c46426571fbc87626bf19a9ec46e16883e7fad700f4fee5cfffd9bba03c3c08e57938fbca77a28b30a32bb7"
|
||||
}]
|
||||
[14:16:28.256] RECV localhost:8888: ["OK", "c6c18d902744fc0aaa4ca9172b3bcd0dde3fd7d943b41b2a39a16927ede67804", true, ""]
|
||||
|
||||
=== NOSTR WebSocket Debug Log Started ===
|
||||
[15:01:18.592] SEND localhost:8888: ["EVENT", {
|
||||
"pubkey": "ec9578ade9e74358ed35d8091d41bfa277e86d649614a8865e3725e38ebe5bc9",
|
||||
"created_at": 1761502101,
|
||||
"kind": 1059,
|
||||
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
|
||||
"content": "AlgLnVwti8Dk2nu0e4bMrXeZiR/u+RPnA85kpts2svaFGfMByS4iap7xqdiSrXpSQPjQsix6jP9Qiy1a6rrvC6MutqTi3JfsMexLR61/ZKTK41sWTXNDTT3keH543vx3fVQH1mq+LgG4mjNzkPe0RqkYFvC8R0nxyAcCecHDxZUlmXQmAGiB5JB2GvstA4eoZLP1OI3fcLA3qaITLNRJOwRUoTYKqUkENHwz74CW0TnYDrKRZVe9zKNWQBLmtsgVoGd5CXNAVgXwmm2h0eCNIcRGnFqDHzpegpEGO+A7tvB0KJwlj4j/GmRgmnWO4pkrM2fmsTdlb5KNqe7NPuTVgYfdvld70zWpenp7jF/0psaEQEl8R7FbG2rNCv8fXtH+womvJQj4S0eUBxfvsUU1wWYmhusEzvyTfpV/nw0Er+pmAUZ2eGk7LEB2GMsJrkT+G5oohm0n+5c72iWJqW9A1eAzjR6Z21FkH4kAEJOl70fw9Xeig+s9rYk3GcKlMvj42zf7DepMXHPy62TbqUeclcm5W/semyasGP521GBuw152IN+dS67OVVmEvEJ89xhwiTeIty78enR4Gq7d1eNK+rqStdtJ7FN6kD/8gv4sFojUXyi0sIxzaSPrwI3ohOqbpEK1dTs6fmTUiyT/Buq++IhD9UwsZgz/kYpZfm1NVnWx+yTEv4I1H80FDxmMzbYnTHuIdRJFeh/NJRy9h+gXoZlZnteHkbwm1w2AejTkVnGs2Pz7aUZgC+1Za8fhtXq3Z9N3R8f8gtVFnnjRzApg5U89QrXUBS0R6F9dTqINk4qti94JWO4dYcPuudCutME5hfCYBoo+LHuRmdPKry7vSK1WgLYQsHuG+r313Ak8DhZYNbL+0d5UJ9kDFlFKaP3xLahSbEc/7u+AyuN68IyM1NwEehllxqVUsX8dsD4bZ2yW5rVjAQm9tT8Ypm73kJEb+DYVqT0WjFx88ee+HX89a9NgszWf1HE1KNQ9gWjn4eH6xbwrOkS4/v2O2tQoAd00vyPKAWly3Zlrz2cRnrSnxTZ5Lt7HtwAt6Err8MhD/w5rMLXHTBCMrroG1VfMo1OgL1YPafKDZmwVcHWacqtZiB0heRx742WipmTonqMjCOTNufdwxQcRPLLio0mtqiIrzgJqqIQenBXSa1jaG6Lvb5PCUKThbg4sSFfgssoUNKM7ytmBAe+PPmOVe11/gGaFWoQeUordbvmiCtzIPUYiKsuhfeK4I3jKvEofQU37hOam8ZxUczXvX6dgOOto002EWyCVfAzFgyey6wI+FGEbhXqlw7nB+azpqLQMJnHg7pfb1stXk3d8rjgVrRsVRJe/5KrXyZ5cd7ftJuJLxpTYmfFu6CKoUE0L5eRxXiwa16Pi0BehxOLaZteiTzttyfj+ClMKs2J/2/T1BVya1oGUW2Wg6ri/qS8oXv8bqiXBZ1/BwfI=",
|
||||
"id": "ea5bb419a8efea8ee86bb8696406a70a0387a7d0ac6e60760026d1aea28b427f",
|
||||
"sig": "0ffde3fd0d83c80693aa656668f2553807f8d474738ff3d9676090a5b8748a8e8e0c75a1d64963e4604046e18a806c4371a9cf2af2fd72f9db50f15bc78a4e25"
|
||||
}]
|
||||
[15:01:18.604] RECV localhost:8888: ["OK", "ea5bb419a8efea8ee86bb8696406a70a0387a7d0ac6e60760026d1aea28b427f", true, ""]
|
||||
|
||||
=== NOSTR WebSocket Debug Log Started ===
|
||||
[07:46:36.863] SEND localhost:8888: ["EVENT", {
|
||||
"pubkey": "99e37bc774d260b464e936ad8945deec62e8f5f8af53e9db662038a717d39bd5",
|
||||
"created_at": 1761562419,
|
||||
"kind": 1059,
|
||||
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
|
||||
"content": "AjYCV8Esqa1L9LQE2G8cDVn+hSXjAJlFVp5nAaC6nuag/1AphKpsAJFGrWZvJ+rte5+4dbmdk+osvlxxfRQHtaZqjaTbVDKJA2b2KTYLgICe7O9rqTqR8oC4sYOVQViEjo30ox3IfDgdR5ONlaprvQ8r71E+oplWOjahUNvf5Yb9OOHbFphOqqWtbYRYqAqvO0bj3rB+tmyUJ8v3mU8NsKJtOTOIvN+jTIwU8cbN6AM6/A87WSi7J7X9/wLpFigBNxrx82MJ025ryApNWyt5PuBia3krPDa51F/A+jFVp1QicVwSt6tP01ktYJn3uyR3qizIiZiXzmxV9+TXopq+mOTlAiwcZBm1ZkS/PgfoUMDUOOVcCAW6ppZlg3oK5jScuDl1d4cZgTmmAPneaHhgB9A/DbWWr03W1vJAXCmDQRUoACfwsvLQf5esXkPcJV+ANgLl8sKd4EPmDAzr946uDcs2BUDftr4++jbdTkg9yIHb4SHnI10osbsqP7BqTrF1TbZHnxev4l5XyaIqhGm6WdQ90uGn1VDSUXXuou3IbqwhZReifYQAL9/5PAeSH6RrrL4neEzBjosSNkcMmtAxqBfd8dOHfqT6r15osKXc1eSmO9qDjZxUHUb5zIJjrkDW0jY8vAfiMqZhKd5Vl7stYf2iJJxdm04r4zpxGBjlYQva2LyrPclBIGxax6sTQTxoRyUBwyis7OnxUC0HO7bqr464RzCX1/OHMOYhFQu3BY8Rvytl9E1hS+rJpWgNkHsbjr5zs1l/B+qwt+zqnYlAtjZZ2T56Pjd+jZcRTt/NKDtyQnLPxreBWgNy8IykNI0q2fgFiWJwh7GnlbYrx5zco04Ory/P+nW2/Xosp0232I48e2KhxtH6L1e6dOWqbZXFQXBqzKNlVTRkPyS9ykSSs7NAVknRz/vF86+jJVJa2z32Y4oQtJna8vK5J5HA3rRSlSEINwmcSiFUmuxeFAcFjYjyVGlBhmH3B/98CtT2+JHgUYpMiG51+HR+OI9qBGgsF5SI9JKai7CFC3OqfaW1rZHN96uta4VVGQ1mJetz/xB3W+QThsZ0IJ6/wBnbUpPBoab4rfnYeeVwOMxiK5B2UIZ1+ihRrSMsjMC8DAEbUAn9XNABJHhDo0KJcYqtpHBIkQgbqfuSKTLmc4mZNJCp8wmry9Tc9ZQo2jT0dZa/NZO+qtqWXWqZWbMngXFer4AtR+Vethhg6BdhYOYI/j8gOW1m8qodBlj9BHiKEU3Ig9z6WawsOD95VosxhqrQDuyO07igXNWMNK5exRfvp2QiHgILuC9diZZGBXPRLIDlKERTotPc5IdutkTG6qVh6+r6wbwtVhiWJVmfVy/D0hvDvlaqzVk3FRVuRuMZI+LmF3OdNGIf0+lfMUeMAABhDNTWyyS8gG21JJZQOBxGc12x49xWvMLbXaPCKBKrqw4FLF4PTCc=",
|
||||
"id": "9899324517c0e1796ea513cfc9fa0a2592cf5532774abc7e2a1bac7bb16c4fbb",
|
||||
"sig": "0d73ac599d0d6d99dd9afa0c92d741e459bc53102557acba5d868089776bb36a521ae800303ce5ceceabc8d643116a74560744243b3a1c7749d6a52117343637"
|
||||
}]
|
||||
[07:46:36.876] RECV localhost:8888: ["OK", "9899324517c0e1796ea513cfc9fa0a2592cf5532774abc7e2a1bac7bb16c4fbb", true, ""]
|
||||
|
||||
=== NOSTR WebSocket Debug Log Started ===
|
||||
[07:46:57.426] SEND localhost:8888: ["EVENT", {
|
||||
"pubkey": "a1efe929139f3f195159389a6eb7199c127c88e32a0264cd826e95806a7c7db3",
|
||||
"created_at": 1761562440,
|
||||
"kind": 1059,
|
||||
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
|
||||
"content": "ArP2HEobkU/QYXy2R94zSkKM1OfQT5SabPeebj9dQVGbUKaKDwuN2RUYTRJ5rD8euyiXat8YzYO7PJ0CHzxclXxO8AWpdN4P76srm3zJ5z6kpQpcCFgInV7k4v6LZmmCrrtdTWqLjuLTPJJd6W9J7HqbTA3Dt4200BSpA4Por1TQAncplwH7O4vBfbPYtdv9w1RL1uSInMWcGwxttTXlyTtAJ0G0hQNofowFCMWuQCKjV5LxPfoXdOCrsp/We6x+hYKDxphsDjQ1tbdtYFmj/YRy6MJm1id4mr+i8fEnyimshE/fAhavOXUg6239MmYj8nR2RT9LMuhVckX+V5MZnVyC3mZlfzkPJiTHxiDkEREjljNOX9I+9yChg6MvyU41s7GjBlPyWiyeXedPcU2Q8ypGsFLhBl5i+IGSn6wCcBH8+h3euG8jtBgKxIP1qBYsXPTlYpSXQcisIZlW2Rubcawf/RF7HYbIRu884mpdVnURcHqfN9yquoVyfvIgQR5Fs6IbKatJ64LfMLkLNs4UIlumtQRdW3NFjglgb/rF9btKYVFHRG9dWwOpZBd0zvXtWKbts2AKQFU30/WegaPh2LT5rN9HfMsA1tI9YwZm//T2NiLaPwJCuOFWBOUiB7jIObQKtOHrICI/jXIGOAfgox5+fcAE6CaysHHzluVcwiw7GioShidaIDsZ10rWJOv1HeRpuiAJJTWk2FOBJzpOxli6s6jGj2S481Xa99I13TihgL0wAPhjsnQhz0kh40g89mipzVO5hbki101zIJCEBrDeT4Ptabc9GminXedq9k1G98usM0JSHsgtdZdztme/UyvYyAKMdez1yNgOp7YgOU15Rpz/KGL6W5Wk3MbUwpuVRzUWEMoBcyMzssn5Sa3mkh1RQqpTcoQaktTNwkhR1R5bgedka61JmcK4Uq3Hi/HfKYHbeUeta6Olu+U19PEwZia1iq+y0ZQm5gMwCK8BsoV44OLsjeDKlyRGCtIjkTc/L2LyuAZFhw560vKflkigQVcajaQVtEDgaT5odgFwvYEMOjbBDloDs589hAn8ZLyRJo3tIXNwqhctKTSqbit5qs85pOHkXSC3gsRQvDfq4qVh8iWXFotmOHlBEh4OZk89xwAnP0wiv5kd8N2c2CTB84SB224GinMhs0gkaCIXPPYv8IfVcow9+3sjnNov4dRRIB80fRXP9X3IyR7tXYCuq1uQO2iWiWKhNaqJRoTM1BUhLv0ebKYjfPevSVHUuV51CcsoFakNT8S0UnW7QHfmsESvCJLLT8ttrJqpRX2tf6SpzofHmzQHVrHFn8C7WKMVelndptmaOt/9Lek2UrZiKmzRP0CtBL+HoPRmZHF9t7y0qEhoApkrB9FPukH/IGV6jx891rH4nC1fLKc6zgkdjnYB7HDB+lWp2JKpV8Z3CbZXtR28kwIvZZIABZ23/U5cFds=",
|
||||
"id": "c8cdf8992fbc17a0ccb74f6dcb7b851f3fdd53317f5a5ea4e202a91b22e15ac6",
|
||||
"sig": "b9efba3448d67de8855838044427396af1958269642a975129fe877e48e5c0e0818d638264f8aa80404886559a7d29464339f63704044dbf11ff09eb0bdeda2b"
|
||||
}]
|
||||
[07:46:57.439] RECV localhost:8888: ["OK", "c8cdf8992fbc17a0ccb74f6dcb7b851f3fdd53317f5a5ea4e202a91b22e15ac6", true, ""]
|
||||
|
||||
=== NOSTR WebSocket Debug Log Started ===
|
||||
[07:48:51.631] SEND localhost:8888: ["EVENT", {
|
||||
"pubkey": "52feea8d0da247ed1537c88e12b2f6bc88697b69abe33bf4f059f9f10c0f2b43",
|
||||
"created_at": 1761562554,
|
||||
"kind": 1059,
|
||||
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
|
||||
"content": "AoU024VIO6IgceC43yYPvKxOb5PuuZQRAUQLC6Crdn5dtIVuHE8M/UUmmNXmWq3jB6kFbFWxNgFWuxCEG9sQHDEngk+tDOGlt+r0vx3jUZG09lzNzcghl/4l/Do48rcy0cfSm+mSHrJsy7N+MSAXQ1heKahKF2fSyfYFM+6EOSEl0sJSq09iWGFft0lWfeZ3AFpji0gp0Z5QY1hQPH3Te/TRuDCoR8GXG3NguLD22Ed8byOQzf/b7nWr70z2Sqg15zhMwyqkl//dOp9iIXE69FONqDfvFF0xttQ1L9PbzQizMt65CqbMuxGMiMA1zZsgQ8iN+xbIN4xv4DrzCtBZnYt3aJSt7cv+8Co5OmGNXu1RNUvxpZZTO8Dq/m08Y5JDYiRCvdh8kTASMVt0MfGsvWgmcHiiCINQUe7n5ynieayFpbl9j1Vtml0lmrfIOYnDQYmuqDDyG7PxhRt3G/SpiabWBwsqmTqCvrclXfTm4t5YYSr/5lKbwHzPk4qtdEs+LqH2/zd4egZnT4Xt/vIP/c55NrEWPmr50G37DpsSVbDxMQXs4dpldDntjEDFuL8VTkAzqibmiZSQnb6l+DpKYNdQCyg1S1ttnzYp2cRTAPzAVbRMqk1R6jagWnZyOs/JIK4on51JHczaUTCMypDLJFoOaFPTAedHfR/Hn2Nm05W8oQ/m/RGmxLgok6WgH3KJ8wvN+8X+XYpTgyxej/hYPqJnq3uaNqMbcD5katWRBmZtyZa3Cn2nZDqmFFJSABWacXNCyHL41Z+MhyYalYzvUev1ozUgx2NEWwxnSTvMkpOfvSDrs5Ncosle0itL9j0QVBrKFjHgq2BJ4FApv/Iq0af+8JEqhVEsMNpGwRJst1kn7kO+Q7O68PQF6PPlNqh7DNea0Bz1sN1QDt2yZTApi2b3IJTsbekye/WsOB0J+bLvxNQ/UoULcyq3SLRSqQEQMLBPz7JrijMzBdPglWpeZ58UDrbmd1KhnHzx7o1NvHyjPRuKj4M094+2/mTEFGOOIF+Ogjqj+wCSDnT5C0d/l2llQkIXCKcLONWT4bKkmTOjvNs6lX+VpBynaegGjzGOvJw1beRZIkRegTpV4pnMZH9833s175rcMcDjPnfT9FD+pDv1DkmXfww1k6MgfHTbwjSgj0K9862xFNwL3mC2g8XFNlcflC0Rd8PzXRg7TBn7855r+urqujCUqZrzdUMHvp08rEEzSJKljk4XN4DqZeWn2evv7UCbYjq46sVf2lEHCvHdqKoPf5ENU72y5Fb9tQJAyUoBTdPdb2SZ2Y2jSF+6+H2wVXrOlm8EwBquaREl25fs7Yqwjru7qz1rO9EA1jlNybFvALHEFQzHEpi8JeNi5T/mI+VleoUDrk/og2mucQVFqAQzRjASsaeDq9fZNqhv3Q3DBIpftmI4g6ZXqJhPRK8wF7Ym3mmC7eFHwalUprA=",
|
||||
"id": "9bc4b5ad293085272bf52ff17abb585f7e63bc155a5a39cfe1a5c046f141e571",
|
||||
"sig": "ee6b917761031a06bc50da0173aef881a61213473d4f533a8a4a96247edcdbd17dbf87919c4d92f8ea8719d5311d51a8028fbf62e3f40f9b8004ccbe9f3adabd"
|
||||
}]
|
||||
[07:49:01.659] RECV localhost:8888: ["OK", "9bc4b5ad293085272bf52ff17abb585f7e63bc155a5a39cfe1a5c046f141e571", false, "error: failed to store gift wrap event"]
|
||||
|
||||
=== NOSTR WebSocket Debug Log Started ===
|
||||
[07:50:47.319] SEND localhost:8888: ["EVENT", {
|
||||
"pubkey": "f206ef335cc3b360cf739680cd4540b852fb9d75aac552b58014a41cfc4c6c65",
|
||||
"created_at": 1761562670,
|
||||
"kind": 1059,
|
||||
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
|
||||
"content": "AuzXkvYpH0IX/T0BKtOAjO5QtglT3nXGXF20awgDX6T1qoWV8qykYY01qPlGLSDkOuOvhG5NZuFPs/hPMnctmnskvHTHqKJdUeT10Qe0JmZiP5y6fZSlrtLMKfyoLpNYFDXOfwooSD2Q0UN8ePfTkkB61ri7avsq8w1WjoVTUSG5kouJfQAgvh75uXNkcvTNWSX5gbCXxoL1D7twPSUXvuZBJTN0iVdh3jg8X1uZWhvpZJIIXceZIUdxaIp2EmrYVW0VEZZSbAFGAldtKasHrP3cJKwTk1IenMFXaPkJnsbvyzUZWKBwTCeBLBhMNzbBWOp5A7SgFW1vf9gm00MSQE/JjwOzDZIaIQ3vRCMbO29XLlcOCevs8FwusZ86LuXZ8EQac9vnc/7gEul/SkOQaSq7v4oGzwfY4iQ8c8VdX/+syE+ClUEjiKdN3/lcRGWhdMGYgK/uajLd9jpfY8CBzP0BZM0Oq6ZgSJt4ydntPfiUq4PtDww+56+bUUQb5V3eZ9SUnQgebO1doRgtvb6LiLGN3D9XolEiHBE3KDt/InfWPUeuf1HEf5IbDc2w7zhgzWbvXK9G4NnsAmOHQNIGbPHXLWEOOhRtcEnrHILKYgs4wfuvSnfSUzfWxHVlhXkuXj4pq/EmJKmQg3zB1C2QzKMx7O/oHplnQFGUfAMDQY9GpBWLCOhQH3ZiHWQ02AjXSze7PGB9ac7KoPmyKafUAgXbASp/G9t7n3pxarzwBNm4zQ6wPRpR5OF+mFYQ9ClJ+3MlUDNq1T8fGuJVIduxFgyWMAgKoJBQe+xP5qHEuCxhG1B2KtoKHbzAtXpV+sduXQOlm0Jq7req59VgtgrIVVoLNyn7ulUFmJvWbaPWyuMrC1z+MdjNw/oJ1mE4+zYSB4Yho7DCwVdIxWrkFSx1yH9s1WPPyERCy4/UQfHpPmAD/JyAnnkKsc7v3MpAWAYCsiil1/PgFFPRRrO7jS+Ez+veQl5tx376ac9MwaN+ZbFADqYaf8CCcWXhMlAYl/zcMWLXKqL/wKb6orpTTHiWU/iJIvbuT0MIN68LIX+G/S5QCIcAQez+G35n5pDUkKikVQguKcJG51iDZqRAc+fnjSa7ifu8HBJ8HIKZjHEEyp6oGU0LCEWH60iIa7toKAwRx8rLPP2tWo+5u41nUrhpUXhUquQu8Dr+LrNdB30qYlH123R0NBBtXG7ngW8WDv2GQcul33ftiI/14QofOthA8SiExW5B7OsWJQON8sS1ZTc5l/M6f5B17CwqmAGd4NdKPQy1SZWGD61jkefwzKW/w4fZFXfploGwuYvFI/G8/YnaJ60p/k+2Aftcst9ikAHZF4xBtuJr4IrT6/f+snv12G4EdowmaSMjXRZv30d4yKwFmwiuoDHWLyYVwBkO+UO3r0WEe1DId0Z1FZnXfgdnM+zAZwITtCVQjZMcsOSNskKd1eE=",
|
||||
"id": "ce28dc9c653a4f5451266bc215942be9a54e4777a27862fddce351a59cc2dbf3",
|
||||
"sig": "539f314c0f0fd685647da358c4153272baf671f1a1bc42b8ff61231c4b5f1f03cb8d15a36fb78437dbf094c546e9ffe8e03de7ddb3b62a981c135a714ec57f93"
|
||||
}]
|
||||
[07:50:47.325] RECV localhost:8888: ["OK", "ce28dc9c653a4f5451266bc215942be9a54e4777a27862fddce351a59cc2dbf3", true, ""]
|
||||
@@ -1,3 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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/
|
||||
|
||||
# Reload systemd daemon to pick up the new service
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Enable the service (if not already enabled)
|
||||
sudo systemctl enable c-relay-local.service
|
||||
|
||||
# Restart the service
|
||||
sudo systemctl restart c-relay-local.service
|
||||
|
||||
# Show service status
|
||||
sudo systemctl status c-relay-local.service --no-pager -l
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# C-Relay Static Binary Deployment Script
|
||||
# Deploys build/c_relay_static_x86_64 to server via sshlt
|
||||
# Deploys build/c_relay_static_x86_64 to server via ssh
|
||||
|
||||
set -e
|
||||
|
||||
@@ -21,7 +21,8 @@ ssh ubuntu@laantungir.com "sudo mv '/tmp/c_relay.tmp' '$REMOTE_BINARY_PATH'"
|
||||
ssh ubuntu@laantungir.com "sudo chown c-relay:c-relay '$REMOTE_BINARY_PATH'"
|
||||
ssh ubuntu@laantungir.com "sudo chmod +x '$REMOTE_BINARY_PATH'"
|
||||
|
||||
# Restart service
|
||||
# Reload systemd and restart service
|
||||
ssh ubuntu@laantungir.com "sudo systemctl daemon-reload"
|
||||
ssh ubuntu@laantungir.com "sudo systemctl restart '$SERVICE_NAME'"
|
||||
|
||||
echo "Deployment complete!"
|
||||
457
docs/c_utils_lib_architecture.md
Normal file
457
docs/c_utils_lib_architecture.md
Normal file
@@ -0,0 +1,457 @@
|
||||
# c_utils_lib Architecture Plan
|
||||
|
||||
## Overview
|
||||
|
||||
`c_utils_lib` is a standalone C utility library designed to provide reusable, general-purpose functions for C projects. It serves as a learning repository and a practical toolkit for common C programming tasks.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
1. **Zero External Dependencies**: Only standard C library dependencies
|
||||
2. **Modular Design**: Each utility is independent and can be used separately
|
||||
3. **Learning-Oriented**: Well-documented code suitable for learning C
|
||||
4. **Production-Ready**: Battle-tested utilities from real projects
|
||||
5. **Cross-Platform**: Works on Linux, macOS, and other POSIX systems
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
c_utils_lib/
|
||||
├── README.md # Main documentation
|
||||
├── LICENSE # MIT License
|
||||
├── VERSION # Current version (e.g., v0.1.0)
|
||||
├── build.sh # Build script
|
||||
├── Makefile # Build system
|
||||
├── .gitignore # Git ignore rules
|
||||
│
|
||||
├── include/ # Public headers
|
||||
│ ├── c_utils.h # Main header (includes all utilities)
|
||||
│ ├── debug.h # Debug/logging system
|
||||
│ ├── version.h # Version utilities
|
||||
│ ├── string_utils.h # String utilities (future)
|
||||
│ └── memory_utils.h # Memory utilities (future)
|
||||
│
|
||||
├── src/ # Implementation files
|
||||
│ ├── debug.c # Debug system implementation
|
||||
│ ├── version.c # Version utilities implementation
|
||||
│ ├── string_utils.c # String utilities (future)
|
||||
│ └── memory_utils.c # Memory utilities (future)
|
||||
│
|
||||
├── examples/ # Usage examples
|
||||
│ ├── debug_example.c # Debug system example
|
||||
│ ├── version_example.c # Version utilities example
|
||||
│ └── Makefile # Examples build system
|
||||
│
|
||||
├── tests/ # Unit tests
|
||||
│ ├── test_debug.c # Debug system tests
|
||||
│ ├── test_version.c # Version utilities tests
|
||||
│ ├── run_tests.sh # Test runner
|
||||
│ └── Makefile # Tests build system
|
||||
│
|
||||
└── docs/ # Additional documentation
|
||||
├── API.md # Complete API reference
|
||||
├── INTEGRATION.md # How to integrate into projects
|
||||
├── VERSIONING.md # Versioning system guide
|
||||
└── CONTRIBUTING.md # Contribution guidelines
|
||||
```
|
||||
|
||||
## Initial Utilities (v0.1.0)
|
||||
|
||||
### 1. Debug System (`debug.h`, `debug.c`)
|
||||
|
||||
**Purpose**: Unified logging and debugging system with configurable verbosity levels.
|
||||
|
||||
**Features**:
|
||||
- 5 debug levels: NONE, ERROR, WARN, INFO, DEBUG, TRACE
|
||||
- Timestamp formatting
|
||||
- File/line information at TRACE level
|
||||
- Macro-based API for zero-cost when disabled
|
||||
- Thread-safe (future enhancement)
|
||||
|
||||
**API**:
|
||||
```c
|
||||
// Initialization
|
||||
void debug_init(int level);
|
||||
|
||||
// Logging macros
|
||||
DEBUG_ERROR(format, ...);
|
||||
DEBUG_WARN(format, ...);
|
||||
DEBUG_INFO(format, ...);
|
||||
DEBUG_LOG(format, ...);
|
||||
DEBUG_TRACE(format, ...);
|
||||
|
||||
// Global debug level
|
||||
extern debug_level_t g_debug_level;
|
||||
```
|
||||
|
||||
**Usage Example**:
|
||||
```c
|
||||
#include <c_utils/debug.h>
|
||||
|
||||
int main() {
|
||||
debug_init(DEBUG_LEVEL_INFO);
|
||||
DEBUG_INFO("Application started");
|
||||
DEBUG_ERROR("Critical error: %s", error_msg);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Version Utilities (`version.h`, `version.c`)
|
||||
|
||||
**Purpose**: Reusable versioning system for C projects using git tags.
|
||||
|
||||
**Features**:
|
||||
- Automatic version extraction from git tags
|
||||
- Semantic versioning support (MAJOR.MINOR.PATCH)
|
||||
- Version comparison functions
|
||||
- Header file generation for embedding version info
|
||||
- Build number tracking
|
||||
|
||||
**API**:
|
||||
```c
|
||||
// Version structure
|
||||
typedef struct {
|
||||
int major;
|
||||
int minor;
|
||||
int patch;
|
||||
char* git_hash;
|
||||
char* build_date;
|
||||
} version_info_t;
|
||||
|
||||
// Get version from git
|
||||
int version_get_from_git(version_info_t* version);
|
||||
|
||||
// Generate version header file
|
||||
int version_generate_header(const char* output_path, const char* prefix);
|
||||
|
||||
// Compare versions
|
||||
int version_compare(version_info_t* v1, version_info_t* v2);
|
||||
|
||||
// Format version string
|
||||
char* version_to_string(version_info_t* version);
|
||||
```
|
||||
|
||||
**Usage Example**:
|
||||
```c
|
||||
#include <c_utils/version.h>
|
||||
|
||||
// In your build system:
|
||||
version_generate_header("src/version.h", "MY_APP");
|
||||
|
||||
// In your code:
|
||||
#include "version.h"
|
||||
printf("Version: %s\n", MY_APP_VERSION);
|
||||
```
|
||||
|
||||
**Integration with Projects**:
|
||||
```bash
|
||||
# In project Makefile
|
||||
version.h:
|
||||
c_utils_lib/bin/generate_version src/version.h MY_PROJECT
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
### Static Library Output
|
||||
|
||||
```
|
||||
libc_utils.a # Static library for linking
|
||||
```
|
||||
|
||||
### Build Targets
|
||||
|
||||
```bash
|
||||
make # Build static library
|
||||
make examples # Build examples
|
||||
make test # Run tests
|
||||
make install # Install to system (optional)
|
||||
make clean # Clean build artifacts
|
||||
```
|
||||
|
||||
### Build Script (`build.sh`)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Simplified build script similar to nostr_core_lib
|
||||
|
||||
case "$1" in
|
||||
lib|"")
|
||||
make
|
||||
;;
|
||||
examples)
|
||||
make examples
|
||||
;;
|
||||
test)
|
||||
make test
|
||||
;;
|
||||
clean)
|
||||
make clean
|
||||
;;
|
||||
install)
|
||||
make install
|
||||
;;
|
||||
*)
|
||||
echo "Usage: ./build.sh [lib|examples|test|clean|install]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
## Versioning System Design
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Git Tags as Source of Truth**
|
||||
- Version tags: `v0.1.0`, `v0.2.0`, etc.
|
||||
- Follows semantic versioning
|
||||
|
||||
2. **Automatic Header Generation**
|
||||
- Script reads git tags
|
||||
- Generates header with version macros
|
||||
- Includes build date and git hash
|
||||
|
||||
3. **Reusable Across Projects**
|
||||
- Each project calls `version_generate_header()`
|
||||
- Customizable prefix (e.g., `C_RELAY_VERSION`, `NOSTR_CORE_VERSION`)
|
||||
- No hardcoded version numbers in source
|
||||
|
||||
### Example Generated Header
|
||||
|
||||
```c
|
||||
// Auto-generated by c_utils_lib version system
|
||||
#ifndef MY_PROJECT_VERSION_H
|
||||
#define MY_PROJECT_VERSION_H
|
||||
|
||||
#define MY_PROJECT_VERSION "v0.1.0"
|
||||
#define MY_PROJECT_VERSION_MAJOR 0
|
||||
#define MY_PROJECT_VERSION_MINOR 1
|
||||
#define MY_PROJECT_VERSION_PATCH 0
|
||||
#define MY_PROJECT_GIT_HASH "a1b2c3d"
|
||||
#define MY_PROJECT_BUILD_DATE "2025-10-15"
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### Integration Pattern
|
||||
|
||||
```makefile
|
||||
# In consuming project's Makefile
|
||||
VERSION_SCRIPT = c_utils_lib/bin/generate_version
|
||||
|
||||
src/version.h: .git/refs/tags/*
|
||||
$(VERSION_SCRIPT) src/version.h MY_PROJECT
|
||||
|
||||
my_app: src/version.h src/main.c
|
||||
$(CC) src/main.c -o my_app -Ic_utils_lib/include -Lc_utils_lib -lc_utils
|
||||
```
|
||||
|
||||
## Future Utilities (Roadmap)
|
||||
|
||||
### String Utilities (`string_utils.h`)
|
||||
- Safe string operations (bounds checking)
|
||||
- String trimming, splitting, joining
|
||||
- Case conversion
|
||||
- Pattern matching helpers
|
||||
|
||||
### Memory Utilities (`memory_utils.h`)
|
||||
- Safe allocation wrappers
|
||||
- Memory pool management
|
||||
- Leak detection helpers (debug builds)
|
||||
- Arena allocators
|
||||
|
||||
### Configuration Utilities (`config_utils.h`)
|
||||
- INI file parsing
|
||||
- JSON configuration (using cJSON)
|
||||
- Environment variable helpers
|
||||
- Command-line argument parsing
|
||||
|
||||
### File Utilities (`file_utils.h`)
|
||||
- Safe file operations
|
||||
- Directory traversal
|
||||
- Path manipulation
|
||||
- File watching (inotify wrapper)
|
||||
|
||||
### Time Utilities (`time_utils.h`)
|
||||
- Timestamp formatting
|
||||
- Duration calculations
|
||||
- Timer utilities
|
||||
- Rate limiting helpers
|
||||
|
||||
## Integration Guide
|
||||
|
||||
### As Git Submodule
|
||||
|
||||
```bash
|
||||
# In your project
|
||||
git submodule add https://github.com/yourusername/c_utils_lib.git
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Build the library
|
||||
cd c_utils_lib && ./build.sh lib && cd ..
|
||||
|
||||
# Update your Makefile
|
||||
INCLUDES += -Ic_utils_lib/include
|
||||
LIBS += -Lc_utils_lib -lc_utils
|
||||
```
|
||||
|
||||
### In Your Makefile
|
||||
|
||||
```makefile
|
||||
# Check if c_utils_lib is built
|
||||
c_utils_lib/libc_utils.a:
|
||||
cd c_utils_lib && ./build.sh lib
|
||||
|
||||
# Link against it
|
||||
my_app: c_utils_lib/libc_utils.a src/main.c
|
||||
$(CC) src/main.c -o my_app \
|
||||
-Ic_utils_lib/include \
|
||||
-Lc_utils_lib -lc_utils
|
||||
```
|
||||
|
||||
### In Your Code
|
||||
|
||||
```c
|
||||
// Option 1: Include everything
|
||||
#include <c_utils/c_utils.h>
|
||||
|
||||
// Option 2: Include specific utilities
|
||||
#include <c_utils/debug.h>
|
||||
#include <c_utils/version.h>
|
||||
|
||||
int main() {
|
||||
debug_init(DEBUG_LEVEL_INFO);
|
||||
DEBUG_INFO("Starting application version %s", MY_APP_VERSION);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Plan for c-relay
|
||||
|
||||
### Phase 1: Extract Debug System
|
||||
1. Create `c_utils_lib` repository
|
||||
2. Move [`debug.c`](../src/debug.c) and [`debug.h`](../src/debug.h)
|
||||
3. Create build system
|
||||
4. Add basic tests
|
||||
|
||||
### Phase 2: Add Versioning System
|
||||
1. Extract version generation logic from c-relay
|
||||
2. Create reusable version utilities
|
||||
3. Update c-relay to use new system
|
||||
4. Update nostr_core_lib to use new system
|
||||
|
||||
### Phase 3: Add as Submodule
|
||||
1. Add `c_utils_lib` as submodule to c-relay
|
||||
2. Update c-relay Makefile
|
||||
3. Update includes in c-relay source files
|
||||
4. Remove old debug files from c-relay
|
||||
|
||||
### Phase 4: Documentation & Examples
|
||||
1. Create comprehensive README
|
||||
2. Add usage examples
|
||||
3. Write integration guide
|
||||
4. Document API
|
||||
|
||||
## Benefits
|
||||
|
||||
### For c-relay
|
||||
- Cleaner separation of concerns
|
||||
- Reusable utilities across projects
|
||||
- Easier to maintain and test
|
||||
- Consistent logging across codebase
|
||||
|
||||
### For Learning C
|
||||
- Real-world utility implementations
|
||||
- Best practices examples
|
||||
- Modular design patterns
|
||||
- Build system examples
|
||||
|
||||
### For Future Projects
|
||||
- Drop-in utility library
|
||||
- Proven, tested code
|
||||
- Consistent patterns
|
||||
- Time savings
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Test each utility independently
|
||||
- Mock external dependencies
|
||||
- Edge case coverage
|
||||
- Memory leak detection (valgrind)
|
||||
|
||||
### Integration Tests
|
||||
- Test with real projects (c-relay, nostr_core_lib)
|
||||
- Cross-platform testing
|
||||
- Performance benchmarks
|
||||
|
||||
### Continuous Integration
|
||||
- GitHub Actions for automated testing
|
||||
- Multiple compiler versions (gcc, clang)
|
||||
- Multiple platforms (Linux, macOS)
|
||||
- Static analysis (cppcheck, clang-tidy)
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Code Documentation
|
||||
- Doxygen-style comments
|
||||
- Function purpose and parameters
|
||||
- Return value descriptions
|
||||
- Usage examples in comments
|
||||
|
||||
### API Documentation
|
||||
- Complete API reference in `docs/API.md`
|
||||
- Usage examples for each function
|
||||
- Common patterns and best practices
|
||||
- Migration guides
|
||||
|
||||
### Learning Resources
|
||||
- Detailed explanations of implementations
|
||||
- Links to relevant C standards
|
||||
- Common pitfalls and how to avoid them
|
||||
- Performance considerations
|
||||
|
||||
## License
|
||||
|
||||
MIT License - permissive and suitable for learning and commercial use.
|
||||
|
||||
## Version History
|
||||
|
||||
- **v0.1.0** (Planned)
|
||||
- Initial release
|
||||
- Debug system
|
||||
- Version utilities
|
||||
- Basic documentation
|
||||
|
||||
- **v0.2.0** (Future)
|
||||
- String utilities
|
||||
- Memory utilities
|
||||
- Enhanced documentation
|
||||
|
||||
- **v0.3.0** (Future)
|
||||
- Configuration utilities
|
||||
- File utilities
|
||||
- Time utilities
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. ✅ Successfully integrated into c-relay
|
||||
2. ✅ Successfully integrated into nostr_core_lib
|
||||
3. ✅ All tests passing
|
||||
4. ✅ Documentation complete
|
||||
5. ✅ Examples working
|
||||
6. ✅ Zero external dependencies (except standard library)
|
||||
7. ✅ Cross-platform compatibility verified
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create repository structure
|
||||
2. Implement debug system
|
||||
3. Implement version utilities
|
||||
4. Create build system
|
||||
5. Write tests
|
||||
6. Create documentation
|
||||
7. Integrate into c-relay
|
||||
8. Publish to GitHub
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is a living document. Update as the library evolves and new utilities are added.
|
||||
621
docs/c_utils_lib_implementation_plan.md
Normal file
621
docs/c_utils_lib_implementation_plan.md
Normal file
@@ -0,0 +1,621 @@
|
||||
# c_utils_lib Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a step-by-step implementation plan for creating the `c_utils_lib` library and integrating it into the c-relay project.
|
||||
|
||||
## Phase 1: Repository Setup & Structure
|
||||
|
||||
### Step 1.1: Create Repository Structure
|
||||
|
||||
**Location**: Create outside c-relay project (sibling directory)
|
||||
|
||||
```bash
|
||||
# Create directory structure
|
||||
mkdir -p c_utils_lib/{include,src,examples,tests,docs,bin}
|
||||
cd c_utils_lib
|
||||
|
||||
# Create subdirectories
|
||||
mkdir -p include/c_utils
|
||||
mkdir -p tests/results
|
||||
```
|
||||
|
||||
### Step 1.2: Initialize Git Repository
|
||||
|
||||
```bash
|
||||
cd c_utils_lib
|
||||
git init
|
||||
git branch -M main
|
||||
```
|
||||
|
||||
### Step 1.3: Create Core Files
|
||||
|
||||
**Files to create**:
|
||||
1. `README.md` - Main documentation
|
||||
2. `LICENSE` - MIT License
|
||||
3. `VERSION` - Version file (v0.1.0)
|
||||
4. `.gitignore` - Git ignore rules
|
||||
5. `Makefile` - Build system
|
||||
6. `build.sh` - Build script
|
||||
|
||||
## Phase 2: Debug System Implementation
|
||||
|
||||
### Step 2.1: Move Debug Files
|
||||
|
||||
**Source files** (from c-relay):
|
||||
- `src/debug.c` → `c_utils_lib/src/debug.c`
|
||||
- `src/debug.h` → `c_utils_lib/include/c_utils/debug.h`
|
||||
|
||||
**Modifications needed**:
|
||||
1. Update header guard in `debug.h`:
|
||||
```c
|
||||
#ifndef C_UTILS_DEBUG_H
|
||||
#define C_UTILS_DEBUG_H
|
||||
```
|
||||
|
||||
2. No namespace changes needed (keep simple API)
|
||||
|
||||
3. Add header documentation:
|
||||
```c
|
||||
/**
|
||||
* @file debug.h
|
||||
* @brief Debug and logging system with configurable verbosity levels
|
||||
*
|
||||
* Provides a simple, efficient logging system with 5 levels:
|
||||
* - ERROR: Critical errors
|
||||
* - WARN: Warnings
|
||||
* - INFO: Informational messages
|
||||
* - DEBUG: Debug messages
|
||||
* - TRACE: Detailed trace with file:line info
|
||||
*/
|
||||
```
|
||||
|
||||
### Step 2.2: Create Main Header
|
||||
|
||||
**File**: `include/c_utils/c_utils.h`
|
||||
|
||||
```c
|
||||
#ifndef C_UTILS_H
|
||||
#define C_UTILS_H
|
||||
|
||||
/**
|
||||
* @file c_utils.h
|
||||
* @brief Main header for c_utils_lib - includes all utilities
|
||||
*
|
||||
* Include this header to access all c_utils_lib functionality.
|
||||
* Alternatively, include specific headers for modular usage.
|
||||
*/
|
||||
|
||||
// Version information
|
||||
#define C_UTILS_VERSION "v0.1.0"
|
||||
#define C_UTILS_VERSION_MAJOR 0
|
||||
#define C_UTILS_VERSION_MINOR 1
|
||||
#define C_UTILS_VERSION_PATCH 0
|
||||
|
||||
// Include all utilities
|
||||
#include "debug.h"
|
||||
#include "version.h"
|
||||
|
||||
#endif /* C_UTILS_H */
|
||||
```
|
||||
|
||||
## Phase 3: Version Utilities Implementation
|
||||
|
||||
### Step 3.1: Design Version API
|
||||
|
||||
**File**: `include/c_utils/version.h`
|
||||
|
||||
```c
|
||||
#ifndef C_UTILS_VERSION_H
|
||||
#define C_UTILS_VERSION_H
|
||||
|
||||
#include <time.h>
|
||||
|
||||
/**
|
||||
* @brief Version information structure
|
||||
*/
|
||||
typedef struct {
|
||||
int major;
|
||||
int minor;
|
||||
int patch;
|
||||
char git_hash[41]; // SHA-1 hash (40 chars + null)
|
||||
char build_date[32]; // ISO 8601 format
|
||||
char version_string[64]; // "vX.Y.Z" format
|
||||
} version_info_t;
|
||||
|
||||
/**
|
||||
* @brief Extract version from git tags
|
||||
* @param version Output version structure
|
||||
* @return 0 on success, -1 on error
|
||||
*/
|
||||
int version_get_from_git(version_info_t* version);
|
||||
|
||||
/**
|
||||
* @brief Generate version header file for a project
|
||||
* @param output_path Path to output header file
|
||||
* @param prefix Prefix for macros (e.g., "MY_APP")
|
||||
* @return 0 on success, -1 on error
|
||||
*/
|
||||
int version_generate_header(const char* output_path, const char* prefix);
|
||||
|
||||
/**
|
||||
* @brief Compare two versions
|
||||
* @return -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
||||
*/
|
||||
int version_compare(const version_info_t* v1, const version_info_t* v2);
|
||||
|
||||
/**
|
||||
* @brief Format version as string
|
||||
* @param version Version structure
|
||||
* @param buffer Output buffer
|
||||
* @param buffer_size Size of output buffer
|
||||
* @return Number of characters written
|
||||
*/
|
||||
int version_to_string(const version_info_t* version, char* buffer, size_t buffer_size);
|
||||
|
||||
#endif /* C_UTILS_VERSION_H */
|
||||
```
|
||||
|
||||
### Step 3.2: Implement Version Utilities
|
||||
|
||||
**File**: `src/version.c`
|
||||
|
||||
Key functions to implement:
|
||||
1. `version_get_from_git()` - Execute `git describe --tags` and parse
|
||||
2. `version_generate_header()` - Generate header file with macros
|
||||
3. `version_compare()` - Semantic version comparison
|
||||
4. `version_to_string()` - Format version string
|
||||
|
||||
### Step 3.3: Create Version Generation Script
|
||||
|
||||
**File**: `bin/generate_version`
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Generate version header for a project
|
||||
|
||||
OUTPUT_FILE="$1"
|
||||
PREFIX="$2"
|
||||
|
||||
if [ -z "$OUTPUT_FILE" ] || [ -z "$PREFIX" ]; then
|
||||
echo "Usage: $0 <output_file> <prefix>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get version from git
|
||||
if [ -d .git ]; then
|
||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "v0.0.0")
|
||||
GIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
else
|
||||
VERSION="v0.0.0"
|
||||
GIT_HASH="unknown"
|
||||
fi
|
||||
|
||||
# Parse version
|
||||
CLEAN_VERSION=$(echo "$VERSION" | sed 's/^v//' | cut -d- -f1)
|
||||
MAJOR=$(echo "$CLEAN_VERSION" | cut -d. -f1)
|
||||
MINOR=$(echo "$CLEAN_VERSION" | cut -d. -f2)
|
||||
PATCH=$(echo "$CLEAN_VERSION" | cut -d. -f3)
|
||||
BUILD_DATE=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
# Generate header
|
||||
cat > "$OUTPUT_FILE" << EOF
|
||||
/* Auto-generated by c_utils_lib version system */
|
||||
/* DO NOT EDIT - This file is automatically generated */
|
||||
|
||||
#ifndef ${PREFIX}_VERSION_H
|
||||
#define ${PREFIX}_VERSION_H
|
||||
|
||||
#define ${PREFIX}_VERSION "v${CLEAN_VERSION}"
|
||||
#define ${PREFIX}_VERSION_MAJOR ${MAJOR}
|
||||
#define ${PREFIX}_VERSION_MINOR ${MINOR}
|
||||
#define ${PREFIX}_VERSION_PATCH ${PATCH}
|
||||
#define ${PREFIX}_GIT_HASH "${GIT_HASH}"
|
||||
#define ${PREFIX}_BUILD_DATE "${BUILD_DATE}"
|
||||
|
||||
#endif /* ${PREFIX}_VERSION_H */
|
||||
EOF
|
||||
|
||||
echo "Generated $OUTPUT_FILE with version v${CLEAN_VERSION}"
|
||||
```
|
||||
|
||||
## Phase 4: Build System
|
||||
|
||||
### Step 4.1: Create Makefile
|
||||
|
||||
**File**: `Makefile`
|
||||
|
||||
```makefile
|
||||
# c_utils_lib Makefile
|
||||
|
||||
CC = gcc
|
||||
AR = ar
|
||||
CFLAGS = -Wall -Wextra -std=c99 -O2 -g
|
||||
INCLUDES = -Iinclude
|
||||
|
||||
# Directories
|
||||
SRC_DIR = src
|
||||
INCLUDE_DIR = include
|
||||
BUILD_DIR = build
|
||||
EXAMPLES_DIR = examples
|
||||
TESTS_DIR = tests
|
||||
|
||||
# Source files
|
||||
SOURCES = $(wildcard $(SRC_DIR)/*.c)
|
||||
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)
|
||||
|
||||
# Output library
|
||||
LIBRARY = libc_utils.a
|
||||
|
||||
# Default target
|
||||
all: $(LIBRARY)
|
||||
|
||||
# Create build directory
|
||||
$(BUILD_DIR):
|
||||
mkdir -p $(BUILD_DIR)
|
||||
|
||||
# Compile source files
|
||||
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
# Create static library
|
||||
$(LIBRARY): $(OBJECTS)
|
||||
$(AR) rcs $@ $^
|
||||
@echo "Built $(LIBRARY)"
|
||||
|
||||
# Build examples
|
||||
examples: $(LIBRARY)
|
||||
$(MAKE) -C $(EXAMPLES_DIR)
|
||||
|
||||
# Run tests
|
||||
test: $(LIBRARY)
|
||||
$(MAKE) -C $(TESTS_DIR)
|
||||
$(TESTS_DIR)/run_tests.sh
|
||||
|
||||
# Install to system (optional)
|
||||
install: $(LIBRARY)
|
||||
install -d /usr/local/lib
|
||||
install -m 644 $(LIBRARY) /usr/local/lib/
|
||||
install -d /usr/local/include/c_utils
|
||||
install -m 644 $(INCLUDE_DIR)/c_utils/*.h /usr/local/include/c_utils/
|
||||
@echo "Installed to /usr/local"
|
||||
|
||||
# Uninstall from system
|
||||
uninstall:
|
||||
rm -f /usr/local/lib/$(LIBRARY)
|
||||
rm -rf /usr/local/include/c_utils
|
||||
@echo "Uninstalled from /usr/local"
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR) $(LIBRARY)
|
||||
$(MAKE) -C $(EXAMPLES_DIR) clean 2>/dev/null || true
|
||||
$(MAKE) -C $(TESTS_DIR) clean 2>/dev/null || true
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "c_utils_lib Build System"
|
||||
@echo ""
|
||||
@echo "Targets:"
|
||||
@echo " all Build static library (default)"
|
||||
@echo " examples Build examples"
|
||||
@echo " test Run tests"
|
||||
@echo " install Install to /usr/local"
|
||||
@echo " uninstall Remove from /usr/local"
|
||||
@echo " clean Clean build artifacts"
|
||||
@echo " help Show this help"
|
||||
|
||||
.PHONY: all examples test install uninstall clean help
|
||||
```
|
||||
|
||||
### Step 4.2: Create Build Script
|
||||
|
||||
**File**: `build.sh`
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# c_utils_lib build script
|
||||
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
lib|"")
|
||||
echo "Building c_utils_lib..."
|
||||
make
|
||||
;;
|
||||
examples)
|
||||
echo "Building examples..."
|
||||
make examples
|
||||
;;
|
||||
test)
|
||||
echo "Running tests..."
|
||||
make test
|
||||
;;
|
||||
clean)
|
||||
echo "Cleaning..."
|
||||
make clean
|
||||
;;
|
||||
install)
|
||||
echo "Installing..."
|
||||
make install
|
||||
;;
|
||||
*)
|
||||
echo "Usage: ./build.sh [lib|examples|test|clean|install]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Done!"
|
||||
```
|
||||
|
||||
## Phase 5: Examples & Tests
|
||||
|
||||
### Step 5.1: Create Debug Example
|
||||
|
||||
**File**: `examples/debug_example.c`
|
||||
|
||||
```c
|
||||
#include <c_utils/debug.h>
|
||||
|
||||
int main() {
|
||||
// Initialize with INFO level
|
||||
debug_init(DEBUG_LEVEL_INFO);
|
||||
|
||||
DEBUG_INFO("Application started");
|
||||
DEBUG_WARN("This is a warning");
|
||||
DEBUG_ERROR("This is an error");
|
||||
|
||||
// This won't print (level too high)
|
||||
DEBUG_LOG("This debug message won't show");
|
||||
|
||||
// Change level to DEBUG
|
||||
g_debug_level = DEBUG_LEVEL_DEBUG;
|
||||
DEBUG_LOG("Now debug messages show");
|
||||
|
||||
// Change to TRACE to see file:line info
|
||||
g_debug_level = DEBUG_LEVEL_TRACE;
|
||||
DEBUG_TRACE("Trace with file:line information");
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5.2: Create Version Example
|
||||
|
||||
**File**: `examples/version_example.c`
|
||||
|
||||
```c
|
||||
#include <c_utils/version.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
version_info_t version;
|
||||
|
||||
// Get version from git
|
||||
if (version_get_from_git(&version) == 0) {
|
||||
char version_str[64];
|
||||
version_to_string(&version, version_str, sizeof(version_str));
|
||||
|
||||
printf("Version: %s\n", version_str);
|
||||
printf("Git Hash: %s\n", version.git_hash);
|
||||
printf("Build Date: %s\n", version.build_date);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5.3: Create Test Suite
|
||||
|
||||
**File**: `tests/test_debug.c`
|
||||
|
||||
```c
|
||||
#include <c_utils/debug.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
int test_debug_init() {
|
||||
debug_init(DEBUG_LEVEL_INFO);
|
||||
return (g_debug_level == DEBUG_LEVEL_INFO) ? 0 : -1;
|
||||
}
|
||||
|
||||
int test_debug_levels() {
|
||||
// Test that higher levels don't print at lower settings
|
||||
debug_init(DEBUG_LEVEL_ERROR);
|
||||
// Would need to capture stdout to verify
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int failed = 0;
|
||||
|
||||
printf("Running debug tests...\n");
|
||||
|
||||
if (test_debug_init() != 0) {
|
||||
printf("FAIL: test_debug_init\n");
|
||||
failed++;
|
||||
} else {
|
||||
printf("PASS: test_debug_init\n");
|
||||
}
|
||||
|
||||
if (test_debug_levels() != 0) {
|
||||
printf("FAIL: test_debug_levels\n");
|
||||
failed++;
|
||||
} else {
|
||||
printf("PASS: test_debug_levels\n");
|
||||
}
|
||||
|
||||
return failed;
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 6: Documentation
|
||||
|
||||
### Step 6.1: Create README.md
|
||||
|
||||
Key sections:
|
||||
1. Overview and purpose
|
||||
2. Quick start guide
|
||||
3. Installation instructions
|
||||
4. Usage examples
|
||||
5. API reference (brief)
|
||||
6. Integration guide
|
||||
7. Contributing guidelines
|
||||
8. License
|
||||
|
||||
### Step 6.2: Create API Documentation
|
||||
|
||||
**File**: `docs/API.md`
|
||||
|
||||
Complete API reference with:
|
||||
- Function signatures
|
||||
- Parameter descriptions
|
||||
- Return values
|
||||
- Usage examples
|
||||
- Common patterns
|
||||
|
||||
### Step 6.3: Create Integration Guide
|
||||
|
||||
**File**: `docs/INTEGRATION.md`
|
||||
|
||||
How to integrate into projects:
|
||||
1. As git submodule
|
||||
2. Makefile integration
|
||||
3. Code examples
|
||||
4. Migration from standalone utilities
|
||||
|
||||
## Phase 7: Integration with c-relay
|
||||
|
||||
### Step 7.1: Add as Submodule
|
||||
|
||||
```bash
|
||||
cd /path/to/c-relay
|
||||
git submodule add <repo-url> c_utils_lib
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### Step 7.2: Update c-relay Makefile
|
||||
|
||||
```makefile
|
||||
# Add to c-relay Makefile
|
||||
C_UTILS_LIB = c_utils_lib/libc_utils.a
|
||||
|
||||
# Update includes
|
||||
INCLUDES += -Ic_utils_lib/include
|
||||
|
||||
# Update libs
|
||||
LIBS += -Lc_utils_lib -lc_utils
|
||||
|
||||
# Add dependency
|
||||
$(C_UTILS_LIB):
|
||||
cd c_utils_lib && ./build.sh lib
|
||||
|
||||
# Update main target
|
||||
$(TARGET): $(C_UTILS_LIB) ...
|
||||
```
|
||||
|
||||
### Step 7.3: Update c-relay Source Files
|
||||
|
||||
**Changes needed**:
|
||||
|
||||
1. Update includes:
|
||||
```c
|
||||
// Old
|
||||
#include "debug.h"
|
||||
|
||||
// New
|
||||
#include <c_utils/debug.h>
|
||||
```
|
||||
|
||||
2. Remove old debug files:
|
||||
```bash
|
||||
git rm src/debug.c src/debug.h
|
||||
```
|
||||
|
||||
3. Update all files that use debug system:
|
||||
- `src/main.c`
|
||||
- `src/config.c`
|
||||
- `src/dm_admin.c`
|
||||
- `src/websockets.c`
|
||||
- `src/subscriptions.c`
|
||||
- Any other files using DEBUG_* macros
|
||||
|
||||
### Step 7.4: Test Integration
|
||||
|
||||
```bash
|
||||
cd c-relay
|
||||
make clean
|
||||
make
|
||||
./make_and_restart_relay.sh
|
||||
```
|
||||
|
||||
Verify:
|
||||
- Compilation succeeds
|
||||
- Debug output works correctly
|
||||
- No functionality regressions
|
||||
|
||||
## Phase 8: Version System Integration
|
||||
|
||||
### Step 8.1: Update c-relay Makefile for Versioning
|
||||
|
||||
```makefile
|
||||
# Add version generation
|
||||
src/version.h: .git/refs/tags/*
|
||||
c_utils_lib/bin/generate_version src/version.h C_RELAY
|
||||
|
||||
# Add dependency
|
||||
$(TARGET): src/version.h ...
|
||||
```
|
||||
|
||||
### Step 8.2: Update c-relay to Use Generated Version
|
||||
|
||||
Replace hardcoded version in `src/main.h` with:
|
||||
```c
|
||||
#include "version.h"
|
||||
// Use C_RELAY_VERSION instead of hardcoded VERSION
|
||||
```
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
- **Phase 1**: Repository Setup - 1 hour
|
||||
- **Phase 2**: Debug System - 2 hours
|
||||
- **Phase 3**: Version Utilities - 4 hours
|
||||
- **Phase 4**: Build System - 2 hours
|
||||
- **Phase 5**: Examples & Tests - 3 hours
|
||||
- **Phase 6**: Documentation - 3 hours
|
||||
- **Phase 7**: c-relay Integration - 2 hours
|
||||
- **Phase 8**: Version Integration - 2 hours
|
||||
|
||||
**Total**: ~19 hours
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] c_utils_lib builds successfully
|
||||
- [ ] All tests pass
|
||||
- [ ] Examples compile and run
|
||||
- [ ] c-relay integrates successfully
|
||||
- [ ] Debug output works in c-relay
|
||||
- [ ] Version generation works
|
||||
- [ ] Documentation complete
|
||||
- [ ] No regressions in c-relay functionality
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review this plan with stakeholders
|
||||
2. Create repository structure
|
||||
3. Implement debug system
|
||||
4. Implement version utilities
|
||||
5. Create build system
|
||||
6. Write tests and examples
|
||||
7. Create documentation
|
||||
8. Integrate into c-relay
|
||||
9. Test thoroughly
|
||||
10. Publish to GitHub
|
||||
|
||||
## Notes
|
||||
|
||||
- Keep the API simple and intuitive
|
||||
- Focus on zero external dependencies
|
||||
- Prioritize learning value in code comments
|
||||
- Make integration as easy as possible
|
||||
- Document everything thoroughly
|
||||
@@ -175,6 +175,18 @@ Configuration events follow the standard Nostr event format with kind 33334:
|
||||
- **Impact**: Allows some flexibility in expiration timing
|
||||
- **Example**: `"600"` (10 minute grace period)
|
||||
|
||||
### NIP-59 Gift Wrap Timestamp Configuration
|
||||
|
||||
#### `nip59_timestamp_max_delay_sec`
|
||||
- **Description**: Controls timestamp randomization for NIP-59 gift wraps
|
||||
- **Default**: `"0"` (no randomization)
|
||||
- **Range**: `0` to `604800` (7 days)
|
||||
- **Impact**: Affects compatibility with other Nostr clients for direct messaging
|
||||
- **Values**:
|
||||
- `"0"`: No randomization (maximum compatibility)
|
||||
- `"1-604800"`: Random timestamp between now and N seconds ago
|
||||
- **Example**: `"172800"` (2 days randomization for privacy)
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Basic Relay Setup
|
||||
|
||||
562
docs/debug_system.md
Normal file
562
docs/debug_system.md
Normal file
@@ -0,0 +1,562 @@
|
||||
# Simple Debug System Proposal
|
||||
|
||||
## Overview
|
||||
|
||||
A minimal debug system with 6 levels (0-5) controlled by a single `--debug-level` flag. TRACE level (5) automatically includes file:line information for ALL messages. Uses compile-time macros to ensure **zero performance impact and zero size increase** in production builds.
|
||||
|
||||
## Debug Levels
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
DEBUG_LEVEL_NONE = 0, // Production: no debug output
|
||||
DEBUG_LEVEL_ERROR = 1, // Errors only
|
||||
DEBUG_LEVEL_WARN = 2, // Errors + Warnings
|
||||
DEBUG_LEVEL_INFO = 3, // Errors + Warnings + Info
|
||||
DEBUG_LEVEL_DEBUG = 4, // All above + Debug messages
|
||||
DEBUG_LEVEL_TRACE = 5 // All above + Trace (very verbose)
|
||||
} debug_level_t;
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Production (default - no debug output)
|
||||
./c_relay_x86
|
||||
|
||||
# Show errors only
|
||||
./c_relay_x86 --debug-level=1
|
||||
|
||||
# Show errors and warnings
|
||||
./c_relay_x86 --debug-level=2
|
||||
|
||||
# Show errors, warnings, and info (recommended for development)
|
||||
./c_relay_x86 --debug-level=3
|
||||
|
||||
# Show all debug messages
|
||||
./c_relay_x86 --debug-level=4
|
||||
|
||||
# Show everything including trace with file:line (very verbose)
|
||||
./c_relay_x86 --debug-level=5
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. Header File (`src/debug.h`)
|
||||
|
||||
```c
|
||||
#ifndef DEBUG_H
|
||||
#define DEBUG_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
// Debug levels
|
||||
typedef enum {
|
||||
DEBUG_LEVEL_NONE = 0,
|
||||
DEBUG_LEVEL_ERROR = 1,
|
||||
DEBUG_LEVEL_WARN = 2,
|
||||
DEBUG_LEVEL_INFO = 3,
|
||||
DEBUG_LEVEL_DEBUG = 4,
|
||||
DEBUG_LEVEL_TRACE = 5
|
||||
} debug_level_t;
|
||||
|
||||
// Global debug level (set at runtime via CLI)
|
||||
extern debug_level_t g_debug_level;
|
||||
|
||||
// Initialize debug system
|
||||
void debug_init(int level);
|
||||
|
||||
// Core logging function
|
||||
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...);
|
||||
|
||||
// Convenience macros that check level before calling
|
||||
// Note: TRACE level (5) and above include file:line information for ALL messages
|
||||
#define DEBUG_ERROR(...) \
|
||||
do { if (g_debug_level >= DEBUG_LEVEL_ERROR) debug_log(DEBUG_LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||
|
||||
#define DEBUG_WARN(...) \
|
||||
do { if (g_debug_level >= DEBUG_LEVEL_WARN) debug_log(DEBUG_LEVEL_WARN, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||
|
||||
#define DEBUG_INFO(...) \
|
||||
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||
|
||||
#define DEBUG_LOG(...) \
|
||||
do { if (g_debug_level >= DEBUG_LEVEL_DEBUG) debug_log(DEBUG_LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||
|
||||
#define DEBUG_TRACE(...) \
|
||||
do { if (g_debug_level >= DEBUG_LEVEL_TRACE) debug_log(DEBUG_LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||
|
||||
#endif /* DEBUG_H */
|
||||
```
|
||||
|
||||
### 2. Implementation File (`src/debug.c`)
|
||||
|
||||
```c
|
||||
#include "debug.h"
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
|
||||
// Global debug level (default: no debug output)
|
||||
debug_level_t g_debug_level = DEBUG_LEVEL_NONE;
|
||||
|
||||
void debug_init(int level) {
|
||||
if (level < 0) level = 0;
|
||||
if (level > 5) level = 5;
|
||||
g_debug_level = (debug_level_t)level;
|
||||
}
|
||||
|
||||
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...) {
|
||||
// Get timestamp
|
||||
time_t now = time(NULL);
|
||||
struct tm* tm_info = localtime(&now);
|
||||
char timestamp[32];
|
||||
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
|
||||
|
||||
// Get level string
|
||||
const char* level_str = "UNKNOWN";
|
||||
switch (level) {
|
||||
case DEBUG_LEVEL_ERROR: level_str = "ERROR"; break;
|
||||
case DEBUG_LEVEL_WARN: level_str = "WARN "; break;
|
||||
case DEBUG_LEVEL_INFO: level_str = "INFO "; break;
|
||||
case DEBUG_LEVEL_DEBUG: level_str = "DEBUG"; break;
|
||||
case DEBUG_LEVEL_TRACE: level_str = "TRACE"; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Print prefix with timestamp and level
|
||||
printf("[%s] [%s] ", timestamp, level_str);
|
||||
|
||||
// Print source location when debug level is TRACE (5) or higher
|
||||
if (file && g_debug_level >= DEBUG_LEVEL_TRACE) {
|
||||
// Extract just the filename (not full path)
|
||||
const char* filename = strrchr(file, '/');
|
||||
filename = filename ? filename + 1 : file;
|
||||
printf("[%s:%d] ", filename, line);
|
||||
}
|
||||
|
||||
// Print message
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vprintf(format, args);
|
||||
va_end(args);
|
||||
|
||||
printf("\n");
|
||||
fflush(stdout);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. CLI Argument Parsing (add to `src/main.c`)
|
||||
|
||||
```c
|
||||
// In main() function, add to argument parsing:
|
||||
|
||||
int debug_level = 0; // Default: no debug output
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strncmp(argv[i], "--debug-level=", 14) == 0) {
|
||||
debug_level = atoi(argv[i] + 14);
|
||||
if (debug_level < 0) debug_level = 0;
|
||||
if (debug_level > 5) debug_level = 5;
|
||||
}
|
||||
// ... other arguments ...
|
||||
}
|
||||
|
||||
// Initialize debug system
|
||||
debug_init(debug_level);
|
||||
```
|
||||
|
||||
### 4. Update Makefile
|
||||
|
||||
```makefile
|
||||
# Add debug.c to source files
|
||||
MAIN_SRC = src/main.c src/config.c src/debug.c src/dm_admin.c src/request_validator.c ...
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Keep Existing Functions
|
||||
|
||||
The existing `log_*` functions can remain as wrappers:
|
||||
|
||||
```c
|
||||
// src/main.c - Update existing functions
|
||||
// Note: These don't include file:line since they're wrappers
|
||||
void log_info(const char* message) {
|
||||
if (g_debug_level >= DEBUG_LEVEL_INFO) {
|
||||
debug_log(DEBUG_LEVEL_INFO, NULL, 0, "%s", message);
|
||||
}
|
||||
}
|
||||
|
||||
void log_error(const char* message) {
|
||||
if (g_debug_level >= DEBUG_LEVEL_ERROR) {
|
||||
debug_log(DEBUG_LEVEL_ERROR, NULL, 0, "%s", message);
|
||||
}
|
||||
}
|
||||
|
||||
void log_warning(const char* message) {
|
||||
if (g_debug_level >= DEBUG_LEVEL_WARN) {
|
||||
debug_log(DEBUG_LEVEL_WARN, NULL, 0, "%s", message);
|
||||
}
|
||||
}
|
||||
|
||||
void log_success(const char* message) {
|
||||
if (g_debug_level >= DEBUG_LEVEL_INFO) {
|
||||
debug_log(DEBUG_LEVEL_INFO, NULL, 0, "✓ %s", message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Gradual Migration
|
||||
|
||||
Gradually replace log calls with debug macros:
|
||||
|
||||
```c
|
||||
// Before:
|
||||
log_info("Starting WebSocket relay server");
|
||||
|
||||
// After:
|
||||
DEBUG_INFO("Starting WebSocket relay server");
|
||||
|
||||
// Before:
|
||||
log_error("Failed to initialize database");
|
||||
|
||||
// After:
|
||||
DEBUG_ERROR("Failed to initialize database");
|
||||
```
|
||||
|
||||
### Add New Debug Levels
|
||||
|
||||
Add debug and trace messages where needed:
|
||||
|
||||
```c
|
||||
// Detailed debugging
|
||||
DEBUG_LOG("Processing subscription: %s", sub_id);
|
||||
DEBUG_LOG("Filter count: %d", filter_count);
|
||||
|
||||
// Very verbose tracing
|
||||
DEBUG_TRACE("Entering handle_req_message()");
|
||||
DEBUG_TRACE("Subscription ID validated: %s", sub_id);
|
||||
DEBUG_TRACE("Exiting handle_req_message()");
|
||||
```
|
||||
## Manual Guards for Expensive Operations
|
||||
|
||||
### The Problem
|
||||
|
||||
Debug macros use **runtime checks**, which means function arguments are always evaluated:
|
||||
|
||||
```c
|
||||
// ❌ BAD: Database query executes even when debug level is 0
|
||||
DEBUG_LOG("Count: %d", expensive_database_query());
|
||||
```
|
||||
|
||||
The `expensive_database_query()` will **always execute** because function arguments are evaluated before the `if` check inside the macro.
|
||||
|
||||
### The Solution: Manual Guards
|
||||
|
||||
For expensive operations (database queries, file I/O, complex calculations), use manual guards:
|
||||
|
||||
```c
|
||||
// ✅ GOOD: Query only executes when debugging is enabled
|
||||
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
||||
int count = expensive_database_query();
|
||||
DEBUG_LOG("Count: %d", count);
|
||||
}
|
||||
```
|
||||
|
||||
### Standardized Comment Format
|
||||
|
||||
To make temporary debug guards easy to find and remove, use this standardized format:
|
||||
|
||||
```c
|
||||
// DEBUG_GUARD_START
|
||||
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
||||
// Expensive operation here
|
||||
sqlite3_stmt* stmt;
|
||||
const char* sql = "SELECT COUNT(*) FROM events";
|
||||
int count = 0;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
count = sqlite3_column_int(stmt, 0);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
DEBUG_LOG("Event count: %d", count);
|
||||
}
|
||||
// DEBUG_GUARD_END
|
||||
```
|
||||
|
||||
### Easy Removal
|
||||
|
||||
When you're done debugging, find and remove all temporary guards:
|
||||
|
||||
```bash
|
||||
# Find all debug guards
|
||||
grep -n "DEBUG_GUARD_START" src/*.c
|
||||
|
||||
# Remove guards with sed (between START and END markers)
|
||||
sed -i '/DEBUG_GUARD_START/,/DEBUG_GUARD_END/d' src/config.c
|
||||
```
|
||||
|
||||
Or use a simple script:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# remove_debug_guards.sh
|
||||
for file in src/*.c; do
|
||||
sed -i '/DEBUG_GUARD_START/,/DEBUG_GUARD_END/d' "$file"
|
||||
echo "Removed debug guards from $file"
|
||||
done
|
||||
```
|
||||
|
||||
### When to Use Manual Guards
|
||||
|
||||
Use manual guards for:
|
||||
- ✅ Database queries
|
||||
- ✅ File I/O operations
|
||||
- ✅ Network requests
|
||||
- ✅ Complex calculations
|
||||
- ✅ Memory allocations for debug data
|
||||
- ✅ String formatting with multiple operations
|
||||
|
||||
Don't need guards for:
|
||||
- ❌ Simple variable access
|
||||
- ❌ Basic arithmetic
|
||||
- ❌ String literals
|
||||
- ❌ Function calls that are already cheap
|
||||
|
||||
### Example: Database Query Guard
|
||||
|
||||
```c
|
||||
// DEBUG_GUARD_START
|
||||
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
||||
sqlite3_stmt* count_stmt;
|
||||
const char* count_sql = "SELECT COUNT(*) FROM config";
|
||||
int config_count = 0;
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(count_stmt) == SQLITE_ROW) {
|
||||
config_count = sqlite3_column_int(count_stmt, 0);
|
||||
}
|
||||
sqlite3_finalize(count_stmt);
|
||||
}
|
||||
|
||||
DEBUG_LOG("Config table has %d rows", config_count);
|
||||
}
|
||||
// DEBUG_GUARD_END
|
||||
```
|
||||
|
||||
### Example: Complex String Formatting Guard
|
||||
|
||||
```c
|
||||
// DEBUG_GUARD_START
|
||||
if (g_debug_level >= DEBUG_LEVEL_TRACE) {
|
||||
char filter_str[1024] = {0};
|
||||
int offset = 0;
|
||||
|
||||
for (int i = 0; i < filter_count && offset < sizeof(filter_str) - 1; i++) {
|
||||
offset += snprintf(filter_str + offset, sizeof(filter_str) - offset,
|
||||
"Filter %d: kind=%d, author=%s; ",
|
||||
i, filters[i].kind, filters[i].author);
|
||||
}
|
||||
|
||||
DEBUG_TRACE("Processing filters: %s", filter_str);
|
||||
}
|
||||
// DEBUG_GUARD_END
|
||||
```
|
||||
|
||||
### Alternative: Compile-Time Guards
|
||||
|
||||
For permanent debug code that should be completely removed in production builds, use compile-time guards:
|
||||
|
||||
```c
|
||||
#ifdef ENABLE_DEBUG_CODE
|
||||
// This code is completely removed when ENABLE_DEBUG_CODE is not defined
|
||||
int count = expensive_database_query();
|
||||
DEBUG_LOG("Count: %d", count);
|
||||
#endif
|
||||
```
|
||||
|
||||
Build with debug code:
|
||||
```bash
|
||||
make CFLAGS="-DENABLE_DEBUG_CODE"
|
||||
```
|
||||
|
||||
Build without debug code (production):
|
||||
```bash
|
||||
make # No debug code compiled in
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always use standardized markers** (`DEBUG_GUARD_START`/`DEBUG_GUARD_END`) for temporary guards
|
||||
2. **Add a comment** explaining what you're debugging
|
||||
3. **Remove guards** when debugging is complete
|
||||
4. **Use compile-time guards** for permanent debug infrastructure
|
||||
5. **Keep guards simple** - one guard per logical debug operation
|
||||
|
||||
|
||||
## Performance Impact
|
||||
|
||||
### Runtime Check
|
||||
|
||||
The macros include a runtime check:
|
||||
|
||||
```c
|
||||
#define DEBUG_INFO(...) \
|
||||
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, NULL, 0, __VA_ARGS__); } while(0)
|
||||
```
|
||||
|
||||
**Cost**: One integer comparison per debug statement (~1 CPU cycle)
|
||||
|
||||
**Impact**: Negligible - the comparison is faster than a function call
|
||||
|
||||
**Note**: Only `DEBUG_TRACE` includes `__FILE__` and `__LINE__`, which are compile-time constants with no runtime overhead.
|
||||
|
||||
### When Debug Level is 0 (Production)
|
||||
|
||||
```c
|
||||
// With g_debug_level = 0:
|
||||
DEBUG_INFO("Starting server");
|
||||
|
||||
// Becomes:
|
||||
if (0 >= 3) debug_log(...); // Never executes
|
||||
|
||||
// Compiler optimizes to:
|
||||
// (nothing - branch is eliminated)
|
||||
```
|
||||
|
||||
**Result**: Modern compilers (gcc -O2 or higher) will completely eliminate the dead code branch.
|
||||
|
||||
### Size Impact
|
||||
|
||||
**Test Case**: 100 debug statements in code
|
||||
|
||||
**Without optimization** (`-O0`):
|
||||
- Binary size increase: ~2KB (branch instructions)
|
||||
- Runtime cost: 100 comparisons per execution
|
||||
|
||||
**With optimization** (`-O2` or `-O3`):
|
||||
- Binary size increase: **0 bytes** (dead code eliminated when g_debug_level = 0)
|
||||
- Runtime cost: **0 cycles** (branches removed by compiler)
|
||||
|
||||
### Verification
|
||||
|
||||
You can verify the optimization with:
|
||||
|
||||
```bash
|
||||
# Compile with optimization
|
||||
gcc -O2 -c debug_test.c -o debug_test.o
|
||||
|
||||
# Disassemble and check
|
||||
objdump -d debug_test.o | grep -A 10 "debug_log"
|
||||
```
|
||||
|
||||
When `g_debug_level = 0` (constant), you'll see the compiler has removed all debug calls.
|
||||
|
||||
## Example Output
|
||||
|
||||
### Level 0 (Production)
|
||||
```
|
||||
(no output)
|
||||
```
|
||||
|
||||
### Level 1 (Errors Only)
|
||||
```
|
||||
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
|
||||
[2025-01-12 14:30:20] [ERROR] WebSocket connection failed: port in use
|
||||
```
|
||||
|
||||
### Level 2 (Errors + Warnings)
|
||||
```
|
||||
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
|
||||
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
|
||||
[2025-01-12 14:30:17] [WARN ] Configuration key 'relay_name' not found, using default
|
||||
```
|
||||
|
||||
### Level 3 (Errors + Warnings + Info)
|
||||
```
|
||||
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
|
||||
[2025-01-12 14:30:15] [INFO ] Loading configuration from database
|
||||
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
|
||||
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
|
||||
[2025-01-12 14:30:17] [INFO ] WebSocket relay started on ws://127.0.0.1:8889
|
||||
```
|
||||
|
||||
### Level 4 (All Debug Messages)
|
||||
```
|
||||
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
|
||||
[2025-01-12 14:30:15] [DEBUG] Opening database: build/abc123...def.db
|
||||
[2025-01-12 14:30:15] [DEBUG] Executing schema initialization
|
||||
[2025-01-12 14:30:15] [INFO ] SQLite WAL mode enabled
|
||||
[2025-01-12 14:30:16] [DEBUG] Attempting to bind to port 8888
|
||||
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
|
||||
[2025-01-12 14:30:17] [DEBUG] Successfully bound to port 8889
|
||||
[2025-01-12 14:30:17] [INFO ] WebSocket relay started on ws://127.0.0.1:8889
|
||||
```
|
||||
|
||||
### Level 5 (Everything Including file:line for ALL messages)
|
||||
```
|
||||
[2025-01-12 14:30:15] [INFO ] [main.c:1607] Initializing C-Relay v0.4.6
|
||||
[2025-01-12 14:30:15] [DEBUG] [main.c:348] Opening database: build/abc123...def.db
|
||||
[2025-01-12 14:30:15] [TRACE] [main.c:330] Entering init_database()
|
||||
[2025-01-12 14:30:15] [ERROR] [config.c:125] Database locked
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Create Files (5 minutes)
|
||||
|
||||
1. Create `src/debug.h` with the header code above
|
||||
2. Create `src/debug.c` with the implementation code above
|
||||
3. Update `Makefile` to include `src/debug.c` in `MAIN_SRC`
|
||||
|
||||
### Step 2: Add CLI Parsing (5 minutes)
|
||||
|
||||
Add `--debug-level` argument parsing to `main()` in `src/main.c`
|
||||
|
||||
### Step 3: Update Existing Functions (5 minutes)
|
||||
|
||||
Update the existing `log_*` functions to use the new debug macros
|
||||
|
||||
### Step 4: Test (5 minutes)
|
||||
|
||||
```bash
|
||||
# Build
|
||||
make clean && make
|
||||
|
||||
# Test different levels
|
||||
./build/c_relay_x86 # No output
|
||||
./build/c_relay_x86 --debug-level=1 # Errors only
|
||||
./build/c_relay_x86 --debug-level=3 # Info + warnings + errors
|
||||
./build/c_relay_x86 --debug-level=4 # All debug messages
|
||||
./build/c_relay_x86 --debug-level=5 # Everything with file:line on TRACE
|
||||
```
|
||||
|
||||
### Step 5: Gradual Migration (Ongoing)
|
||||
|
||||
As you work on different parts of the code, replace `log_*` calls with `DEBUG_*` macros and add new debug/trace statements where helpful.
|
||||
|
||||
## Benefits
|
||||
|
||||
✅ **Simple**: Single flag, 6 levels, easy to understand
|
||||
✅ **Zero Overhead**: Compiler optimizes away unused debug code
|
||||
✅ **Zero Size Impact**: No binary size increase in production
|
||||
✅ **Backward Compatible**: Existing `log_*` functions still work
|
||||
✅ **Easy Migration**: Gradual replacement of log calls
|
||||
✅ **Flexible**: Can add detailed debugging without affecting production
|
||||
|
||||
## Total Implementation Time
|
||||
|
||||
**~20 minutes** for basic implementation
|
||||
**Ongoing** for gradual migration of existing log calls
|
||||
|
||||
## Recommendation
|
||||
|
||||
This is the simplest possible debug system that provides:
|
||||
- Multiple debug levels for different verbosity
|
||||
- Zero performance impact in production
|
||||
- Zero binary size increase
|
||||
- Easy to use and understand
|
||||
- Backward compatible with existing code
|
||||
|
||||
Start with the basic implementation, test it, then gradually migrate existing log calls and add new debug statements as needed.
|
||||
@@ -1,358 +0,0 @@
|
||||
# Event-Based Configuration System Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a detailed implementation plan for transitioning the C Nostr Relay from command line arguments and file-based configuration to a pure event-based configuration system using kind 33334 Nostr events stored directly in the database.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 0: File Structure Preparation ✅ COMPLETED
|
||||
|
||||
#### 0.1 Backup and Prepare Files ✅ COMPLETED
|
||||
**Actions:**
|
||||
1. ✅ Rename `src/config.c` to `src/config.c.old` - DONE
|
||||
2. ✅ Rename `src/config.h` to `src/config.h.old` - DONE
|
||||
3. ✅ Create new empty `src/config.c` and `src/config.h` - DONE
|
||||
4. ✅ Create new `src/default_config_event.h` - DONE
|
||||
|
||||
### Phase 1: Database Schema and Core Infrastructure ✅ COMPLETED
|
||||
|
||||
#### 1.1 Update Database Naming System ✅ COMPLETED
|
||||
**File:** `src/main.c`, new `src/config.c`, new `src/config.h`
|
||||
|
||||
```c
|
||||
// New functions implemented: ✅
|
||||
char* get_database_name_from_relay_pubkey(const char* relay_pubkey);
|
||||
int create_database_with_relay_pubkey(const char* relay_pubkey);
|
||||
```
|
||||
|
||||
**Changes Completed:** ✅
|
||||
- ✅ Create completely new `src/config.c` and `src/config.h` files
|
||||
- ✅ Rename old files to `src/config.c.old` and `src/config.h.old`
|
||||
- ✅ Modify `init_database()` to use relay pubkey for database naming
|
||||
- ✅ Use `nostr_core_lib` functions for all keypair generation
|
||||
- ✅ Database path: `./<relay_pubkey>.nrdb`
|
||||
- ✅ Remove all database path command line argument handling
|
||||
|
||||
#### 1.2 Configuration Event Storage ✅ COMPLETED
|
||||
**File:** new `src/config.c`, new `src/default_config_event.h`
|
||||
|
||||
```c
|
||||
// Configuration functions implemented: ✅
|
||||
int store_config_event_in_database(const cJSON* event);
|
||||
cJSON* load_config_event_from_database(const char* relay_pubkey);
|
||||
```
|
||||
|
||||
**Changes Completed:** ✅
|
||||
- ✅ Create new `src/default_config_event.h` for default configuration values
|
||||
- ✅ Add functions to store/retrieve kind 33334 events from events table
|
||||
- ✅ Use `nostr_core_lib` functions for all event validation
|
||||
- ✅ Clean separation: default config values isolated in header file
|
||||
- ✅ Remove existing config table dependencies
|
||||
|
||||
### Phase 2: Event Processing Integration ✅ COMPLETED
|
||||
|
||||
#### 2.1 Real-time Configuration Processing ✅ COMPLETED
|
||||
**File:** `src/main.c` (event processing functions)
|
||||
|
||||
**Integration Points:** ✅ IMPLEMENTED
|
||||
```c
|
||||
// In existing event processing loop: ✅ IMPLEMENTED
|
||||
// Added kind 33334 event detection in main event loop
|
||||
if (kind_num == 33334) {
|
||||
if (handle_configuration_event(event, error_message, sizeof(error_message)) == 0) {
|
||||
// Configuration event processed successfully
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration event processing implemented: ✅
|
||||
int process_configuration_event(const cJSON* event);
|
||||
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
|
||||
```
|
||||
|
||||
#### 2.2 Configuration Application System ⚠️ PARTIALLY COMPLETED
|
||||
**File:** `src/config.c`
|
||||
|
||||
**Status:** Configuration access functions implemented, field handlers need completion
|
||||
```c
|
||||
// Configuration access implemented: ✅
|
||||
const char* get_config_value(const char* key);
|
||||
int get_config_int(const char* key, int default_value);
|
||||
int get_config_bool(const char* key, int default_value);
|
||||
|
||||
// Field handlers need implementation: ⏳ IN PROGRESS
|
||||
// Need to implement specific apply functions for runtime changes
|
||||
```
|
||||
|
||||
### Phase 3: First-Time Startup System ✅ COMPLETED
|
||||
|
||||
#### 3.1 Key Generation and Initial Setup ✅ COMPLETED
|
||||
**File:** new `src/config.c`, `src/default_config_event.h`
|
||||
|
||||
**Status:** ✅ FULLY IMPLEMENTED with secure /dev/urandom + nostr_core_lib validation
|
||||
|
||||
```c
|
||||
int first_time_startup_sequence() {
|
||||
// 1. Generate admin keypair using nostr_core_lib
|
||||
unsigned char admin_privkey_bytes[32];
|
||||
char admin_privkey[65], admin_pubkey[65];
|
||||
|
||||
if (nostr_generate_private_key(admin_privkey_bytes) != 0) {
|
||||
return -1;
|
||||
}
|
||||
nostr_bytes_to_hex(admin_privkey_bytes, 32, admin_privkey);
|
||||
|
||||
unsigned char admin_pubkey_bytes[32];
|
||||
if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != 0) {
|
||||
return -1;
|
||||
}
|
||||
nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey);
|
||||
|
||||
// 2. Generate relay keypair using nostr_core_lib
|
||||
unsigned char relay_privkey_bytes[32];
|
||||
char relay_privkey[65], relay_pubkey[65];
|
||||
|
||||
if (nostr_generate_private_key(relay_privkey_bytes) != 0) {
|
||||
return -1;
|
||||
}
|
||||
nostr_bytes_to_hex(relay_privkey_bytes, 32, relay_privkey);
|
||||
|
||||
unsigned char relay_pubkey_bytes[32];
|
||||
if (nostr_ec_public_key_from_private_key(relay_privkey_bytes, relay_pubkey_bytes) != 0) {
|
||||
return -1;
|
||||
}
|
||||
nostr_bytes_to_hex(relay_pubkey_bytes, 32, relay_pubkey);
|
||||
|
||||
// 3. Create database with relay pubkey name
|
||||
if (create_database_with_relay_pubkey(relay_pubkey) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 4. Create initial configuration event using defaults from header
|
||||
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey);
|
||||
|
||||
// 5. Store configuration event in database
|
||||
store_config_event_in_database(config_event);
|
||||
|
||||
// 6. Print admin private key for user to save
|
||||
printf("=== SAVE THIS ADMIN PRIVATE KEY ===\n");
|
||||
printf("Admin Private Key: %s\n", admin_privkey);
|
||||
printf("===================================\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 Database Detection Logic ✅ COMPLETED
|
||||
**File:** `src/main.c`
|
||||
|
||||
**Status:** ✅ FULLY IMPLEMENTED
|
||||
```c
|
||||
// Implemented functions: ✅
|
||||
char** find_existing_nrdb_files(void);
|
||||
char* extract_pubkey_from_filename(const char* filename);
|
||||
int is_first_time_startup(void);
|
||||
int first_time_startup_sequence(void);
|
||||
int startup_existing_relay(const char* relay_pubkey);
|
||||
```
|
||||
|
||||
### Phase 4: Legacy System Removal ✅ PARTIALLY COMPLETED
|
||||
|
||||
#### 4.1 Remove Command Line Arguments ✅ COMPLETED
|
||||
**File:** `src/main.c`
|
||||
|
||||
**Status:** ✅ COMPLETED
|
||||
- ✅ All argument parsing logic removed except --help and --version
|
||||
- ✅ `--port`, `--config-dir`, `--config-file`, `--database-path` handling removed
|
||||
- ✅ Environment variable override systems removed
|
||||
- ✅ Clean help and version functions implemented
|
||||
|
||||
#### 4.2 Remove Configuration File System ✅ COMPLETED
|
||||
**File:** `src/config.c`
|
||||
|
||||
**Status:** ✅ COMPLETED - New file created from scratch
|
||||
- ✅ All legacy file-based configuration functions removed
|
||||
- ✅ XDG configuration directory logic removed
|
||||
- ✅ Pure event-based system implemented
|
||||
|
||||
#### 4.3 Remove Legacy Database Tables ⏳ PENDING
|
||||
**File:** `src/sql_schema.h`
|
||||
|
||||
**Status:** ⏳ NEEDS COMPLETION
|
||||
```sql
|
||||
-- Still need to remove these tables:
|
||||
DROP TABLE IF EXISTS config;
|
||||
DROP TABLE IF EXISTS config_history;
|
||||
DROP TABLE IF EXISTS config_file_cache;
|
||||
DROP VIEW IF EXISTS active_config;
|
||||
```
|
||||
|
||||
### Phase 5: Configuration Management
|
||||
|
||||
#### 5.1 Configuration Field Mapping
|
||||
**File:** `src/config.c`
|
||||
|
||||
```c
|
||||
// Map configuration tags to current system
|
||||
static const config_field_handler_t config_handlers[] = {
|
||||
{"auth_enabled", 0, apply_auth_enabled},
|
||||
{"relay_port", 1, apply_relay_port}, // requires restart
|
||||
{"max_connections", 0, apply_max_connections},
|
||||
{"relay_description", 0, apply_relay_description},
|
||||
{"relay_contact", 0, apply_relay_contact},
|
||||
{"relay_pubkey", 1, apply_relay_pubkey}, // requires restart
|
||||
{"relay_privkey", 1, apply_relay_privkey}, // requires restart
|
||||
{"pow_min_difficulty", 0, apply_pow_difficulty},
|
||||
{"nip40_expiration_enabled", 0, apply_expiration_enabled},
|
||||
{"max_subscriptions_per_client", 0, apply_max_subscriptions},
|
||||
{"max_event_tags", 0, apply_max_event_tags},
|
||||
{"max_content_length", 0, apply_max_content_length},
|
||||
{"default_limit", 0, apply_default_limit},
|
||||
{"max_limit", 0, apply_max_limit},
|
||||
// ... etc
|
||||
};
|
||||
```
|
||||
|
||||
#### 5.2 Startup Configuration Loading
|
||||
**File:** `src/main.c`
|
||||
|
||||
```c
|
||||
int startup_existing_relay(const char* relay_pubkey) {
|
||||
// 1. Open database
|
||||
if (init_database_with_pubkey(relay_pubkey) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 2. Load configuration event from database
|
||||
cJSON* config_event = load_config_event_from_database(relay_pubkey);
|
||||
if (!config_event) {
|
||||
log_error("No configuration event found in database");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 3. Apply all configuration from event
|
||||
if (apply_configuration_from_event(config_event) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 4. Continue with normal startup
|
||||
return start_relay_services();
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Order - PROGRESS STATUS
|
||||
|
||||
### Step 1: Core Infrastructure ✅ COMPLETED
|
||||
1. ✅ Implement database naming with relay pubkey
|
||||
2. ✅ Add key generation functions using `nostr_core_lib`
|
||||
3. ✅ Create configuration event storage/retrieval functions
|
||||
4. ✅ Test basic event creation and storage
|
||||
|
||||
### Step 2: Event Processing Integration ✅ MOSTLY COMPLETED
|
||||
1. ✅ Add kind 33334 event detection to event processing loop
|
||||
2. ✅ Implement configuration event validation
|
||||
3. ⚠️ Create configuration application handlers (basic access implemented, runtime handlers pending)
|
||||
4. ⏳ Test real-time configuration updates (infrastructure ready)
|
||||
|
||||
### Step 3: First-Time Startup ✅ COMPLETED
|
||||
1. ✅ Implement first-time startup detection
|
||||
2. ✅ Add automatic key generation and database creation
|
||||
3. ✅ Create default configuration event generation
|
||||
4. ✅ Test complete first-time startup flow
|
||||
|
||||
### Step 4: Legacy Removal ⚠️ MOSTLY COMPLETED
|
||||
1. ✅ Remove command line argument parsing
|
||||
2. ✅ Remove configuration file system
|
||||
3. ⏳ Remove legacy database tables (pending)
|
||||
4. ✅ Update all references to use event-based config
|
||||
|
||||
### Step 5: Testing and Validation ⚠️ PARTIALLY COMPLETED
|
||||
1. ✅ Test complete startup flow (first time and existing)
|
||||
2. ⏳ Test configuration updates via events (infrastructure ready)
|
||||
3. ⚠️ Test error handling and recovery (basic error handling implemented)
|
||||
4. ⏳ Performance testing and optimization (pending)
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### For Existing Installations
|
||||
Since the new system uses a completely different approach:
|
||||
|
||||
1. **No Automatic Migration**: The new system starts fresh
|
||||
2. **Manual Migration**: Users can manually copy configuration values
|
||||
3. **Documentation**: Provide clear migration instructions
|
||||
4. **Coexistence**: Old and new systems use different database names
|
||||
|
||||
### Migration Steps for Users
|
||||
1. Stop existing relay
|
||||
2. Note current configuration values
|
||||
3. Start new relay (generates keys and new database)
|
||||
4. Create kind 33334 event with desired configuration using admin private key
|
||||
5. Send event to relay to update configuration
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests
|
||||
- Key generation functions
|
||||
- Configuration event creation and validation
|
||||
- Database naming logic
|
||||
- Configuration application handlers
|
||||
|
||||
### Integration Tests
|
||||
- Complete first-time startup flow
|
||||
- Configuration update via events
|
||||
- Error handling scenarios
|
||||
- Database operations
|
||||
|
||||
### Performance Tests
|
||||
- Startup time comparison
|
||||
- Configuration update response time
|
||||
- Memory usage analysis
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Admin Private Key**: Never stored, only printed once
|
||||
2. **Event Validation**: All configuration events must be signed by admin
|
||||
3. **Database Security**: Relay database contains relay private key
|
||||
4. **Key Generation**: Use `nostr_core_lib` for cryptographically secure generation
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### Major Changes
|
||||
- `src/main.c` - Startup logic, event processing, argument removal
|
||||
- `src/config.c` - Complete rewrite for event-based configuration
|
||||
- `src/config.h` - Update function signatures and structures
|
||||
- `src/sql_schema.h` - Remove config tables
|
||||
|
||||
### Minor Changes
|
||||
- `Makefile` - Remove any config file generation
|
||||
- `systemd/` - Update service files if needed
|
||||
- Documentation updates
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
**Breaking Changes:**
|
||||
- Command line arguments removed (except --help, --version)
|
||||
- Configuration files no longer used
|
||||
- Database naming scheme changed
|
||||
- Configuration table removed
|
||||
|
||||
**Migration Required:** This is a breaking change that requires manual migration for existing installations.
|
||||
|
||||
## Success Criteria - CURRENT STATUS
|
||||
|
||||
1. ✅ **Zero Command Line Arguments**: Relay starts with just `./c-relay`
|
||||
2. ✅ **Automatic First-Time Setup**: Generates keys and database automatically
|
||||
3. ⚠️ **Real-Time Configuration**: Infrastructure ready, handlers need completion
|
||||
4. ✅ **Single Database File**: All configuration and data in one `.nrdb` file
|
||||
5. ⚠️ **Admin Control**: Event processing implemented, signature validation ready
|
||||
6. ⚠️ **Clean Codebase**: Most legacy code removed, database tables cleanup pending
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
1. **Backup Strategy**: Document manual backup procedures for relay database
|
||||
2. **Key Loss Recovery**: Document recovery procedures if admin key is lost
|
||||
3. **Testing Coverage**: Comprehensive test suite before deployment
|
||||
4. **Rollback Plan**: Keep old version available during transition period
|
||||
5. **Documentation**: Comprehensive user and developer documentation
|
||||
|
||||
This implementation plan provides a clear path from the current system to the new event-based configuration architecture while maintaining security and reliability.
|
||||
298
docs/libwebsockets_proper_pattern.md
Normal file
298
docs/libwebsockets_proper_pattern.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Libwebsockets Proper Pattern - Message Queue Design
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
### Current Violation
|
||||
We're calling `lws_write()` directly from multiple code paths:
|
||||
1. **Event broadcast** (subscriptions.c:667) - when events arrive
|
||||
2. **OK responses** (websockets.c:855) - when processing EVENT messages
|
||||
3. **EOSE responses** (websockets.c:976) - when processing REQ messages
|
||||
4. **COUNT responses** (websockets.c:1922) - when processing COUNT messages
|
||||
|
||||
This violates libwebsockets' design pattern which requires:
|
||||
- **`lws_write()` ONLY called from `LWS_CALLBACK_SERVER_WRITEABLE`**
|
||||
- Application queues messages and requests writeable callback
|
||||
- Libwebsockets handles write timing and socket buffer management
|
||||
|
||||
### Consequences of Violation
|
||||
1. Partial writes when socket buffer is full
|
||||
2. Multiple concurrent write attempts before callback fires
|
||||
3. "write already pending" errors with single buffer
|
||||
4. Frame corruption from interleaved partial writes
|
||||
5. "Invalid frame header" errors on client side
|
||||
|
||||
## Correct Architecture
|
||||
|
||||
### Message Queue Pattern
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Event Arrives → Queue Message → Request Writeable Callback │
|
||||
│ REQ Received → Queue EOSE → Request Writeable Callback │
|
||||
│ EVENT Received→ Queue OK → Request Writeable Callback │
|
||||
│ COUNT Received→ Queue COUNT → Request Writeable Callback │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
lws_callback_on_writable(wsi)
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LWS_CALLBACK_SERVER_WRITEABLE │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Dequeue next message from queue │
|
||||
│ 2. Call lws_write() with message data │
|
||||
│ 3. If queue not empty, request another callback │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
libwebsockets handles:
|
||||
- Socket buffer management
|
||||
- Partial write handling
|
||||
- Frame atomicity
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
### Message Queue Node
|
||||
```c
|
||||
typedef struct message_queue_node {
|
||||
unsigned char* data; // Message data (with LWS_PRE space)
|
||||
size_t length; // Message length (without LWS_PRE)
|
||||
enum lws_write_protocol type; // LWS_WRITE_TEXT, etc.
|
||||
struct message_queue_node* next;
|
||||
} message_queue_node_t;
|
||||
```
|
||||
|
||||
### Per-Session Data Updates
|
||||
```c
|
||||
struct per_session_data {
|
||||
// ... existing fields ...
|
||||
|
||||
// Message queue (replaces single buffer)
|
||||
message_queue_node_t* message_queue_head;
|
||||
message_queue_node_t* message_queue_tail;
|
||||
int message_queue_count;
|
||||
int writeable_requested; // Flag to prevent duplicate requests
|
||||
};
|
||||
```
|
||||
|
||||
## Implementation Functions
|
||||
|
||||
### 1. Queue Message (Application Layer)
|
||||
```c
|
||||
int queue_message(struct lws* wsi, struct per_session_data* pss,
|
||||
const char* message, size_t length,
|
||||
enum lws_write_protocol type)
|
||||
{
|
||||
// Allocate node
|
||||
message_queue_node_t* node = malloc(sizeof(message_queue_node_t));
|
||||
|
||||
// Allocate buffer with LWS_PRE space
|
||||
node->data = malloc(LWS_PRE + length);
|
||||
memcpy(node->data + LWS_PRE, message, length);
|
||||
node->length = length;
|
||||
node->type = type;
|
||||
node->next = NULL;
|
||||
|
||||
// Add to queue (FIFO)
|
||||
pthread_mutex_lock(&pss->session_lock);
|
||||
if (!pss->message_queue_head) {
|
||||
pss->message_queue_head = node;
|
||||
pss->message_queue_tail = node;
|
||||
} else {
|
||||
pss->message_queue_tail->next = node;
|
||||
pss->message_queue_tail = node;
|
||||
}
|
||||
pss->message_queue_count++;
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
|
||||
// Request writeable callback (only if not already requested)
|
||||
if (!pss->writeable_requested) {
|
||||
pss->writeable_requested = 1;
|
||||
lws_callback_on_writable(wsi);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Process Queue (Writeable Callback)
|
||||
```c
|
||||
int process_message_queue(struct lws* wsi, struct per_session_data* pss)
|
||||
{
|
||||
pthread_mutex_lock(&pss->session_lock);
|
||||
|
||||
// Get next message from queue
|
||||
message_queue_node_t* node = pss->message_queue_head;
|
||||
if (!node) {
|
||||
pss->writeable_requested = 0;
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
return 0; // Queue empty
|
||||
}
|
||||
|
||||
// Remove from queue
|
||||
pss->message_queue_head = node->next;
|
||||
if (!pss->message_queue_head) {
|
||||
pss->message_queue_tail = NULL;
|
||||
}
|
||||
pss->message_queue_count--;
|
||||
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
|
||||
// Write message (libwebsockets handles partial writes)
|
||||
int result = lws_write(wsi, node->data + LWS_PRE, node->length, node->type);
|
||||
|
||||
// Free node
|
||||
free(node->data);
|
||||
free(node);
|
||||
|
||||
// If queue not empty, request another callback
|
||||
pthread_mutex_lock(&pss->session_lock);
|
||||
if (pss->message_queue_head) {
|
||||
lws_callback_on_writable(wsi);
|
||||
} else {
|
||||
pss->writeable_requested = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
|
||||
return (result < 0) ? -1 : 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Refactoring Changes
|
||||
|
||||
### Before (WRONG - Direct Write)
|
||||
```c
|
||||
// websockets.c:855 - OK response
|
||||
int write_result = lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
|
||||
if (write_result < 0) {
|
||||
DEBUG_ERROR("Write failed");
|
||||
} else if ((size_t)write_result != response_len) {
|
||||
// Partial write - queue remaining data
|
||||
queue_websocket_write(wsi, pss, ...);
|
||||
}
|
||||
```
|
||||
|
||||
### After (CORRECT - Queue Message)
|
||||
```c
|
||||
// websockets.c:855 - OK response
|
||||
queue_message(wsi, pss, response_str, response_len, LWS_WRITE_TEXT);
|
||||
// That's it! Writeable callback will handle the actual write
|
||||
```
|
||||
|
||||
### Before (WRONG - Direct Write in Broadcast)
|
||||
```c
|
||||
// subscriptions.c:667 - EVENT broadcast
|
||||
int write_result = lws_write(current_temp->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
||||
if (write_result < 0) {
|
||||
DEBUG_ERROR("Write failed");
|
||||
} else if ((size_t)write_result != msg_len) {
|
||||
queue_websocket_write(...);
|
||||
}
|
||||
```
|
||||
|
||||
### After (CORRECT - Queue Message)
|
||||
```c
|
||||
// subscriptions.c:667 - EVENT broadcast
|
||||
struct per_session_data* pss = lws_wsi_user(current_temp->wsi);
|
||||
queue_message(current_temp->wsi, pss, msg_str, msg_len, LWS_WRITE_TEXT);
|
||||
// Writeable callback will handle the actual write
|
||||
```
|
||||
|
||||
## Benefits of Correct Pattern
|
||||
|
||||
1. **No Partial Write Handling Needed**
|
||||
- Libwebsockets handles partial writes internally
|
||||
- We just queue complete messages
|
||||
|
||||
2. **No "Write Already Pending" Errors**
|
||||
- Queue can hold unlimited messages
|
||||
- Each processed sequentially from callback
|
||||
|
||||
3. **Thread Safety**
|
||||
- Queue operations protected by session lock
|
||||
- Write only from single callback thread
|
||||
|
||||
4. **Frame Atomicity**
|
||||
- Libwebsockets ensures complete frame transmission
|
||||
- No interleaved partial writes
|
||||
|
||||
5. **Simpler Code**
|
||||
- No complex partial write state machine
|
||||
- Just queue and forget
|
||||
|
||||
6. **Better Performance**
|
||||
- Libwebsockets optimizes write timing
|
||||
- Batches writes when socket ready
|
||||
|
||||
## Migration Steps
|
||||
|
||||
1. ✅ Identify all `lws_write()` call sites
|
||||
2. ✅ Confirm violation of libwebsockets pattern
|
||||
3. ⏳ Design message queue structure
|
||||
4. ⏳ Implement `queue_message()` function
|
||||
5. ⏳ Implement `process_message_queue()` function
|
||||
6. ⏳ Update `per_session_data` structure
|
||||
7. ⏳ Refactor OK response to use queue
|
||||
8. ⏳ Refactor EOSE response to use queue
|
||||
9. ⏳ Refactor COUNT response to use queue
|
||||
10. ⏳ Refactor EVENT broadcast to use queue
|
||||
11. ⏳ Update `LWS_CALLBACK_SERVER_WRITEABLE` handler
|
||||
12. ⏳ Add queue cleanup in `LWS_CALLBACK_CLOSED`
|
||||
13. ⏳ Remove old partial write code
|
||||
14. ⏳ Test with rapid multiple events
|
||||
15. ⏳ Test with large events (>4KB)
|
||||
16. ⏳ Test under load
|
||||
17. ⏳ Verify no frame errors
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test 1: Multiple Rapid Events
|
||||
```bash
|
||||
# Send 10 events rapidly to same client
|
||||
for i in {1..10}; do
|
||||
echo '["EVENT",{"kind":1,"content":"test'$i'","created_at":'$(date +%s)',...}]' | \
|
||||
websocat ws://localhost:8888 &
|
||||
done
|
||||
```
|
||||
|
||||
**Expected**: All events queued and sent sequentially, no errors
|
||||
|
||||
### Test 2: Large Events
|
||||
```bash
|
||||
# Send event >4KB (forces multiple socket writes)
|
||||
nak event --content "$(head -c 5000 /dev/urandom | base64)" | \
|
||||
websocat ws://localhost:8888
|
||||
```
|
||||
|
||||
**Expected**: Event queued, libwebsockets handles partial writes internally
|
||||
|
||||
### Test 3: Concurrent Connections
|
||||
```bash
|
||||
# 100 concurrent connections, each sending events
|
||||
for i in {1..100}; do
|
||||
(echo '["REQ","sub'$i'",{}]'; sleep 1) | websocat ws://localhost:8888 &
|
||||
done
|
||||
```
|
||||
|
||||
**Expected**: All subscriptions work, events broadcast correctly
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ✅ No `lws_write()` calls outside `LWS_CALLBACK_SERVER_WRITEABLE`
|
||||
- ✅ No "write already pending" errors in logs
|
||||
- ✅ No "Invalid frame header" errors on client side
|
||||
- ✅ All messages delivered in correct order
|
||||
- ✅ Large events (>4KB) handled correctly
|
||||
- ✅ Multiple rapid events to same client work
|
||||
- ✅ Concurrent connections stable under load
|
||||
|
||||
## References
|
||||
|
||||
- [libwebsockets documentation](https://libwebsockets.org/lws-api-doc-main/html/index.html)
|
||||
- [LWS_CALLBACK_SERVER_WRITEABLE](https://libwebsockets.org/lws-api-doc-main/html/group__callback-when-writeable.html)
|
||||
- [lws_callback_on_writable()](https://libwebsockets.org/lws-api-doc-main/html/group__callback-when-writeable.html#ga96f3ad8e1e2c3e0c8e0b0e5e5e5e5e5e)
|
||||
601
docs/monitoring_simplified_plan.md
Normal file
601
docs/monitoring_simplified_plan.md
Normal file
@@ -0,0 +1,601 @@
|
||||
# Simplified Monitoring Implementation Plan
|
||||
## Kind 34567 Event Kind Distribution Reporting
|
||||
|
||||
**Date:** 2025-10-16
|
||||
**Status:** Implementation Ready
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Simplified real-time monitoring system that:
|
||||
- Reports event kind distribution (which includes total event count)
|
||||
- Uses kind 34567 addressable events with `d=event_kinds`
|
||||
- Controlled by two config variables
|
||||
- Enabled on-demand when admin logs in
|
||||
- Uses simple throttling to prevent performance impact
|
||||
|
||||
---
|
||||
|
||||
## Configuration Variables
|
||||
|
||||
### Database Config Table
|
||||
|
||||
Add two new configuration keys:
|
||||
|
||||
```sql
|
||||
INSERT INTO config (key, value, data_type, description, category) VALUES
|
||||
('kind_34567_reporting_enabled', 'false', 'boolean',
|
||||
'Enable/disable kind 34567 event kind distribution reporting', 'monitoring'),
|
||||
('kind_34567_reporting_throttling_sec', '5', 'integer',
|
||||
'Minimum seconds between kind 34567 reports (throttling)', 'monitoring');
|
||||
```
|
||||
|
||||
### Configuration Access
|
||||
|
||||
```c
|
||||
// In src/monitoring.c or src/api.c
|
||||
int is_monitoring_enabled(void) {
|
||||
return get_config_bool("kind_34567_reporting_enabled", 0);
|
||||
}
|
||||
|
||||
int get_monitoring_throttle_seconds(void) {
|
||||
return get_config_int("kind_34567_reporting_throttling_sec", 5);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Structure
|
||||
|
||||
### Kind 34567 Event Format
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<event_id>",
|
||||
"pubkey": "<relay_pubkey>",
|
||||
"created_at": 1697123456,
|
||||
"kind": 34567,
|
||||
"content": "{\"data_type\":\"event_kinds\",\"timestamp\":1697123456,\"data\":{\"total_events\":125000,\"distribution\":[{\"kind\":1,\"count\":45000,\"percentage\":36.0},{\"kind\":3,\"count\":12500,\"percentage\":10.0}]}}",
|
||||
"tags": [
|
||||
["d", "event_kinds"],
|
||||
["relay", "<relay_pubkey>"]
|
||||
],
|
||||
"sig": "<signature>"
|
||||
}
|
||||
```
|
||||
|
||||
### Content JSON Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"data_type": "event_kinds",
|
||||
"timestamp": 1697123456,
|
||||
"data": {
|
||||
"total_events": 125000,
|
||||
"distribution": [
|
||||
{
|
||||
"kind": 1,
|
||||
"count": 45000,
|
||||
"percentage": 36.0
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"count": 12500,
|
||||
"percentage": 10.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"query_time_ms": 18
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
monitoring.h # New file - monitoring system header
|
||||
monitoring.c # New file - monitoring implementation
|
||||
main.c # Modified - add trigger hook
|
||||
config.c # Modified - add config keys (or use migration)
|
||||
```
|
||||
|
||||
### 1. Header File: `src/monitoring.h`
|
||||
|
||||
```c
|
||||
#ifndef MONITORING_H
|
||||
#define MONITORING_H
|
||||
|
||||
#include <time.h>
|
||||
#include <cjson/cJSON.h>
|
||||
|
||||
// Initialize monitoring system
|
||||
int init_monitoring_system(void);
|
||||
|
||||
// Cleanup monitoring system
|
||||
void cleanup_monitoring_system(void);
|
||||
|
||||
// Called when an event is stored (from main.c)
|
||||
void monitoring_on_event_stored(void);
|
||||
|
||||
// Enable/disable monitoring (called from admin API)
|
||||
int set_monitoring_enabled(int enabled);
|
||||
|
||||
// Get monitoring status
|
||||
int is_monitoring_enabled(void);
|
||||
|
||||
// Get throttle interval
|
||||
int get_monitoring_throttle_seconds(void);
|
||||
|
||||
#endif /* MONITORING_H */
|
||||
```
|
||||
|
||||
### 2. Implementation: `src/monitoring.c`
|
||||
|
||||
```c
|
||||
#include "monitoring.h"
|
||||
#include "config.h"
|
||||
#include "debug.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include <sqlite3.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
// External references
|
||||
extern sqlite3* g_db;
|
||||
extern int broadcast_event_to_subscriptions(cJSON* event);
|
||||
extern int store_event(cJSON* event);
|
||||
extern const char* get_config_value(const char* key);
|
||||
extern int get_config_bool(const char* key, int default_value);
|
||||
extern int get_config_int(const char* key, int default_value);
|
||||
extern char* get_relay_private_key(void);
|
||||
|
||||
// Throttling state
|
||||
static time_t last_report_time = 0;
|
||||
|
||||
// Initialize monitoring system
|
||||
int init_monitoring_system(void) {
|
||||
DEBUG_LOG("Monitoring system initialized");
|
||||
last_report_time = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Cleanup monitoring system
|
||||
void cleanup_monitoring_system(void) {
|
||||
DEBUG_LOG("Monitoring system cleaned up");
|
||||
}
|
||||
|
||||
// Check if monitoring is enabled
|
||||
int is_monitoring_enabled(void) {
|
||||
return get_config_bool("kind_34567_reporting_enabled", 0);
|
||||
}
|
||||
|
||||
// Get throttle interval
|
||||
int get_monitoring_throttle_seconds(void) {
|
||||
return get_config_int("kind_34567_reporting_throttling_sec", 5);
|
||||
}
|
||||
|
||||
// Enable/disable monitoring
|
||||
int set_monitoring_enabled(int enabled) {
|
||||
// Update config table
|
||||
const char* value = enabled ? "true" : "false";
|
||||
|
||||
// This would call update_config_in_table() or similar
|
||||
// For now, assume we have a function to update config
|
||||
extern int update_config_in_table(const char* key, const char* value);
|
||||
return update_config_in_table("kind_34567_reporting_enabled", value);
|
||||
}
|
||||
|
||||
// Query event kind distribution from database
|
||||
static char* query_event_kind_distribution(void) {
|
||||
if (!g_db) {
|
||||
DEBUG_ERROR("Database not available for monitoring query");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct timespec start_time;
|
||||
clock_gettime(CLOCK_MONOTONIC, &start_time);
|
||||
|
||||
// Query total events
|
||||
sqlite3_stmt* stmt;
|
||||
int total_events = 0;
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
total_events = sqlite3_column_int(stmt, 0);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Query kind distribution
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "data_type", "event_kinds");
|
||||
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
|
||||
|
||||
cJSON* data = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(data, "total_events", total_events);
|
||||
|
||||
cJSON* distribution = cJSON_CreateArray();
|
||||
|
||||
const char* sql =
|
||||
"SELECT kind, COUNT(*) as count, "
|
||||
"ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events), 2) as percentage "
|
||||
"FROM events GROUP BY kind ORDER BY count DESC";
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
cJSON* kind_obj = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(kind_obj, "kind", sqlite3_column_int(stmt, 0));
|
||||
cJSON_AddNumberToObject(kind_obj, "count", sqlite3_column_int64(stmt, 1));
|
||||
cJSON_AddNumberToObject(kind_obj, "percentage", sqlite3_column_double(stmt, 2));
|
||||
cJSON_AddItemToArray(distribution, kind_obj);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(data, "distribution", distribution);
|
||||
cJSON_AddItemToObject(response, "data", data);
|
||||
|
||||
// Calculate query time
|
||||
struct timespec end_time;
|
||||
clock_gettime(CLOCK_MONOTONIC, &end_time);
|
||||
double query_time_ms = (end_time.tv_sec - start_time.tv_sec) * 1000.0 +
|
||||
(end_time.tv_nsec - start_time.tv_nsec) / 1000000.0;
|
||||
|
||||
cJSON* metadata = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(metadata, "query_time_ms", query_time_ms);
|
||||
cJSON_AddItemToObject(response, "metadata", metadata);
|
||||
|
||||
char* json_string = cJSON_Print(response);
|
||||
cJSON_Delete(response);
|
||||
|
||||
return json_string;
|
||||
}
|
||||
|
||||
// Generate and broadcast kind 34567 event
|
||||
static int generate_monitoring_event(const char* json_content) {
|
||||
if (!json_content) return -1;
|
||||
|
||||
// Get relay keys
|
||||
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||||
char* relay_privkey_hex = get_relay_private_key();
|
||||
if (!relay_pubkey || !relay_privkey_hex) {
|
||||
if (relay_privkey_hex) free(relay_privkey_hex);
|
||||
DEBUG_ERROR("Could not get relay keys for monitoring event");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert relay private key to bytes
|
||||
unsigned char relay_privkey[32];
|
||||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||||
free(relay_privkey_hex);
|
||||
DEBUG_ERROR("Failed to convert relay private key");
|
||||
return -1;
|
||||
}
|
||||
free(relay_privkey_hex);
|
||||
|
||||
// Create tags array
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
|
||||
// d tag for addressable event
|
||||
cJSON* d_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("event_kinds"));
|
||||
cJSON_AddItemToArray(tags, d_tag);
|
||||
|
||||
// relay tag
|
||||
cJSON* relay_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay"));
|
||||
cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_pubkey));
|
||||
cJSON_AddItemToArray(tags, relay_tag);
|
||||
|
||||
// Create and sign event
|
||||
cJSON* event = nostr_create_and_sign_event(
|
||||
34567, // kind
|
||||
json_content, // content
|
||||
tags, // tags
|
||||
relay_privkey, // private key
|
||||
time(NULL) // timestamp
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
DEBUG_ERROR("Failed to create and sign monitoring event");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Broadcast to subscriptions
|
||||
broadcast_event_to_subscriptions(event);
|
||||
|
||||
// Store in database
|
||||
int result = store_event(event);
|
||||
|
||||
cJSON_Delete(event);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Called when an event is stored
|
||||
void monitoring_on_event_stored(void) {
|
||||
// Check if monitoring is enabled
|
||||
if (!is_monitoring_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check throttling
|
||||
time_t now = time(NULL);
|
||||
int throttle_seconds = get_monitoring_throttle_seconds();
|
||||
|
||||
if (now - last_report_time < throttle_seconds) {
|
||||
return; // Too soon, skip this update
|
||||
}
|
||||
|
||||
// Query event kind distribution
|
||||
char* json_content = query_event_kind_distribution();
|
||||
if (!json_content) {
|
||||
DEBUG_ERROR("Failed to query event kind distribution");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate and broadcast monitoring event
|
||||
int result = generate_monitoring_event(json_content);
|
||||
free(json_content);
|
||||
|
||||
if (result == 0) {
|
||||
last_report_time = now;
|
||||
DEBUG_LOG("Generated kind 34567 monitoring event");
|
||||
} else {
|
||||
DEBUG_ERROR("Failed to generate monitoring event");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Integration: Modify `src/main.c`
|
||||
|
||||
Add monitoring hook to event storage:
|
||||
|
||||
```c
|
||||
// At top of file
|
||||
#include "monitoring.h"
|
||||
|
||||
// In main() function, after init_database()
|
||||
if (init_monitoring_system() != 0) {
|
||||
DEBUG_WARN("Failed to initialize monitoring system");
|
||||
// Continue anyway - monitoring is optional
|
||||
}
|
||||
|
||||
// In store_event() function, after successful storage
|
||||
int store_event(cJSON* event) {
|
||||
// ... existing code ...
|
||||
|
||||
if (rc != SQLITE_DONE) {
|
||||
// ... error handling ...
|
||||
}
|
||||
|
||||
free(tags_json);
|
||||
|
||||
// Trigger monitoring update
|
||||
monitoring_on_event_stored();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// In cleanup section of main()
|
||||
cleanup_monitoring_system();
|
||||
```
|
||||
|
||||
### 4. Admin API: Enable/Disable Monitoring
|
||||
|
||||
Add admin command to enable monitoring (in `src/dm_admin.c` or `src/api.c`):
|
||||
|
||||
```c
|
||||
// Handle admin command to enable monitoring
|
||||
if (strcmp(command, "enable_monitoring") == 0) {
|
||||
set_monitoring_enabled(1);
|
||||
send_nip17_response(sender_pubkey,
|
||||
"✅ Kind 34567 monitoring enabled",
|
||||
error_msg, sizeof(error_msg));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle admin command to disable monitoring
|
||||
if (strcmp(command, "disable_monitoring") == 0) {
|
||||
set_monitoring_enabled(0);
|
||||
send_nip17_response(sender_pubkey,
|
||||
"🔴 Kind 34567 monitoring disabled",
|
||||
error_msg, sizeof(error_msg));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle admin command to set throttle interval
|
||||
if (strncmp(command, "set_monitoring_throttle ", 24) == 0) {
|
||||
int seconds = atoi(command + 24);
|
||||
if (seconds >= 1 && seconds <= 3600) {
|
||||
char value[16];
|
||||
snprintf(value, sizeof(value), "%d", seconds);
|
||||
update_config_in_table("kind_34567_reporting_throttling_sec", value);
|
||||
|
||||
char response[128];
|
||||
snprintf(response, sizeof(response),
|
||||
"✅ Monitoring throttle set to %d seconds", seconds);
|
||||
send_nip17_response(sender_pubkey, response, error_msg, sizeof(error_msg));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Admin Dashboard Subscription
|
||||
|
||||
```javascript
|
||||
// When admin logs in to dashboard
|
||||
async function enableMonitoring() {
|
||||
// Send admin command to enable monitoring
|
||||
await sendAdminCommand(['enable_monitoring']);
|
||||
|
||||
// Subscribe to kind 34567 events
|
||||
const subscription = {
|
||||
kinds: [34567],
|
||||
authors: [relayPubkey],
|
||||
"#d": ["event_kinds"]
|
||||
};
|
||||
|
||||
relay.subscribe([subscription], {
|
||||
onevent: (event) => {
|
||||
handleMonitoringEvent(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle incoming monitoring events
|
||||
function handleMonitoringEvent(event) {
|
||||
const content = JSON.parse(event.content);
|
||||
|
||||
if (content.data_type === 'event_kinds') {
|
||||
updateEventKindsChart(content.data);
|
||||
updateTotalEventsDisplay(content.data.total_events);
|
||||
}
|
||||
}
|
||||
|
||||
// When admin logs out or closes dashboard
|
||||
async function disableMonitoring() {
|
||||
await sendAdminCommand(['disable_monitoring']);
|
||||
}
|
||||
```
|
||||
|
||||
### Display Event Kind Distribution
|
||||
|
||||
```javascript
|
||||
function updateEventKindsChart(data) {
|
||||
const { total_events, distribution } = data;
|
||||
|
||||
// Update total events display
|
||||
document.getElementById('total-events').textContent =
|
||||
total_events.toLocaleString();
|
||||
|
||||
// Update chart/table with distribution
|
||||
const tableBody = document.getElementById('kind-distribution-table');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
distribution.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>Kind ${item.kind}</td>
|
||||
<td>${item.count.toLocaleString()}</td>
|
||||
<td>${item.percentage}%</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Migration
|
||||
|
||||
### Add to Schema or Migration Script
|
||||
|
||||
```sql
|
||||
-- Add monitoring configuration
|
||||
INSERT INTO config (key, value, data_type, description, category) VALUES
|
||||
('kind_34567_reporting_enabled', 'false', 'boolean',
|
||||
'Enable/disable kind 34567 event kind distribution reporting', 'monitoring'),
|
||||
('kind_34567_reporting_throttling_sec', '5', 'integer',
|
||||
'Minimum seconds between kind 34567 reports (throttling)', 'monitoring');
|
||||
```
|
||||
|
||||
Or add to existing config initialization in `src/config.c`.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Enable Monitoring
|
||||
|
||||
```bash
|
||||
# Via admin command (NIP-17 DM)
|
||||
echo '["enable_monitoring"]' | nak event --kind 14 --content - ws://localhost:8888
|
||||
```
|
||||
|
||||
### 2. Subscribe to Monitoring Events
|
||||
|
||||
```bash
|
||||
# Subscribe to kind 34567 events
|
||||
nak req --kinds 34567 --authors <relay_pubkey> ws://localhost:8888
|
||||
```
|
||||
|
||||
### 3. Generate Events
|
||||
|
||||
```bash
|
||||
# Send some test events to trigger monitoring
|
||||
for i in {1..10}; do
|
||||
nak event -c "Test event $i" ws://localhost:8888
|
||||
sleep 1
|
||||
done
|
||||
```
|
||||
|
||||
### 4. Verify Monitoring Events
|
||||
|
||||
You should see kind 34567 events every 5 seconds (or configured throttle interval) with event kind distribution.
|
||||
|
||||
---
|
||||
|
||||
## Performance Impact
|
||||
|
||||
### With 3 events/second (relay.damus.io scale)
|
||||
|
||||
**Query execution**:
|
||||
- Frequency: Every 5 seconds (throttled)
|
||||
- Query time: ~700ms (for 1M events)
|
||||
- Overhead: 700ms / 5000ms = 14% (acceptable)
|
||||
|
||||
**Per-event overhead**:
|
||||
- Check if enabled: < 0.01ms
|
||||
- Check throttle: < 0.01ms
|
||||
- Total: < 0.02ms per event (negligible)
|
||||
|
||||
**Overall impact**: < 1% on event processing, 14% on query thread (separate from event processing)
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Once this is working, easy to add:
|
||||
|
||||
1. **More data types**: Add `d=connections`, `d=subscriptions`, etc.
|
||||
2. **Materialized counters**: Optimize queries for very large databases
|
||||
3. **Historical data**: Store monitoring events for trending
|
||||
4. **Alerts**: Trigger on thresholds (e.g., > 90% capacity)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
This simplified plan provides:
|
||||
|
||||
✅ **Single data type**: Event kind distribution (includes total events)
|
||||
✅ **Two config variables**: Enable/disable and throttle control
|
||||
✅ **On-demand activation**: Enabled when admin logs in
|
||||
✅ **Simple throttling**: Prevents performance impact
|
||||
✅ **Clean implementation**: ~200 lines of code
|
||||
✅ **Easy to extend**: Add more data types later
|
||||
|
||||
**Estimated implementation time**: 4-6 hours
|
||||
|
||||
**Files to create/modify**:
|
||||
- Create: `src/monitoring.h` (~30 lines)
|
||||
- Create: `src/monitoring.c` (~200 lines)
|
||||
- Modify: `src/main.c` (~10 lines)
|
||||
- Modify: `src/config.c` or migration (~5 lines)
|
||||
- Modify: `src/dm_admin.c` or `src/api.c` (~30 lines)
|
||||
- Create: `api/monitoring.js` (frontend, ~100 lines)
|
||||
|
||||
**Total new code**: ~375 lines
|
||||
517
docs/nip59_timestamp_configuration_plan.md
Normal file
517
docs/nip59_timestamp_configuration_plan.md
Normal file
@@ -0,0 +1,517 @@
|
||||
# NIP-59 Timestamp Configuration Implementation Plan
|
||||
|
||||
## Overview
|
||||
Add configurable timestamp randomization for NIP-59 gift wraps to improve compatibility with Nostr apps that don't implement timestamp randomization.
|
||||
|
||||
## Problem Statement
|
||||
The NIP-59 protocol specifies that timestamps on gift wraps should have randomness to prevent time-analysis attacks. However, some Nostr platforms don't implement this, causing compatibility issues with direct messaging (NIP-17).
|
||||
|
||||
## Solution
|
||||
Add a configuration parameter `nip59_timestamp_max_delay_sec` that controls the maximum random delay applied to timestamps:
|
||||
- **Value = 0**: Use current timestamp (no randomization) for maximum compatibility
|
||||
- **Value > 0**: Use random timestamp between now and N seconds ago
|
||||
- **Default = 0**: Maximum compatibility mode (no randomization)
|
||||
|
||||
## Implementation Approach: Option B (Direct Parameter Addition)
|
||||
We chose Option B because:
|
||||
1. Explicit and stateless - value flows through call chain
|
||||
2. Thread-safe by design
|
||||
3. No global state needed in nostr_core_lib
|
||||
4. DMs are sent rarely, so database query per call is acceptable
|
||||
|
||||
---
|
||||
|
||||
## Detailed Implementation Steps
|
||||
|
||||
### Phase 1: Configuration Setup in c-relay
|
||||
|
||||
#### 1.1 Add Configuration Parameter
|
||||
**File:** `src/default_config_event.h`
|
||||
**Location:** Line 82 (after `trust_proxy_headers`)
|
||||
|
||||
```c
|
||||
// NIP-59 Gift Wrap Timestamp Configuration
|
||||
{"nip59_timestamp_max_delay_sec", "0"} // Default: 0 (no randomization for compatibility)
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Default of 0 seconds (no randomization) for maximum compatibility
|
||||
- Placed after proxy settings, before closing brace
|
||||
- Follows existing naming convention
|
||||
|
||||
#### 1.2 Add Configuration Validation
|
||||
**File:** `src/config.c`
|
||||
**Function:** `validate_config_field()` (around line 923)
|
||||
|
||||
Add validation case:
|
||||
```c
|
||||
else if (strcmp(key, "nip59_timestamp_max_delay_sec") == 0) {
|
||||
long value = strtol(value_str, NULL, 10);
|
||||
if (value < 0 || value > 604800) { // Max 7 days
|
||||
snprintf(error_msg, error_size,
|
||||
"nip59_timestamp_max_delay_sec must be between 0 and 604800 (7 days)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- 0 = no randomization (compatibility mode)
|
||||
- 604800 = 7 days maximum (reasonable upper bound)
|
||||
- Prevents negative values or excessive delays
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Modify nostr_core_lib Functions
|
||||
|
||||
#### 2.1 Update random_past_timestamp() Function
|
||||
**File:** `nostr_core_lib/nostr_core/nip059.c`
|
||||
**Current Location:** Lines 31-36
|
||||
|
||||
**Current Code:**
|
||||
```c
|
||||
static time_t random_past_timestamp(void) {
|
||||
time_t now = time(NULL);
|
||||
// Random time up to 2 days (172800 seconds) in the past
|
||||
long random_offset = (long)(rand() % 172800);
|
||||
return now - random_offset;
|
||||
}
|
||||
```
|
||||
|
||||
**New Code:**
|
||||
```c
|
||||
static time_t random_past_timestamp(long max_delay_sec) {
|
||||
time_t now = time(NULL);
|
||||
|
||||
// If max_delay_sec is 0, return current timestamp (no randomization)
|
||||
if (max_delay_sec == 0) {
|
||||
return now;
|
||||
}
|
||||
|
||||
// Random time up to max_delay_sec in the past
|
||||
long random_offset = (long)(rand() % max_delay_sec);
|
||||
return now - random_offset;
|
||||
}
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- Add `long max_delay_sec` parameter
|
||||
- Handle special case: `max_delay_sec == 0` returns current time
|
||||
- Use `max_delay_sec` instead of hardcoded 172800
|
||||
|
||||
#### 2.2 Update nostr_nip59_create_seal() Function
|
||||
**File:** `nostr_core_lib/nostr_core/nip059.c`
|
||||
**Current Location:** Lines 144-215
|
||||
|
||||
**Function Signature Change:**
|
||||
```c
|
||||
// OLD:
|
||||
cJSON* nostr_nip59_create_seal(cJSON* rumor,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key);
|
||||
|
||||
// NEW:
|
||||
cJSON* nostr_nip59_create_seal(cJSON* rumor,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key,
|
||||
long max_delay_sec);
|
||||
```
|
||||
|
||||
**Code Change at Line 181:**
|
||||
```c
|
||||
// OLD:
|
||||
time_t seal_time = random_past_timestamp();
|
||||
|
||||
// NEW:
|
||||
time_t seal_time = random_past_timestamp(max_delay_sec);
|
||||
```
|
||||
|
||||
#### 2.3 Update nostr_nip59_create_gift_wrap() Function
|
||||
**File:** `nostr_core_lib/nostr_core/nip059.c`
|
||||
**Current Location:** Lines 220-323
|
||||
|
||||
**Function Signature Change:**
|
||||
```c
|
||||
// OLD:
|
||||
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal,
|
||||
const char* recipient_public_key_hex);
|
||||
|
||||
// NEW:
|
||||
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal,
|
||||
const char* recipient_public_key_hex,
|
||||
long max_delay_sec);
|
||||
```
|
||||
|
||||
**Code Change at Line 275:**
|
||||
```c
|
||||
// OLD:
|
||||
time_t wrap_time = random_past_timestamp();
|
||||
|
||||
// NEW:
|
||||
time_t wrap_time = random_past_timestamp(max_delay_sec);
|
||||
```
|
||||
|
||||
#### 2.4 Update nip059.h Header
|
||||
**File:** `nostr_core_lib/nostr_core/nip059.h`
|
||||
**Locations:** Lines 38-39 and 48
|
||||
|
||||
**Update Function Declarations:**
|
||||
```c
|
||||
// Line 38-39: Update nostr_nip59_create_seal
|
||||
cJSON* nostr_nip59_create_seal(cJSON* rumor,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key,
|
||||
long max_delay_sec);
|
||||
|
||||
// Line 48: Update nostr_nip59_create_gift_wrap
|
||||
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal,
|
||||
const char* recipient_public_key_hex,
|
||||
long max_delay_sec);
|
||||
```
|
||||
|
||||
**Update Documentation Comments:**
|
||||
```c
|
||||
/**
|
||||
* NIP-59: Create a seal (kind 13) wrapping a rumor
|
||||
*
|
||||
* @param rumor The rumor event to seal (cJSON object)
|
||||
* @param sender_private_key 32-byte sender private key
|
||||
* @param recipient_public_key 32-byte recipient public key (x-only)
|
||||
* @param max_delay_sec Maximum random delay in seconds (0 = no randomization)
|
||||
* @return cJSON object representing the seal event, or NULL on error
|
||||
*/
|
||||
|
||||
/**
|
||||
* NIP-59: Create a gift wrap (kind 1059) wrapping a seal
|
||||
*
|
||||
* @param seal The seal event to wrap (cJSON object)
|
||||
* @param recipient_public_key_hex Recipient's public key in hex format
|
||||
* @param max_delay_sec Maximum random delay in seconds (0 = no randomization)
|
||||
* @return cJSON object representing the gift wrap event, or NULL on error
|
||||
*/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Update NIP-17 Integration
|
||||
|
||||
#### 3.1 Update nostr_nip17_send_dm() Function
|
||||
**File:** `nostr_core_lib/nostr_core/nip017.c`
|
||||
**Current Location:** Lines 260-320
|
||||
|
||||
**Function Signature Change:**
|
||||
```c
|
||||
// OLD:
|
||||
int nostr_nip17_send_dm(cJSON* dm_event,
|
||||
const char** recipient_pubkeys,
|
||||
int num_recipients,
|
||||
const unsigned char* sender_private_key,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps);
|
||||
|
||||
// NEW:
|
||||
int nostr_nip17_send_dm(cJSON* dm_event,
|
||||
const char** recipient_pubkeys,
|
||||
int num_recipients,
|
||||
const unsigned char* sender_private_key,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec);
|
||||
```
|
||||
|
||||
**Code Changes:**
|
||||
|
||||
At line 281 (seal creation):
|
||||
```c
|
||||
// OLD:
|
||||
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key);
|
||||
|
||||
// NEW:
|
||||
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key, max_delay_sec);
|
||||
```
|
||||
|
||||
At line 287 (gift wrap creation):
|
||||
```c
|
||||
// OLD:
|
||||
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i]);
|
||||
|
||||
// NEW:
|
||||
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i], max_delay_sec);
|
||||
```
|
||||
|
||||
At line 306 (sender seal creation):
|
||||
```c
|
||||
// OLD:
|
||||
cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key);
|
||||
|
||||
// NEW:
|
||||
cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key, max_delay_sec);
|
||||
```
|
||||
|
||||
At line 309 (sender gift wrap creation):
|
||||
```c
|
||||
// OLD:
|
||||
cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex);
|
||||
|
||||
// NEW:
|
||||
cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex, max_delay_sec);
|
||||
```
|
||||
|
||||
#### 3.2 Update nip017.h Header
|
||||
**File:** `nostr_core_lib/nostr_core/nip017.h`
|
||||
**Location:** Lines 102-107
|
||||
|
||||
**Update Function Declaration:**
|
||||
```c
|
||||
int nostr_nip17_send_dm(cJSON* dm_event,
|
||||
const char** recipient_pubkeys,
|
||||
int num_recipients,
|
||||
const unsigned char* sender_private_key,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec);
|
||||
```
|
||||
|
||||
**Update Documentation Comment (lines 88-100):**
|
||||
```c
|
||||
/**
|
||||
* NIP-17: Send a direct message to recipients
|
||||
*
|
||||
* This function creates the appropriate rumor, seals it, gift wraps it,
|
||||
* and returns the final gift wrap events ready for publishing.
|
||||
*
|
||||
* @param dm_event The unsigned DM event (kind 14 or 15)
|
||||
* @param recipient_pubkeys Array of recipient public keys (hex strings)
|
||||
* @param num_recipients Number of recipients
|
||||
* @param sender_private_key 32-byte sender private key
|
||||
* @param gift_wraps_out Array to store resulting gift wrap events (caller must free)
|
||||
* @param max_gift_wraps Maximum number of gift wraps to create
|
||||
* @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization)
|
||||
* @return Number of gift wrap events created, or -1 on error
|
||||
*/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Update c-relay Call Sites
|
||||
|
||||
#### 4.1 Update src/api.c
|
||||
**Location:** Line 1319
|
||||
|
||||
**Current Code:**
|
||||
```c
|
||||
int send_result = nostr_nip17_send_dm(
|
||||
dm_response, // dm_event
|
||||
recipient_pubkeys, // recipient_pubkeys
|
||||
1, // num_recipients
|
||||
relay_privkey, // sender_private_key
|
||||
gift_wraps, // gift_wraps_out
|
||||
1 // max_gift_wraps
|
||||
);
|
||||
```
|
||||
|
||||
**New Code:**
|
||||
```c
|
||||
// Get timestamp delay configuration
|
||||
long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0);
|
||||
|
||||
int send_result = nostr_nip17_send_dm(
|
||||
dm_response, // dm_event
|
||||
recipient_pubkeys, // recipient_pubkeys
|
||||
1, // num_recipients
|
||||
relay_privkey, // sender_private_key
|
||||
gift_wraps, // gift_wraps_out
|
||||
1, // max_gift_wraps
|
||||
max_delay_sec // max_delay_sec
|
||||
);
|
||||
```
|
||||
|
||||
#### 4.2 Update src/dm_admin.c
|
||||
**Location:** Line 371
|
||||
|
||||
**Current Code:**
|
||||
```c
|
||||
int send_result = nostr_nip17_send_dm(
|
||||
success_dm, // dm_event
|
||||
sender_pubkey_array, // recipient_pubkeys
|
||||
1, // num_recipients
|
||||
relay_privkey, // sender_private_key
|
||||
success_gift_wraps, // gift_wraps_out
|
||||
1 // max_gift_wraps
|
||||
);
|
||||
```
|
||||
|
||||
**New Code:**
|
||||
```c
|
||||
// Get timestamp delay configuration
|
||||
long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0);
|
||||
|
||||
int send_result = nostr_nip17_send_dm(
|
||||
success_dm, // dm_event
|
||||
sender_pubkey_array, // recipient_pubkeys
|
||||
1, // num_recipients
|
||||
relay_privkey, // sender_private_key
|
||||
success_gift_wraps, // gift_wraps_out
|
||||
1, // max_gift_wraps
|
||||
max_delay_sec // max_delay_sec
|
||||
);
|
||||
```
|
||||
|
||||
**Note:** Both files already include `config.h`, so `get_config_int()` is available.
|
||||
|
||||
---
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Test Case 1: No Randomization (Compatibility Mode)
|
||||
**Configuration:** `nip59_timestamp_max_delay_sec = 0`
|
||||
|
||||
**Expected Behavior:**
|
||||
- Gift wrap timestamps should equal current time
|
||||
- Seal timestamps should equal current time
|
||||
- No random delay applied
|
||||
|
||||
**Test Command:**
|
||||
```bash
|
||||
# Set config via admin API
|
||||
# Send test DM
|
||||
# Verify timestamps are current (within 1 second of send time)
|
||||
```
|
||||
|
||||
### Test Case 2: Custom Delay
|
||||
**Configuration:** `nip59_timestamp_max_delay_sec = 1000`
|
||||
|
||||
**Expected Behavior:**
|
||||
- Gift wrap timestamps should be between now and 1000 seconds ago
|
||||
- Seal timestamps should be between now and 1000 seconds ago
|
||||
- Random delay applied within specified range
|
||||
|
||||
**Test Command:**
|
||||
```bash
|
||||
# Set config via admin API
|
||||
# Send test DM
|
||||
# Verify timestamps are in past but within 1000 seconds
|
||||
```
|
||||
|
||||
### Test Case 3: Default Behavior
|
||||
**Configuration:** `nip59_timestamp_max_delay_sec = 0` (default)
|
||||
|
||||
**Expected Behavior:**
|
||||
- Gift wrap timestamps should equal current time
|
||||
- Seal timestamps should equal current time
|
||||
- No randomization (maximum compatibility)
|
||||
|
||||
**Test Command:**
|
||||
```bash
|
||||
# Use default config
|
||||
# Send test DM
|
||||
# Verify timestamps are current (within 1 second of send time)
|
||||
```
|
||||
|
||||
### Test Case 4: Configuration Validation
|
||||
**Test Invalid Values:**
|
||||
- Negative value: Should be rejected
|
||||
- Value > 604800: Should be rejected
|
||||
- Valid boundary values (0, 604800): Should be accepted
|
||||
|
||||
### Test Case 5: Interoperability
|
||||
**Test with Other Nostr Clients:**
|
||||
- Send DM with `max_delay_sec = 0` to clients that don't randomize
|
||||
- Send DM with `max_delay_sec = 172800` to clients that do randomize
|
||||
- Verify both scenarios work correctly
|
||||
|
||||
---
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
### Update docs/configuration_guide.md
|
||||
|
||||
Add new section:
|
||||
|
||||
```markdown
|
||||
### NIP-59 Gift Wrap Timestamp Configuration
|
||||
|
||||
#### nip59_timestamp_max_delay_sec
|
||||
- **Type:** Integer
|
||||
- **Default:** 0 (no randomization)
|
||||
- **Range:** 0 to 604800 (7 days)
|
||||
- **Description:** Controls timestamp randomization for NIP-59 gift wraps
|
||||
|
||||
The NIP-59 protocol recommends randomizing timestamps on gift wraps to prevent
|
||||
time-analysis attacks. However, some Nostr platforms don't implement this,
|
||||
causing compatibility issues.
|
||||
|
||||
**Values:**
|
||||
- `0` (default): No randomization - uses current timestamp (maximum compatibility)
|
||||
- `1-604800`: Random timestamp between now and N seconds ago
|
||||
|
||||
**Use Cases:**
|
||||
- Keep default `0` for maximum compatibility with clients that don't randomize
|
||||
- Set to `172800` for privacy per NIP-59 specification (2 days randomization)
|
||||
- Set to custom value (e.g., `3600`) for 1-hour randomization window
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
["nip59_timestamp_max_delay_sec", "0"] // Default: compatibility mode
|
||||
["nip59_timestamp_max_delay_sec", "3600"] // 1 hour randomization
|
||||
["nip59_timestamp_max_delay_sec", "172800"] // 2 days randomization
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### nostr_core_lib Changes
|
||||
- [ ] Modify `random_past_timestamp()` to accept `max_delay_sec` parameter
|
||||
- [ ] Update `nostr_nip59_create_seal()` signature and implementation
|
||||
- [ ] Update `nostr_nip59_create_gift_wrap()` signature and implementation
|
||||
- [ ] Update `nip059.h` function declarations and documentation
|
||||
- [ ] Update `nostr_nip17_send_dm()` signature and implementation
|
||||
- [ ] Update `nip017.h` function declaration and documentation
|
||||
|
||||
### c-relay Changes
|
||||
- [ ] Add `nip59_timestamp_max_delay_sec` to `default_config_event.h`
|
||||
- [ ] Add validation in `config.c` for new parameter
|
||||
- [ ] Update `src/api.c` call site to pass `max_delay_sec`
|
||||
- [ ] Update `src/dm_admin.c` call site to pass `max_delay_sec`
|
||||
|
||||
### Testing
|
||||
- [ ] Test with `max_delay_sec = 0` (no randomization)
|
||||
- [ ] Test with `max_delay_sec = 1000` (custom delay)
|
||||
- [ ] Test with `max_delay_sec = 172800` (default behavior)
|
||||
- [ ] Test configuration validation (invalid values)
|
||||
- [ ] Test interoperability with other Nostr clients
|
||||
|
||||
### Documentation
|
||||
- [ ] Update `docs/configuration_guide.md`
|
||||
- [ ] Add this implementation plan to docs
|
||||
- [ ] Update README if needed
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise:
|
||||
1. Revert nostr_core_lib changes (git revert in submodule)
|
||||
2. Revert c-relay changes
|
||||
3. Configuration parameter will be ignored if not used
|
||||
4. Default behavior (0) provides maximum compatibility
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The configuration is read on each DM send, allowing runtime changes
|
||||
- No restart required when changing `nip59_timestamp_max_delay_sec`
|
||||
- Thread-safe by design (no global state)
|
||||
- Default value of 0 provides maximum compatibility with other Nostr clients
|
||||
- Can be changed to 172800 or other values for NIP-59 privacy features
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [NIP-59: Gift Wrap](https://github.com/nostr-protocol/nips/blob/master/59.md)
|
||||
- [NIP-17: Private Direct Messages](https://github.com/nostr-protocol/nips/blob/master/17.md)
|
||||
- [NIP-44: Versioned Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
|
||||
1189
docs/realtime_monitoring_design.md
Normal file
1189
docs/realtime_monitoring_design.md
Normal file
File diff suppressed because it is too large
Load Diff
325
docs/relay_traffic_measurement.md
Normal file
325
docs/relay_traffic_measurement.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# Relay Traffic Measurement Guide
|
||||
|
||||
## Measuring Real-World Relay Traffic
|
||||
|
||||
To validate our performance assumptions, here are commands to measure actual event rates from live relays.
|
||||
|
||||
---
|
||||
|
||||
## Command: Count Events Over 1 Minute
|
||||
|
||||
### Basic Command
|
||||
|
||||
```bash
|
||||
# Count events from relay.damus.io over 60 seconds
|
||||
timeout 60 nak req -s $(date +%s) --stream wss://relay.damus.io | wc -l
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Subscribe to all new events (`-s $(date +%s)` = since now)
|
||||
2. Stream for 60 seconds (`timeout 60`)
|
||||
3. Count the lines (each line = 1 event)
|
||||
|
||||
### With Event Rate Display
|
||||
|
||||
```bash
|
||||
# Show events per second in real-time
|
||||
timeout 60 nak req -s $(date +%s) --stream wss://relay.damus.io | \
|
||||
pv -l -i 1 -r > /dev/null
|
||||
```
|
||||
|
||||
This displays:
|
||||
- Total events received
|
||||
- Current rate (events/second)
|
||||
- Average rate
|
||||
|
||||
### With Detailed Statistics
|
||||
|
||||
```bash
|
||||
# Count events and calculate statistics
|
||||
echo "Measuring relay traffic for 60 seconds..."
|
||||
START=$(date +%s)
|
||||
COUNT=$(timeout 60 nak req -s $START --stream wss://relay.damus.io | wc -l)
|
||||
END=$(date +%s)
|
||||
DURATION=$((END - START))
|
||||
|
||||
echo "Results:"
|
||||
echo " Total events: $COUNT"
|
||||
echo " Duration: ${DURATION}s"
|
||||
echo " Events/second: $(echo "scale=2; $COUNT / $DURATION" | bc)"
|
||||
echo " Events/minute: $COUNT"
|
||||
```
|
||||
|
||||
### With Event Kind Distribution
|
||||
|
||||
```bash
|
||||
# Count events by kind over 60 seconds
|
||||
timeout 60 nak req -s $(date +%s) --stream wss://relay.damus.io | \
|
||||
jq -r '.kind' | \
|
||||
sort | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
Output example:
|
||||
```
|
||||
45 1 # 45 text notes
|
||||
12 3 # 12 contact lists
|
||||
8 7 # 8 reactions
|
||||
3 6 # 3 reposts
|
||||
```
|
||||
|
||||
### With Timestamp Analysis
|
||||
|
||||
```bash
|
||||
# Show event timestamps and calculate intervals
|
||||
timeout 60 nak req -s $(date +%s) --stream wss://relay.damus.io | \
|
||||
jq -r '.created_at' | \
|
||||
awk 'NR>1 {print $1-prev} {prev=$1}' | \
|
||||
awk '{sum+=$1; count++} END {
|
||||
print "Average interval:", sum/count, "seconds"
|
||||
print "Events per second:", count/sum
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Multiple Relays
|
||||
|
||||
### Compare Traffic Across Relays
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# test_relay_traffic.sh
|
||||
|
||||
RELAYS=(
|
||||
"wss://relay.damus.io"
|
||||
"wss://nos.lol"
|
||||
"wss://relay.nostr.band"
|
||||
"wss://nostr.wine"
|
||||
)
|
||||
|
||||
DURATION=60
|
||||
|
||||
echo "Measuring relay traffic for ${DURATION} seconds..."
|
||||
echo ""
|
||||
|
||||
for relay in "${RELAYS[@]}"; do
|
||||
echo "Testing: $relay"
|
||||
count=$(timeout $DURATION nak req -s $(date +%s) --stream "$relay" 2>/dev/null | wc -l)
|
||||
rate=$(echo "scale=2; $count / $DURATION" | bc)
|
||||
echo " Events: $count"
|
||||
echo " Rate: ${rate}/sec"
|
||||
echo ""
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected Results (Based on Real Measurements)
|
||||
|
||||
### relay.damus.io (Large Public Relay)
|
||||
- **Expected rate**: 0.5-2 events/second
|
||||
- **60-second count**: 30-120 events
|
||||
- **Peak times**: Higher during US daytime hours
|
||||
|
||||
### nos.lol (Medium Public Relay)
|
||||
- **Expected rate**: 0.2-0.8 events/second
|
||||
- **60-second count**: 12-48 events
|
||||
|
||||
### Personal/Small Relays
|
||||
- **Expected rate**: 0.01-0.1 events/second
|
||||
- **60-second count**: 1-6 events
|
||||
|
||||
---
|
||||
|
||||
## Using Results to Validate Performance Assumptions
|
||||
|
||||
After measuring your relay's traffic:
|
||||
|
||||
1. **Calculate average events/second**:
|
||||
```
|
||||
events_per_second = total_events / 60
|
||||
```
|
||||
|
||||
2. **Estimate query overhead**:
|
||||
```
|
||||
# For 100k event database:
|
||||
query_time = 70ms
|
||||
overhead_percentage = (query_time * events_per_second) / 1000 * 100
|
||||
|
||||
# Example: 0.5 events/sec
|
||||
overhead = (70 * 0.5) / 1000 * 100 = 3.5%
|
||||
```
|
||||
|
||||
3. **Determine if optimization needed**:
|
||||
- < 5% overhead: No optimization needed
|
||||
- 5-20% overhead: Consider 1-second throttling
|
||||
- > 20% overhead: Use materialized counters
|
||||
|
||||
---
|
||||
|
||||
## Real-Time Monitoring During Development
|
||||
|
||||
### Monitor Your Own Relay
|
||||
|
||||
```bash
|
||||
# Watch events in real-time with count
|
||||
nak req -s $(date +%s) --stream ws://localhost:8888 | \
|
||||
awk '{count++; print count, $0}'
|
||||
```
|
||||
|
||||
### Monitor with Event Details
|
||||
|
||||
```bash
|
||||
# Show event kind and pubkey for each event
|
||||
nak req -s $(date +%s) --stream ws://localhost:8888 | \
|
||||
jq -r '"[\(.kind)] \(.pubkey[0:8])... \(.content[0:50])"'
|
||||
```
|
||||
|
||||
### Continuous Traffic Monitoring
|
||||
|
||||
```bash
|
||||
# Monitor traffic in 10-second windows
|
||||
while true; do
|
||||
echo "=== $(date) ==="
|
||||
count=$(timeout 10 nak req -s $(date +%s) --stream ws://localhost:8888 | wc -l)
|
||||
rate=$(echo "scale=2; $count / 10" | bc)
|
||||
echo "Events: $count (${rate}/sec)"
|
||||
sleep 1
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Testing Commands
|
||||
|
||||
### Simulate Load
|
||||
|
||||
```bash
|
||||
# Send test events to measure query performance
|
||||
for i in {1..100}; do
|
||||
nak event -c "Test event $i" ws://localhost:8888
|
||||
sleep 0.1 # 10 events/second
|
||||
done
|
||||
```
|
||||
|
||||
### Measure Query Response Time
|
||||
|
||||
```bash
|
||||
# Time how long queries take with current database
|
||||
time sqlite3 your_relay.db "SELECT COUNT(*) FROM events"
|
||||
time sqlite3 your_relay.db "SELECT kind, COUNT(*) FROM events GROUP BY kind"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automated Traffic Analysis Script
|
||||
|
||||
Save this as `analyze_relay_traffic.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Comprehensive relay traffic analysis
|
||||
|
||||
RELAY="${1:-ws://localhost:8888}"
|
||||
DURATION="${2:-60}"
|
||||
|
||||
echo "Analyzing relay: $RELAY"
|
||||
echo "Duration: ${DURATION} seconds"
|
||||
echo ""
|
||||
|
||||
# Collect events
|
||||
TMPFILE=$(mktemp)
|
||||
timeout $DURATION nak req -s $(date +%s) --stream "$RELAY" > "$TMPFILE" 2>/dev/null
|
||||
|
||||
# Calculate statistics
|
||||
TOTAL=$(wc -l < "$TMPFILE")
|
||||
RATE=$(echo "scale=2; $TOTAL / $DURATION" | bc)
|
||||
|
||||
echo "=== Traffic Statistics ==="
|
||||
echo "Total events: $TOTAL"
|
||||
echo "Events/second: $RATE"
|
||||
echo "Events/minute: $(echo "$TOTAL * 60 / $DURATION" | bc)"
|
||||
echo ""
|
||||
|
||||
echo "=== Event Kind Distribution ==="
|
||||
jq -r '.kind' "$TMPFILE" | sort | uniq -c | sort -rn | head -10
|
||||
echo ""
|
||||
|
||||
echo "=== Top Publishers ==="
|
||||
jq -r '.pubkey[0:16]' "$TMPFILE" | sort | uniq -c | sort -rn | head -5
|
||||
echo ""
|
||||
|
||||
echo "=== Performance Estimate ==="
|
||||
echo "For 100k event database:"
|
||||
echo " Query time: ~70ms"
|
||||
echo " Overhead: $(echo "scale=2; 70 * $RATE / 10" | bc)%"
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
rm "$TMPFILE"
|
||||
```
|
||||
|
||||
Usage:
|
||||
```bash
|
||||
chmod +x analyze_relay_traffic.sh
|
||||
./analyze_relay_traffic.sh wss://relay.damus.io 60
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interpreting Results
|
||||
|
||||
### Low Traffic (< 0.1 events/sec)
|
||||
- **Typical for**: Personal relays, small communities
|
||||
- **Recommendation**: Trigger on every event, no optimization
|
||||
- **Expected overhead**: < 1%
|
||||
|
||||
### Medium Traffic (0.1-0.5 events/sec)
|
||||
- **Typical for**: Medium public relays
|
||||
- **Recommendation**: Trigger on every event, consider throttling if database > 100k
|
||||
- **Expected overhead**: 1-5%
|
||||
|
||||
### High Traffic (0.5-2 events/sec)
|
||||
- **Typical for**: Large public relays
|
||||
- **Recommendation**: Use 1-second throttling
|
||||
- **Expected overhead**: 5-20% without throttling, < 1% with throttling
|
||||
|
||||
### Very High Traffic (> 2 events/sec)
|
||||
- **Typical for**: Major public relays (rare)
|
||||
- **Recommendation**: Use materialized counters
|
||||
- **Expected overhead**: > 20% without optimization
|
||||
|
||||
---
|
||||
|
||||
## Continuous Monitoring in Production
|
||||
|
||||
### Add to Relay Startup
|
||||
|
||||
```bash
|
||||
# In your relay startup script
|
||||
echo "Starting traffic monitoring..."
|
||||
nohup bash -c 'while true; do
|
||||
count=$(timeout 60 nak req -s $(date +%s) --stream ws://localhost:8888 2>/dev/null | wc -l)
|
||||
echo "$(date +%Y-%m-%d\ %H:%M:%S) - Events/min: $count" >> traffic.log
|
||||
done' &
|
||||
```
|
||||
|
||||
### Analyze Historical Traffic
|
||||
|
||||
```bash
|
||||
# View traffic trends
|
||||
cat traffic.log | awk '{print $4}' | \
|
||||
awk '{sum+=$1; count++} END {print "Average:", sum/count, "events/min"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Use these commands to:
|
||||
1. ✅ Measure real-world traffic on your relay
|
||||
2. ✅ Validate performance assumptions
|
||||
3. ✅ Determine if optimization is needed
|
||||
4. ✅ Monitor traffic trends over time
|
||||
|
||||
**Remember**: Most relays will measure < 1 event/second, making the simple "trigger on every event" approach perfectly viable.
|
||||
630
docs/sql_query_admin_api.md
Normal file
630
docs/sql_query_admin_api.md
Normal file
@@ -0,0 +1,630 @@
|
||||
# SQL Query Admin API Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the design for a general-purpose SQL query interface for the C-Relay admin API. This allows administrators to execute read-only SQL queries against the relay database through cryptographically signed kind 23456 events with NIP-44 encrypted command arrays.
|
||||
|
||||
## Security Model
|
||||
|
||||
### Authentication
|
||||
- All queries must be sent as kind 23456 events with NIP-44 encrypted content
|
||||
- Events must be signed by the admin's private key
|
||||
- Admin pubkey verified against `config.admin_pubkey`
|
||||
- Follows the same authentication pattern as existing admin commands
|
||||
|
||||
### Query Restrictions
|
||||
While authentication is cryptographically secure, we implement defensive safeguards:
|
||||
|
||||
1. **Read-Only Enforcement**
|
||||
- Only SELECT statements allowed
|
||||
- Block: INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, PRAGMA (write operations)
|
||||
- Allow: SELECT, WITH (for CTEs)
|
||||
|
||||
2. **Resource Limits**
|
||||
- Query timeout: 5 seconds (configurable)
|
||||
- Result row limit: 1000 rows (configurable)
|
||||
- Result size limit: 1MB (configurable)
|
||||
|
||||
3. **Query Logging**
|
||||
- All queries logged with timestamp, admin pubkey, execution time
|
||||
- Failed queries logged with error message
|
||||
|
||||
## Command Format
|
||||
|
||||
### Admin Event Structure (Kind 23456)
|
||||
```json
|
||||
{
|
||||
"id": "event_id",
|
||||
"pubkey": "admin_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23456,
|
||||
"content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
|
||||
"tags": [
|
||||
["p", "relay_public_key"]
|
||||
],
|
||||
"sig": "event_signature"
|
||||
}
|
||||
```
|
||||
|
||||
The `content` field contains a NIP-44 encrypted JSON array:
|
||||
```json
|
||||
["sql_query", "SELECT * FROM events LIMIT 10"]
|
||||
```
|
||||
|
||||
### Response Format (Kind 23457)
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44_encrypted_content",
|
||||
"tags": [
|
||||
["p", "admin_public_key"],
|
||||
["e", "request_event_id"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
The `content` field contains NIP-44 encrypted JSON:
|
||||
```json
|
||||
{
|
||||
"query_type": "sql_query",
|
||||
"request_id": "request_event_id",
|
||||
"timestamp": 1234567890,
|
||||
"query": "SELECT * FROM events LIMIT 10",
|
||||
"execution_time_ms": 45,
|
||||
"row_count": 10,
|
||||
"columns": ["id", "pubkey", "created_at", "kind", "content"],
|
||||
"rows": [
|
||||
["abc123...", "def456...", 1234567890, 1, "Hello world"],
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The response includes the request event ID in two places:
|
||||
1. **In tags**: `["e", "request_event_id"]` - Standard Nostr convention for event references
|
||||
2. **In content**: `"request_id": "request_event_id"` - For easy access after decryption
|
||||
|
||||
### Error Response Format (Kind 23457)
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44_encrypted_content",
|
||||
"tags": [
|
||||
["p", "admin_public_key"],
|
||||
["e", "request_event_id"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
The `content` field contains NIP-44 encrypted JSON:
|
||||
```json
|
||||
{
|
||||
"query_type": "sql_query",
|
||||
"request_id": "request_event_id",
|
||||
"timestamp": 1234567890,
|
||||
"query": "DELETE FROM events",
|
||||
"status": "error",
|
||||
"error": "Query blocked: DELETE statements not allowed",
|
||||
"error_type": "blocked_statement"
|
||||
}
|
||||
```
|
||||
|
||||
## Available Database Tables and Views
|
||||
|
||||
### Core Tables
|
||||
- **events** - All Nostr events (id, pubkey, created_at, kind, content, tags, sig)
|
||||
- **config** - Configuration key-value pairs
|
||||
- **auth_rules** - Authentication and authorization rules
|
||||
- **subscription_events** - Subscription lifecycle events
|
||||
- **event_broadcasts** - Event broadcast log
|
||||
|
||||
### Useful Views
|
||||
- **recent_events** - Last 1000 events
|
||||
- **event_stats** - Event statistics by type
|
||||
- **configuration_events** - Kind 33334 configuration events
|
||||
- **subscription_analytics** - Subscription metrics by date
|
||||
- **active_subscriptions_log** - Currently active subscriptions
|
||||
- **event_kinds_view** - Event distribution by kind
|
||||
- **top_pubkeys_view** - Top 10 pubkeys by event count
|
||||
- **time_stats_view** - Time-based statistics (24h, 7d, 30d)
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Backend (dm_admin.c)
|
||||
|
||||
#### 1. Query Validation Function
|
||||
```c
|
||||
int validate_sql_query(const char* query, char* error_msg, size_t error_size);
|
||||
```
|
||||
- Check for blocked keywords (case-insensitive)
|
||||
- Validate query syntax (basic checks)
|
||||
- Return 0 on success, -1 on failure
|
||||
|
||||
#### 2. Query Execution Function
|
||||
```c
|
||||
char* execute_sql_query(const char* query, char* error_msg, size_t error_size);
|
||||
```
|
||||
- Set query timeout using sqlite3_busy_timeout()
|
||||
- Execute query with row/size limits
|
||||
- Build JSON response with results
|
||||
- Log query execution
|
||||
- Return JSON string or NULL on error
|
||||
|
||||
#### 3. Command Handler Integration
|
||||
Add to `process_dm_admin_command()` in [`dm_admin.c`](src/dm_admin.c:131):
|
||||
```c
|
||||
else if (strcmp(command_type, "sql_query") == 0) {
|
||||
const char* query = get_tag_value(event, "sql_query", 1);
|
||||
if (!query) {
|
||||
DEBUG_ERROR("DM Admin: Missing sql_query parameter");
|
||||
snprintf(error_message, error_size, "invalid: missing SQL query");
|
||||
} else {
|
||||
result = handle_sql_query_unified(event, query, error_message, error_size, wsi);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add unified handler function:
|
||||
```c
|
||||
int handle_sql_query_unified(cJSON* event, const char* query,
|
||||
char* error_message, size_t error_size,
|
||||
struct lws* wsi) {
|
||||
// Get request event ID for response correlation
|
||||
cJSON* request_id_obj = cJSON_GetObjectItem(event, "id");
|
||||
if (!request_id_obj || !cJSON_IsString(request_id_obj)) {
|
||||
snprintf(error_message, error_size, "Missing request event ID");
|
||||
return -1;
|
||||
}
|
||||
const char* request_id = cJSON_GetStringValue(request_id_obj);
|
||||
|
||||
// Validate query
|
||||
if (!validate_sql_query(query, error_message, error_size)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Execute query and include request_id in result
|
||||
char* result_json = execute_sql_query(query, request_id, error_message, error_size);
|
||||
if (!result_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Send response as kind 23457 event with request ID in tags
|
||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
|
||||
free(result_json);
|
||||
snprintf(error_message, error_size, "Missing sender pubkey");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
||||
int send_result = send_admin_response(sender_pubkey, result_json, request_id,
|
||||
error_message, error_size, wsi);
|
||||
free(result_json);
|
||||
|
||||
return send_result;
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend (api/index.html)
|
||||
|
||||
#### SQL Query Section UI
|
||||
Add to [`api/index.html`](api/index.html:1):
|
||||
```html
|
||||
<section id="sql-query-section" class="admin-section">
|
||||
<h2>SQL Query Console</h2>
|
||||
|
||||
<div class="query-selector">
|
||||
<label for="query-dropdown">Quick Queries & History:</label>
|
||||
<select id="query-dropdown" onchange="loadSelectedQuery()">
|
||||
<option value="">-- Select a query --</option>
|
||||
<optgroup label="Common Queries">
|
||||
<option value="recent_events">Recent Events</option>
|
||||
<option value="event_stats">Event Statistics</option>
|
||||
<option value="subscriptions">Active Subscriptions</option>
|
||||
<option value="top_pubkeys">Top Pubkeys</option>
|
||||
<option value="event_kinds">Event Kinds Distribution</option>
|
||||
<option value="time_stats">Time-based Statistics</option>
|
||||
</optgroup>
|
||||
<optgroup label="Query History" id="history-group">
|
||||
<!-- Dynamically populated from localStorage -->
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="query-editor">
|
||||
<label for="sql-input">SQL Query:</label>
|
||||
<textarea id="sql-input" rows="5" placeholder="SELECT * FROM events LIMIT 10"></textarea>
|
||||
<div class="query-actions">
|
||||
<button onclick="executeSqlQuery()" class="primary-button">Execute Query</button>
|
||||
<button onclick="clearSqlQuery()">Clear</button>
|
||||
<button onclick="clearQueryHistory()" class="danger-button">Clear History</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="query-results">
|
||||
<h3>Results</h3>
|
||||
<div id="query-info" class="info-box"></div>
|
||||
<div id="query-table" class="table-container"></div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
#### JavaScript Functions (api/index.js)
|
||||
Add to [`api/index.js`](api/index.js:1):
|
||||
```javascript
|
||||
// Predefined query templates
|
||||
const SQL_QUERY_TEMPLATES = {
|
||||
recent_events: "SELECT id, pubkey, created_at, kind, substr(content, 1, 50) as content FROM events ORDER BY created_at DESC LIMIT 20",
|
||||
event_stats: "SELECT * FROM event_stats",
|
||||
subscriptions: "SELECT * FROM active_subscriptions_log ORDER BY created_at DESC",
|
||||
top_pubkeys: "SELECT * FROM top_pubkeys_view",
|
||||
event_kinds: "SELECT * FROM event_kinds_view ORDER BY count DESC",
|
||||
time_stats: "SELECT * FROM time_stats_view"
|
||||
};
|
||||
|
||||
// Query history management (localStorage)
|
||||
const QUERY_HISTORY_KEY = 'c_relay_sql_history';
|
||||
const MAX_HISTORY_ITEMS = 20;
|
||||
|
||||
// Load query history from localStorage
|
||||
function loadQueryHistory() {
|
||||
try {
|
||||
const history = localStorage.getItem(QUERY_HISTORY_KEY);
|
||||
return history ? JSON.parse(history) : [];
|
||||
} catch (e) {
|
||||
console.error('Failed to load query history:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Save query to history
|
||||
function saveQueryToHistory(query) {
|
||||
if (!query || query.trim().length === 0) return;
|
||||
|
||||
try {
|
||||
let history = loadQueryHistory();
|
||||
|
||||
// Remove duplicate if exists
|
||||
history = history.filter(q => q !== query);
|
||||
|
||||
// Add to beginning
|
||||
history.unshift(query);
|
||||
|
||||
// Limit size
|
||||
if (history.length > MAX_HISTORY_ITEMS) {
|
||||
history = history.slice(0, MAX_HISTORY_ITEMS);
|
||||
}
|
||||
|
||||
localStorage.setItem(QUERY_HISTORY_KEY, JSON.stringify(history));
|
||||
updateQueryDropdown();
|
||||
} catch (e) {
|
||||
console.error('Failed to save query history:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear query history
|
||||
function clearQueryHistory() {
|
||||
if (confirm('Clear all query history?')) {
|
||||
localStorage.removeItem(QUERY_HISTORY_KEY);
|
||||
updateQueryDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
// Update dropdown with history
|
||||
function updateQueryDropdown() {
|
||||
const historyGroup = document.getElementById('history-group');
|
||||
if (!historyGroup) return;
|
||||
|
||||
// Clear existing history options
|
||||
historyGroup.innerHTML = '';
|
||||
|
||||
const history = loadQueryHistory();
|
||||
if (history.length === 0) {
|
||||
const option = document.createElement('option');
|
||||
option.value = '';
|
||||
option.textContent = '(no history)';
|
||||
option.disabled = true;
|
||||
historyGroup.appendChild(option);
|
||||
return;
|
||||
}
|
||||
|
||||
history.forEach((query, index) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = `history_${index}`;
|
||||
// Truncate long queries for display
|
||||
const displayQuery = query.length > 60 ? query.substring(0, 60) + '...' : query;
|
||||
option.textContent = displayQuery;
|
||||
option.dataset.query = query;
|
||||
historyGroup.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Load selected query from dropdown
|
||||
function loadSelectedQuery() {
|
||||
const dropdown = document.getElementById('query-dropdown');
|
||||
const selectedValue = dropdown.value;
|
||||
|
||||
if (!selectedValue) return;
|
||||
|
||||
let query = '';
|
||||
|
||||
// Check if it's a template
|
||||
if (SQL_QUERY_TEMPLATES[selectedValue]) {
|
||||
query = SQL_QUERY_TEMPLATES[selectedValue];
|
||||
}
|
||||
// Check if it's from history
|
||||
else if (selectedValue.startsWith('history_')) {
|
||||
const selectedOption = dropdown.options[dropdown.selectedIndex];
|
||||
query = selectedOption.dataset.query;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
document.getElementById('sql-input').value = query;
|
||||
}
|
||||
|
||||
// Reset dropdown to placeholder
|
||||
dropdown.value = '';
|
||||
}
|
||||
|
||||
// Initialize query history on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateQueryDropdown();
|
||||
});
|
||||
|
||||
// Clear the SQL query input
|
||||
function clearSqlQuery() {
|
||||
document.getElementById('sql-input').value = '';
|
||||
document.getElementById('query-info').innerHTML = '';
|
||||
document.getElementById('query-table').innerHTML = '';
|
||||
}
|
||||
|
||||
// Track pending SQL queries by request ID
|
||||
const pendingSqlQueries = new Map();
|
||||
|
||||
// Execute SQL query via admin API
|
||||
async function executeSqlQuery() {
|
||||
const query = document.getElementById('sql-input').value;
|
||||
if (!query.trim()) {
|
||||
showError('Please enter a SQL query');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
document.getElementById('query-info').innerHTML = '<div class="loading">Executing query...</div>';
|
||||
document.getElementById('query-table').innerHTML = '';
|
||||
|
||||
// Save to history (before execution, so it's saved even if query fails)
|
||||
saveQueryToHistory(query.trim());
|
||||
|
||||
// Send query as kind 23456 admin command
|
||||
const command = ["sql_query", query];
|
||||
const requestEvent = await sendAdminCommand(command);
|
||||
|
||||
// Store query info for when response arrives
|
||||
if (requestEvent && requestEvent.id) {
|
||||
pendingSqlQueries.set(requestEvent.id, {
|
||||
query: query,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Note: Response will be handled by the event listener
|
||||
// which will call displaySqlQueryResults() when response arrives
|
||||
} catch (error) {
|
||||
showError('Failed to execute query: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle SQL query response (called by event listener)
|
||||
function handleSqlQueryResponse(response) {
|
||||
// Check if this is a response to one of our queries
|
||||
if (response.request_id && pendingSqlQueries.has(response.request_id)) {
|
||||
const queryInfo = pendingSqlQueries.get(response.request_id);
|
||||
pendingSqlQueries.delete(response.request_id);
|
||||
|
||||
// Display results
|
||||
displaySqlQueryResults(response);
|
||||
}
|
||||
}
|
||||
|
||||
// Display SQL query results
|
||||
function displaySqlQueryResults(response) {
|
||||
const infoDiv = document.getElementById('query-info');
|
||||
const tableDiv = document.getElementById('query-table');
|
||||
|
||||
if (response.status === 'error' || response.error) {
|
||||
infoDiv.innerHTML = `<div class="error-message">❌ ${response.error || 'Query failed'}</div>`;
|
||||
tableDiv.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show query info with request ID for debugging
|
||||
const rowCount = response.row_count || 0;
|
||||
const execTime = response.execution_time_ms || 0;
|
||||
const requestId = response.request_id ? response.request_id.substring(0, 8) + '...' : 'unknown';
|
||||
infoDiv.innerHTML = `
|
||||
<div class="query-info-success">
|
||||
<span>✅ Query executed successfully</span>
|
||||
<span>Rows: ${rowCount}</span>
|
||||
<span>Execution Time: ${execTime}ms</span>
|
||||
<span class="request-id" title="${response.request_id || ''}">Request: ${requestId}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Build results table
|
||||
if (response.rows && response.rows.length > 0) {
|
||||
let html = '<table class="sql-results-table"><thead><tr>';
|
||||
response.columns.forEach(col => {
|
||||
html += `<th>${escapeHtml(col)}</th>`;
|
||||
});
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
response.rows.forEach(row => {
|
||||
html += '<tr>';
|
||||
row.forEach(cell => {
|
||||
const cellValue = cell === null ? '<em>NULL</em>' : escapeHtml(String(cell));
|
||||
html += `<td>${cellValue}</td>`;
|
||||
});
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
tableDiv.innerHTML = html;
|
||||
} else {
|
||||
tableDiv.innerHTML = '<p class="no-results">No results returned</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to escape HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
```
|
||||
|
||||
## Example Queries
|
||||
|
||||
### Subscription Statistics
|
||||
```sql
|
||||
SELECT
|
||||
date,
|
||||
subscriptions_created,
|
||||
subscriptions_ended,
|
||||
avg_duration_seconds,
|
||||
unique_clients
|
||||
FROM subscription_analytics
|
||||
ORDER BY date DESC
|
||||
LIMIT 7;
|
||||
```
|
||||
|
||||
### Event Distribution by Kind
|
||||
```sql
|
||||
SELECT kind, count, percentage
|
||||
FROM event_kinds_view
|
||||
ORDER BY count DESC;
|
||||
```
|
||||
|
||||
### Recent Events by Specific Pubkey
|
||||
```sql
|
||||
SELECT id, created_at, kind, content
|
||||
FROM events
|
||||
WHERE pubkey = 'abc123...'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
### Active Subscriptions with Details
|
||||
```sql
|
||||
SELECT
|
||||
subscription_id,
|
||||
client_ip,
|
||||
events_sent,
|
||||
duration_seconds,
|
||||
filter_json
|
||||
FROM active_subscriptions_log
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### Database Size and Event Count
|
||||
```sql
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM events) as total_events,
|
||||
(SELECT COUNT(*) FROM subscription_events) as total_subscriptions,
|
||||
(SELECT COUNT(*) FROM auth_rules WHERE active = 1) as active_rules;
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Add to config table:
|
||||
```sql
|
||||
INSERT INTO config (key, value, data_type, description, category) VALUES
|
||||
('sql_query_enabled', 'true', 'boolean', 'Enable SQL query admin API', 'admin'),
|
||||
('sql_query_timeout', '5', 'integer', 'Query timeout in seconds', 'admin'),
|
||||
('sql_query_row_limit', '1000', 'integer', 'Maximum rows per query', 'admin'),
|
||||
('sql_query_size_limit', '1048576', 'integer', 'Maximum result size in bytes', 'admin'),
|
||||
('sql_query_log_enabled', 'true', 'boolean', 'Log all SQL queries', 'admin');
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### What This Protects Against
|
||||
1. **Unauthorized Access** - Only admin can execute queries (cryptographic verification)
|
||||
2. **Data Modification** - Read-only enforcement prevents accidental/malicious changes
|
||||
3. **Resource Exhaustion** - Timeouts and limits prevent DoS
|
||||
4. **Audit Trail** - All queries logged for security review
|
||||
|
||||
### What This Does NOT Protect Against
|
||||
1. **Admin Compromise** - If admin private key is stolen, attacker has full read access
|
||||
2. **Information Disclosure** - Admin can read all data (by design)
|
||||
3. **Complex Attacks** - Sophisticated SQL injection might bypass simple keyword blocking
|
||||
|
||||
### Recommendations
|
||||
1. **Secure Admin Key** - Store admin private key securely, never commit to git
|
||||
2. **Monitor Query Logs** - Review query logs regularly for suspicious activity
|
||||
3. **Backup Database** - Regular backups in case of issues
|
||||
4. **Test Queries** - Test complex queries on development relay first
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Unit Tests
|
||||
1. Query validation (blocked keywords, syntax)
|
||||
2. Result formatting (JSON structure)
|
||||
3. Error handling (timeouts, limits)
|
||||
|
||||
### Integration Tests
|
||||
1. Execute queries through NIP-17 DM
|
||||
2. Verify authentication (admin vs non-admin)
|
||||
3. Test resource limits (timeout, row limit)
|
||||
4. Test error responses
|
||||
|
||||
### Security Tests
|
||||
1. Attempt blocked statements (INSERT, DELETE, etc.)
|
||||
2. Attempt SQL injection patterns
|
||||
3. Test query timeout with slow queries
|
||||
4. Test row limit with large result sets
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Query History** - Store recent queries for quick re-execution
|
||||
2. **Query Favorites** - Save frequently used queries
|
||||
3. **Export Results** - Download results as CSV/JSON
|
||||
4. **Query Builder** - Visual query builder for common operations
|
||||
5. **Real-time Updates** - WebSocket updates for live data
|
||||
6. **Query Sharing** - Share queries with other admins (if multi-admin support added)
|
||||
|
||||
## Migration Path
|
||||
|
||||
### Phase 1: Backend Implementation
|
||||
1. Add query validation function
|
||||
2. Add query execution function
|
||||
3. Integrate with NIP-17 command handler
|
||||
4. Add configuration options
|
||||
5. Add query logging
|
||||
|
||||
### Phase 2: Frontend Implementation
|
||||
1. Add SQL query section to index.html
|
||||
2. Add query execution JavaScript
|
||||
3. Add predefined query templates
|
||||
4. Add results display formatting
|
||||
|
||||
### Phase 3: Testing and Documentation
|
||||
1. Write unit tests
|
||||
2. Write integration tests
|
||||
3. Update user documentation
|
||||
4. Create query examples guide
|
||||
|
||||
### Phase 4: Enhancement
|
||||
1. Add query history
|
||||
2. Add export functionality
|
||||
3. Optimize performance
|
||||
4. Add more predefined templates
|
||||
258
docs/sql_test_design.md
Normal file
258
docs/sql_test_design.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# SQL Query Test Script Design
|
||||
|
||||
## Overview
|
||||
|
||||
Test script for validating the SQL query admin API functionality. Tests query validation, execution, error handling, and security features.
|
||||
|
||||
## Script: tests/sql_test.sh
|
||||
|
||||
### Test Categories
|
||||
|
||||
#### 1. Query Validation Tests
|
||||
- ✅ Valid SELECT queries accepted
|
||||
- ❌ INSERT statements blocked
|
||||
- ❌ UPDATE statements blocked
|
||||
- ❌ DELETE statements blocked
|
||||
- ❌ DROP statements blocked
|
||||
- ❌ CREATE statements blocked
|
||||
- ❌ ALTER statements blocked
|
||||
- ❌ PRAGMA write operations blocked
|
||||
|
||||
#### 2. Query Execution Tests
|
||||
- ✅ Simple SELECT query
|
||||
- ✅ SELECT with WHERE clause
|
||||
- ✅ SELECT with JOIN
|
||||
- ✅ SELECT with ORDER BY and LIMIT
|
||||
- ✅ Query against views
|
||||
- ✅ Query with aggregate functions (COUNT, SUM, AVG)
|
||||
|
||||
#### 3. Response Format Tests
|
||||
- ✅ Response includes request_id
|
||||
- ✅ Response includes query_type
|
||||
- ✅ Response includes columns array
|
||||
- ✅ Response includes rows array
|
||||
- ✅ Response includes row_count
|
||||
- ✅ Response includes execution_time_ms
|
||||
|
||||
#### 4. Error Handling Tests
|
||||
- ❌ Invalid SQL syntax
|
||||
- ❌ Non-existent table
|
||||
- ❌ Non-existent column
|
||||
- ❌ Query timeout (if configurable)
|
||||
|
||||
#### 5. Security Tests
|
||||
- ❌ SQL injection attempts blocked
|
||||
- ❌ Nested query attacks blocked
|
||||
- ❌ Comment-based attacks blocked
|
||||
|
||||
#### 6. Concurrent Query Tests
|
||||
- ✅ Multiple queries in parallel
|
||||
- ✅ Responses correctly correlated to requests
|
||||
|
||||
## Script Structure
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# SQL Query Admin API Test Script
|
||||
# Tests the sql_query command functionality
|
||||
|
||||
set -e
|
||||
|
||||
RELAY_URL="${RELAY_URL:-ws://localhost:8888}"
|
||||
ADMIN_PRIVKEY="${ADMIN_PRIVKEY:-}"
|
||||
RELAY_PUBKEY="${RELAY_PUBKEY:-}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test counters
|
||||
TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Helper functions
|
||||
print_test() {
|
||||
echo -e "${YELLOW}TEST: $1${NC}"
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
}
|
||||
|
||||
print_pass() {
|
||||
echo -e "${GREEN}✓ PASS: $1${NC}"
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
}
|
||||
|
||||
print_fail() {
|
||||
echo -e "${RED}✗ FAIL: $1${NC}"
|
||||
TESTS_FAILED=$((TESTS_FAILED + 1))
|
||||
}
|
||||
|
||||
# Send SQL query command
|
||||
send_sql_query() {
|
||||
local query="$1"
|
||||
# Implementation using nostr CLI tools or curl
|
||||
# Returns response JSON
|
||||
}
|
||||
|
||||
# Test functions
|
||||
test_valid_select() {
|
||||
print_test "Valid SELECT query"
|
||||
local response=$(send_sql_query "SELECT * FROM events LIMIT 1")
|
||||
if echo "$response" | grep -q '"query_type":"sql_query"'; then
|
||||
print_pass "Valid SELECT accepted"
|
||||
else
|
||||
print_fail "Valid SELECT rejected"
|
||||
fi
|
||||
}
|
||||
|
||||
test_blocked_insert() {
|
||||
print_test "INSERT statement blocked"
|
||||
local response=$(send_sql_query "INSERT INTO events VALUES (...)")
|
||||
if echo "$response" | grep -q '"error"'; then
|
||||
print_pass "INSERT correctly blocked"
|
||||
else
|
||||
print_fail "INSERT not blocked"
|
||||
fi
|
||||
}
|
||||
|
||||
# ... more test functions ...
|
||||
|
||||
# Main test execution
|
||||
main() {
|
||||
echo "================================"
|
||||
echo "SQL Query Admin API Tests"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
# Check prerequisites
|
||||
if [ -z "$ADMIN_PRIVKEY" ]; then
|
||||
echo "Error: ADMIN_PRIVKEY not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run test suites
|
||||
echo "1. Query Validation Tests"
|
||||
test_valid_select
|
||||
test_blocked_insert
|
||||
test_blocked_update
|
||||
test_blocked_delete
|
||||
test_blocked_drop
|
||||
|
||||
echo ""
|
||||
echo "2. Query Execution Tests"
|
||||
test_simple_select
|
||||
test_select_with_where
|
||||
test_select_with_join
|
||||
test_select_views
|
||||
|
||||
echo ""
|
||||
echo "3. Response Format Tests"
|
||||
test_response_format
|
||||
test_request_id_correlation
|
||||
|
||||
echo ""
|
||||
echo "4. Error Handling Tests"
|
||||
test_invalid_syntax
|
||||
test_nonexistent_table
|
||||
|
||||
echo ""
|
||||
echo "5. Security Tests"
|
||||
test_sql_injection
|
||||
|
||||
echo ""
|
||||
echo "6. Concurrent Query Tests"
|
||||
test_concurrent_queries
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "Test Summary"
|
||||
echo "================================"
|
||||
echo "Tests Run: $TESTS_RUN"
|
||||
echo "Tests Passed: $TESTS_PASSED"
|
||||
echo "Tests Failed: $TESTS_FAILED"
|
||||
|
||||
if [ $TESTS_FAILED -eq 0 ]; then
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some tests failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
## Test Data Setup
|
||||
|
||||
The script should work with the existing relay database without requiring special test data, using:
|
||||
- Existing events table
|
||||
- Existing views (event_stats, recent_events, etc.)
|
||||
- Existing config table
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Set environment variables
|
||||
export ADMIN_PRIVKEY="your_admin_private_key_hex"
|
||||
export RELAY_PUBKEY="relay_public_key_hex"
|
||||
export RELAY_URL="ws://localhost:8888"
|
||||
|
||||
# Run tests
|
||||
./tests/sql_test.sh
|
||||
|
||||
# Run specific test category
|
||||
./tests/sql_test.sh validation
|
||||
./tests/sql_test.sh security
|
||||
```
|
||||
|
||||
## Integration with CI/CD
|
||||
|
||||
The script should:
|
||||
- Return exit code 0 on success, 1 on failure
|
||||
- Output TAP (Test Anything Protocol) format for CI integration
|
||||
- Be runnable in automated test pipelines
|
||||
- Not require manual intervention
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `bash` (version 4+)
|
||||
- `curl` or `websocat` for WebSocket communication
|
||||
- `jq` for JSON parsing
|
||||
- Nostr CLI tools (optional, for event signing)
|
||||
- Running c-relay instance
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
================================
|
||||
SQL Query Admin API Tests
|
||||
================================
|
||||
|
||||
1. Query Validation Tests
|
||||
TEST: Valid SELECT query
|
||||
✓ PASS: Valid SELECT accepted
|
||||
TEST: INSERT statement blocked
|
||||
✓ PASS: INSERT correctly blocked
|
||||
TEST: UPDATE statement blocked
|
||||
✓ PASS: UPDATE correctly blocked
|
||||
|
||||
2. Query Execution Tests
|
||||
TEST: Simple SELECT query
|
||||
✓ PASS: Query executed successfully
|
||||
TEST: SELECT with WHERE clause
|
||||
✓ PASS: WHERE clause works correctly
|
||||
|
||||
...
|
||||
|
||||
================================
|
||||
Test Summary
|
||||
================================
|
||||
Tests Run: 24
|
||||
Tests Passed: 24
|
||||
Tests Failed: 0
|
||||
All tests passed!
|
||||
@@ -1,128 +0,0 @@
|
||||
# Startup Configuration Design Analysis
|
||||
|
||||
## Review of startup_config_design.md
|
||||
|
||||
### Key Design Principles Identified
|
||||
|
||||
1. **Zero Command Line Arguments**: Complete elimination of CLI arguments for true "quick start"
|
||||
2. **Event-Based Configuration**: Configuration stored as Nostr event (kind 33334) in events table
|
||||
3. **Self-Contained Database**: Database named after relay pubkey (`<pubkey>.nrdb`)
|
||||
4. **First-Time Setup**: Automatic key generation and initial configuration creation
|
||||
5. **Configuration Consistency**: Always read from event, never from hardcoded defaults
|
||||
|
||||
### Implementation Gaps and Specifications Needed
|
||||
|
||||
#### 1. Key Generation Process
|
||||
**Specification:**
|
||||
```
|
||||
First Startup Key Generation:
|
||||
1. Generate all keys on first startup (admin private/public, relay private/public)
|
||||
2. Use nostr_core_lib for key generation entropy
|
||||
3. Keys are encoded in hex format
|
||||
4. Print admin private key to stdout for user to save (never stored)
|
||||
5. Store admin public key, relay private key, and relay public key in configuration event
|
||||
6. Admin can later change the 33334 event to alter stored keys
|
||||
```
|
||||
|
||||
#### 2. Database Naming and Location
|
||||
**Specification:**
|
||||
```
|
||||
Database Naming:
|
||||
1. Database is named using relay pubkey: ./<relay_pubkey>.nrdb
|
||||
2. Database path structure: ./<relay_pubkey>.nrdb
|
||||
3. If database creation fails, program quits (can't run without database)
|
||||
4. c_nostr_relay.db should never exist in new system
|
||||
```
|
||||
|
||||
#### 3. Configuration Event Structure (Kind 33334)
|
||||
**Specification:**
|
||||
```
|
||||
Event Structure:
|
||||
- Kind: 33334 (parameterized replaceable event)
|
||||
- Event validation: Use nostr_core_lib to validate event
|
||||
- Event content field: "C Nostr Relay Configuration" (descriptive text)
|
||||
- Configuration update mechanism: TBD
|
||||
- Complete tag structure provided in configuration section below
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 4. Configuration Change Monitoring
|
||||
**Configuration Monitoring System:**
|
||||
```
|
||||
Every event that is received is checked to see if it is a kind 33334 event from the admin pubkey.
|
||||
If so, it is processed as a configuration update.
|
||||
```
|
||||
|
||||
#### 5. Error Handling and Recovery
|
||||
**Specification:**
|
||||
```
|
||||
Error Recovery Priority:
|
||||
1. Try to load latest valid config event
|
||||
2. Generate new default configuration event if none exists
|
||||
3. Exit with error if all recovery attempts fail
|
||||
|
||||
Note: There is only ever one configuration event (parameterized replaceable event),
|
||||
so no fallback to previous versions.
|
||||
```
|
||||
|
||||
### Design Clarifications
|
||||
|
||||
**Key Management:**
|
||||
- Admin private key is never stored, only printed once at first startup
|
||||
- Single admin system (no multi-admin support)
|
||||
- No key rotation support
|
||||
|
||||
**Configuration Management:**
|
||||
- No configuration versioning/timestamping
|
||||
- No automatic backup of configuration events
|
||||
- Configuration events are not broadcastable to other relays
|
||||
- Future: Auth system to restrict admin access to configuration events
|
||||
|
||||
---
|
||||
|
||||
## Complete Current Configuration Structure
|
||||
|
||||
Based on analysis of [`src/config.c`](src/config.c:753-795), here is the complete current configuration structure that will be converted to event tags:
|
||||
|
||||
### Complete Event Structure Example
|
||||
```json
|
||||
{
|
||||
"kind": 33334,
|
||||
"created_at": 1725661483,
|
||||
"tags": [
|
||||
["d", "<relay_pubkey>"],
|
||||
["auth_enabled", "false"],
|
||||
["relay_port", "8888"],
|
||||
["max_connections", "100"],
|
||||
|
||||
["relay_description", "High-performance C Nostr relay with SQLite storage"],
|
||||
["relay_contact", ""],
|
||||
["relay_pubkey", "<relay_public_key>"],
|
||||
["relay_privkey", "<relay_private_key>"],
|
||||
["relay_software", "https://git.laantungir.net/laantungir/c-relay.git"],
|
||||
["relay_version", "v1.0.0"],
|
||||
|
||||
["pow_min_difficulty", "0"],
|
||||
["pow_mode", "basic"],
|
||||
["nip40_expiration_enabled", "true"],
|
||||
["nip40_expiration_strict", "true"],
|
||||
["nip40_expiration_filter", "true"],
|
||||
["nip40_expiration_grace_period", "300"],
|
||||
["max_subscriptions_per_client", "25"],
|
||||
["max_total_subscriptions", "5000"],
|
||||
["max_filters_per_subscription", "10"],
|
||||
["max_event_tags", "100"],
|
||||
["max_content_length", "8196"],
|
||||
["max_message_length", "16384"],
|
||||
["default_limit", "500"],
|
||||
["max_limit", "5000"]
|
||||
],
|
||||
"content": "C Nostr Relay Configuration",
|
||||
"pubkey": "<admin_public_key>",
|
||||
"id": "<computed_event_id>",
|
||||
"sig": "<event_signature>"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `admin_pubkey` tag is omitted as it's redundant with the event's `pubkey` field.
|
||||
1090
docs/startup_flows_complete.md
Normal file
1090
docs/startup_flows_complete.md
Normal file
File diff suppressed because it is too large
Load Diff
209
docs/subscription_matching_debug_plan.md
Normal file
209
docs/subscription_matching_debug_plan.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# Subscription Matching Debug Plan
|
||||
|
||||
## Problem
|
||||
The relay is not matching kind 1059 (NIP-17 gift wrap) events to subscriptions, even though a subscription exists with `kinds:[1059]` filter. The log shows:
|
||||
```
|
||||
Event broadcast complete: 0 subscriptions matched
|
||||
```
|
||||
|
||||
But we have this subscription:
|
||||
```
|
||||
sub:3 146.70.187.119 0x78edc9b43210 8m 27s kinds:[1059], since:10/23/2025, 4:27:59 PM, limit:50
|
||||
```
|
||||
|
||||
## Investigation Strategy
|
||||
|
||||
### 1. Add Debug Output to `event_matches_filter()` (lines 386-564)
|
||||
Add debug logging at each filter check to trace the matching logic:
|
||||
|
||||
- **Entry point**: Log the event kind and filter being tested
|
||||
- **Kinds filter check** (lines 392-415): Log whether kinds filter exists, the event kind value, and each filter kind being compared
|
||||
- **Authors filter check** (lines 417-442): Log if authors filter exists and matching results
|
||||
- **IDs filter check** (lines 444-469): Log if IDs filter exists and matching results
|
||||
- **Since filter check** (lines 471-482): Log the event timestamp vs filter since value
|
||||
- **Until filter check** (lines 484-495): Log the event timestamp vs filter until value
|
||||
- **Tag filters check** (lines 497-561): Log tag filter matching details
|
||||
- **Exit point**: Log whether the overall filter matched
|
||||
|
||||
### 2. Add Debug Output to `event_matches_subscription()` (lines 567-581)
|
||||
Add logging to show:
|
||||
- How many filters are in the subscription
|
||||
- Which filter (if any) matched
|
||||
- Overall subscription match result
|
||||
|
||||
### 3. Add Debug Output to `broadcast_event_to_subscriptions()` (lines 584-726)
|
||||
Add logging to show:
|
||||
- The event being broadcast (kind, id, created_at)
|
||||
- Total number of active subscriptions being checked
|
||||
- How many subscriptions matched after the first pass
|
||||
|
||||
### 4. Key Areas to Focus On
|
||||
|
||||
Based on the code analysis, the most likely issues are:
|
||||
|
||||
1. **Kind matching logic** (lines 392-415): The event kind might not be extracted correctly, or the comparison might be failing
|
||||
2. **Since timestamp** (lines 471-482): The subscription has a `since` filter - if the event timestamp is before this, it won't match
|
||||
3. **Event structure**: The event JSON might not have the expected structure
|
||||
|
||||
### 5. Specific Debug Additions
|
||||
|
||||
#### In `event_matches_filter()` at line 386:
|
||||
```c
|
||||
// Add at start of function
|
||||
cJSON* event_kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||
cJSON* event_created_at_obj = cJSON_GetObjectItem(event, "created_at");
|
||||
|
||||
DEBUG_TRACE("FILTER_MATCH: Testing event kind=%d id=%.8s created_at=%ld",
|
||||
event_kind_obj ? (int)cJSON_GetNumberValue(event_kind_obj) : -1,
|
||||
event_id_obj && cJSON_IsString(event_id_obj) ? cJSON_GetStringValue(event_id_obj) : "null",
|
||||
event_created_at_obj ? (long)cJSON_GetNumberValue(event_created_at_obj) : 0);
|
||||
```
|
||||
|
||||
#### In kinds filter check (after line 392):
|
||||
```c
|
||||
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
|
||||
DEBUG_TRACE("FILTER_MATCH: Checking kinds filter with %d kinds", cJSON_GetArraySize(filter->kinds));
|
||||
|
||||
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||
if (!event_kind || !cJSON_IsNumber(event_kind)) {
|
||||
DEBUG_WARN("FILTER_MATCH: Event has no valid kind field");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
|
||||
DEBUG_TRACE("FILTER_MATCH: Event kind=%d", event_kind_val);
|
||||
|
||||
int kind_match = 0;
|
||||
cJSON* kind_item = NULL;
|
||||
cJSON_ArrayForEach(kind_item, filter->kinds) {
|
||||
if (cJSON_IsNumber(kind_item)) {
|
||||
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
|
||||
DEBUG_TRACE("FILTER_MATCH: Comparing event kind %d with filter kind %d", event_kind_val, filter_kind);
|
||||
if (filter_kind == event_kind_val) {
|
||||
kind_match = 1;
|
||||
DEBUG_TRACE("FILTER_MATCH: Kind matched!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!kind_match) {
|
||||
DEBUG_TRACE("FILTER_MATCH: No kind match, filter rejected");
|
||||
return 0;
|
||||
}
|
||||
DEBUG_TRACE("FILTER_MATCH: Kinds filter passed");
|
||||
}
|
||||
```
|
||||
|
||||
#### In since filter check (after line 472):
|
||||
```c
|
||||
if (filter->since > 0) {
|
||||
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
|
||||
DEBUG_WARN("FILTER_MATCH: Event has no valid created_at field");
|
||||
return 0;
|
||||
}
|
||||
|
||||
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
|
||||
DEBUG_TRACE("FILTER_MATCH: Checking since filter: event_ts=%ld filter_since=%ld",
|
||||
event_timestamp, filter->since);
|
||||
|
||||
if (event_timestamp < filter->since) {
|
||||
DEBUG_TRACE("FILTER_MATCH: Event too old (before since), filter rejected");
|
||||
return 0;
|
||||
}
|
||||
DEBUG_TRACE("FILTER_MATCH: Since filter passed");
|
||||
}
|
||||
```
|
||||
|
||||
#### At end of `event_matches_filter()` (before line 563):
|
||||
```c
|
||||
DEBUG_TRACE("FILTER_MATCH: All filters passed, event matches!");
|
||||
return 1; // All filters passed
|
||||
```
|
||||
|
||||
#### In `event_matches_subscription()` at line 567:
|
||||
```c
|
||||
int event_matches_subscription(cJSON* event, subscription_t* subscription) {
|
||||
if (!event || !subscription || !subscription->filters) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("SUB_MATCH: Testing subscription '%s'", subscription->id);
|
||||
|
||||
int filter_num = 0;
|
||||
subscription_filter_t* filter = subscription->filters;
|
||||
while (filter) {
|
||||
filter_num++;
|
||||
DEBUG_TRACE("SUB_MATCH: Testing filter #%d", filter_num);
|
||||
|
||||
if (event_matches_filter(event, filter)) {
|
||||
DEBUG_TRACE("SUB_MATCH: Filter #%d matched! Subscription '%s' matches",
|
||||
filter_num, subscription->id);
|
||||
return 1; // Match found (OR logic)
|
||||
}
|
||||
filter = filter->next;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("SUB_MATCH: No filters matched for subscription '%s'", subscription->id);
|
||||
return 0; // No filters matched
|
||||
}
|
||||
```
|
||||
|
||||
#### In `broadcast_event_to_subscriptions()` at line 584:
|
||||
```c
|
||||
int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
if (!event) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Log event details
|
||||
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* event_id = cJSON_GetObjectItem(event, "id");
|
||||
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||
|
||||
DEBUG_TRACE("BROADCAST: Event kind=%d id=%.8s created_at=%ld",
|
||||
event_kind ? (int)cJSON_GetNumberValue(event_kind) : -1,
|
||||
event_id && cJSON_IsString(event_id) ? cJSON_GetStringValue(event_id) : "null",
|
||||
event_created_at ? (long)cJSON_GetNumberValue(event_created_at) : 0);
|
||||
|
||||
// ... existing expiration check code ...
|
||||
|
||||
// After line 611 (before pthread_mutex_lock):
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
int total_subs = 0;
|
||||
subscription_t* count_sub = g_subscription_manager.active_subscriptions;
|
||||
while (count_sub) {
|
||||
total_subs++;
|
||||
count_sub = count_sub->next;
|
||||
}
|
||||
DEBUG_TRACE("BROADCAST: Checking %d active subscriptions", total_subs);
|
||||
|
||||
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||
// ... rest of matching logic ...
|
||||
```
|
||||
|
||||
## Expected Outcome
|
||||
|
||||
With these debug additions, we should see output like:
|
||||
```
|
||||
BROADCAST: Event kind=1059 id=abc12345 created_at=1729712279
|
||||
BROADCAST: Checking 1 active subscriptions
|
||||
SUB_MATCH: Testing subscription 'sub:3'
|
||||
SUB_MATCH: Testing filter #1
|
||||
FILTER_MATCH: Testing event kind=1059 id=abc12345 created_at=1729712279
|
||||
FILTER_MATCH: Checking kinds filter with 1 kinds
|
||||
FILTER_MATCH: Event kind=1059
|
||||
FILTER_MATCH: Comparing event kind 1059 with filter kind 1059
|
||||
FILTER_MATCH: Kind matched!
|
||||
FILTER_MATCH: Kinds filter passed
|
||||
FILTER_MATCH: Checking since filter: event_ts=1729712279 filter_since=1729708079
|
||||
FILTER_MATCH: Since filter passed
|
||||
FILTER_MATCH: All filters passed, event matches!
|
||||
SUB_MATCH: Filter #1 matched! Subscription 'sub:3' matches
|
||||
Event broadcast complete: 1 subscriptions matched
|
||||
```
|
||||
|
||||
This will help us identify exactly where the matching is failing.
|
||||
427
docs/unified_startup_design.md
Normal file
427
docs/unified_startup_design.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# Unified Startup Sequence Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the new unified startup sequence where all config values are created first, then CLI overrides are applied as a separate atomic operation. This eliminates the current 3-step incremental building process.
|
||||
|
||||
## Current Problems
|
||||
|
||||
1. **Incremental Config Building**: Config is built in 3 steps:
|
||||
- Step 1: `populate_default_config_values()` - adds defaults
|
||||
- Step 2: CLI overrides applied via `update_config_in_table()`
|
||||
- Step 3: `add_pubkeys_to_config_table()` - adds generated keys
|
||||
|
||||
2. **Race Conditions**: Cache can be refreshed between steps, causing incomplete config reads
|
||||
|
||||
3. **Complexity**: Multiple code paths for first-time vs restart scenarios
|
||||
|
||||
## New Design Principles
|
||||
|
||||
1. **Atomic Config Creation**: All config values created in single transaction
|
||||
2. **Separate Override Phase**: CLI overrides applied after complete config exists
|
||||
3. **Unified Code Path**: Same logic for first-time and restart scenarios
|
||||
4. **Cache Safety**: Cache only loaded after config is complete
|
||||
|
||||
---
|
||||
|
||||
## Scenario 1: First-Time Startup (No Database)
|
||||
|
||||
### Sequence
|
||||
|
||||
```
|
||||
1. Key Generation Phase
|
||||
├─ generate_random_private_key_bytes() → admin_privkey_bytes
|
||||
├─ nostr_bytes_to_hex() → admin_privkey (hex)
|
||||
├─ nostr_ec_public_key_from_private_key() → admin_pubkey_bytes
|
||||
├─ nostr_bytes_to_hex() → admin_pubkey (hex)
|
||||
├─ generate_random_private_key_bytes() → relay_privkey_bytes
|
||||
├─ nostr_bytes_to_hex() → relay_privkey (hex)
|
||||
├─ nostr_ec_public_key_from_private_key() → relay_pubkey_bytes
|
||||
└─ nostr_bytes_to_hex() → relay_pubkey (hex)
|
||||
|
||||
2. Database Creation Phase
|
||||
├─ create_database_with_relay_pubkey(relay_pubkey)
|
||||
│ └─ Sets g_database_path = "<relay_pubkey>.db"
|
||||
└─ init_database(g_database_path)
|
||||
└─ Creates database with embedded schema (includes config table)
|
||||
|
||||
3. Complete Config Population Phase (ATOMIC)
|
||||
├─ BEGIN TRANSACTION
|
||||
├─ populate_all_config_values_atomic()
|
||||
│ ├─ Insert ALL default config values from DEFAULT_CONFIG_VALUES[]
|
||||
│ ├─ Insert admin_pubkey
|
||||
│ └─ Insert relay_pubkey
|
||||
└─ COMMIT TRANSACTION
|
||||
|
||||
4. CLI Override Phase (ATOMIC)
|
||||
├─ BEGIN TRANSACTION
|
||||
├─ apply_cli_overrides()
|
||||
│ ├─ IF cli_options.port_override > 0:
|
||||
│ │ └─ UPDATE config SET value = ? WHERE key = 'relay_port'
|
||||
│ ├─ IF cli_options.admin_pubkey_override[0]:
|
||||
│ │ └─ UPDATE config SET value = ? WHERE key = 'admin_pubkey'
|
||||
│ └─ IF cli_options.relay_privkey_override[0]:
|
||||
│ └─ UPDATE config SET value = ? WHERE key = 'relay_privkey'
|
||||
└─ COMMIT TRANSACTION
|
||||
|
||||
5. Secure Key Storage Phase
|
||||
└─ store_relay_private_key(relay_privkey)
|
||||
└─ INSERT INTO relay_seckey (private_key_hex) VALUES (?)
|
||||
|
||||
6. Cache Initialization Phase
|
||||
└─ refresh_unified_cache_from_table()
|
||||
└─ Loads complete config into g_unified_cache
|
||||
```
|
||||
|
||||
### Function Call Sequence
|
||||
|
||||
```c
|
||||
// In main.c - first_time_startup branch
|
||||
if (is_first_time_startup()) {
|
||||
// 1. Key Generation
|
||||
first_time_startup_sequence(&cli_options);
|
||||
// → Generates keys, stores in g_unified_cache
|
||||
// → Sets g_database_path
|
||||
// → Does NOT populate config yet
|
||||
|
||||
// 2. Database Creation
|
||||
init_database(g_database_path);
|
||||
// → Creates database with schema
|
||||
|
||||
// 3. Complete Config Population (NEW FUNCTION)
|
||||
populate_all_config_values_atomic(&cli_options);
|
||||
// → Inserts ALL defaults + pubkeys in single transaction
|
||||
// → Does NOT apply CLI overrides yet
|
||||
|
||||
// 4. CLI Override Phase (NEW FUNCTION)
|
||||
apply_cli_overrides_atomic(&cli_options);
|
||||
// → Updates config table with CLI overrides
|
||||
// → Separate transaction after complete config exists
|
||||
|
||||
// 5. Secure Key Storage
|
||||
store_relay_private_key(relay_privkey);
|
||||
|
||||
// 6. Cache Initialization
|
||||
refresh_unified_cache_from_table();
|
||||
}
|
||||
```
|
||||
|
||||
### New Functions Needed
|
||||
|
||||
```c
|
||||
// In config.c
|
||||
int populate_all_config_values_atomic(const cli_options_t* cli_options) {
|
||||
// BEGIN TRANSACTION
|
||||
// Insert ALL defaults from DEFAULT_CONFIG_VALUES[]
|
||||
// Insert admin_pubkey from g_unified_cache
|
||||
// Insert relay_pubkey from g_unified_cache
|
||||
// COMMIT TRANSACTION
|
||||
return 0;
|
||||
}
|
||||
|
||||
int apply_cli_overrides_atomic(const cli_options_t* cli_options) {
|
||||
// BEGIN TRANSACTION
|
||||
// IF port_override: UPDATE config SET value = ? WHERE key = 'relay_port'
|
||||
// IF admin_pubkey_override: UPDATE config SET value = ? WHERE key = 'admin_pubkey'
|
||||
// IF relay_privkey_override: UPDATE config SET value = ? WHERE key = 'relay_privkey'
|
||||
// COMMIT TRANSACTION
|
||||
// invalidate_config_cache()
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scenario 2: Restart with Existing Database + CLI Options
|
||||
|
||||
### Sequence
|
||||
|
||||
```
|
||||
1. Database Discovery Phase
|
||||
├─ find_existing_db_files() → ["<relay_pubkey>.db"]
|
||||
├─ extract_pubkey_from_filename() → relay_pubkey
|
||||
└─ Sets g_database_path = "<relay_pubkey>.db"
|
||||
|
||||
2. Database Initialization Phase
|
||||
└─ init_database(g_database_path)
|
||||
└─ Opens existing database
|
||||
|
||||
3. Config Validation Phase
|
||||
└─ validate_config_table_completeness()
|
||||
├─ Check if all required keys exist
|
||||
└─ IF missing keys: populate_missing_config_values()
|
||||
|
||||
4. CLI Override Phase (ATOMIC)
|
||||
├─ BEGIN TRANSACTION
|
||||
├─ apply_cli_overrides()
|
||||
│ └─ UPDATE config SET value = ? WHERE key = ?
|
||||
└─ COMMIT TRANSACTION
|
||||
|
||||
5. Cache Initialization Phase
|
||||
└─ refresh_unified_cache_from_table()
|
||||
└─ Loads complete config into g_unified_cache
|
||||
```
|
||||
|
||||
### Function Call Sequence
|
||||
|
||||
```c
|
||||
// In main.c - existing relay branch
|
||||
else {
|
||||
// 1. Database Discovery
|
||||
char** existing_files = find_existing_db_files();
|
||||
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
|
||||
startup_existing_relay(relay_pubkey);
|
||||
// → Sets g_database_path
|
||||
|
||||
// 2. Database Initialization
|
||||
init_database(g_database_path);
|
||||
|
||||
// 3. Config Validation (NEW FUNCTION)
|
||||
validate_config_table_completeness();
|
||||
// → Checks for missing keys
|
||||
// → Populates any missing defaults
|
||||
|
||||
// 4. CLI Override Phase (REUSE FUNCTION)
|
||||
if (has_cli_overrides(&cli_options)) {
|
||||
apply_cli_overrides_atomic(&cli_options);
|
||||
}
|
||||
|
||||
// 5. Cache Initialization
|
||||
refresh_unified_cache_from_table();
|
||||
}
|
||||
```
|
||||
|
||||
### New Functions Needed
|
||||
|
||||
```c
|
||||
// In config.c
|
||||
int validate_config_table_completeness(void) {
|
||||
// Check if all DEFAULT_CONFIG_VALUES keys exist
|
||||
// IF missing: populate_missing_config_values()
|
||||
return 0;
|
||||
}
|
||||
|
||||
int populate_missing_config_values(void) {
|
||||
// BEGIN TRANSACTION
|
||||
// For each key in DEFAULT_CONFIG_VALUES:
|
||||
// IF NOT EXISTS: INSERT INTO config
|
||||
// COMMIT TRANSACTION
|
||||
return 0;
|
||||
}
|
||||
|
||||
int has_cli_overrides(const cli_options_t* cli_options) {
|
||||
return (cli_options->port_override > 0 ||
|
||||
cli_options->admin_pubkey_override[0] != '\0' ||
|
||||
cli_options->relay_privkey_override[0] != '\0');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scenario 3: Restart with Existing Database + No CLI Options
|
||||
|
||||
### Sequence
|
||||
|
||||
```
|
||||
1. Database Discovery Phase
|
||||
├─ find_existing_db_files() → ["<relay_pubkey>.db"]
|
||||
├─ extract_pubkey_from_filename() → relay_pubkey
|
||||
└─ Sets g_database_path = "<relay_pubkey>.db"
|
||||
|
||||
2. Database Initialization Phase
|
||||
└─ init_database(g_database_path)
|
||||
└─ Opens existing database
|
||||
|
||||
3. Config Validation Phase
|
||||
└─ validate_config_table_completeness()
|
||||
├─ Check if all required keys exist
|
||||
└─ IF missing keys: populate_missing_config_values()
|
||||
|
||||
4. Cache Initialization Phase (IMMEDIATE)
|
||||
└─ refresh_unified_cache_from_table()
|
||||
└─ Loads complete config into g_unified_cache
|
||||
```
|
||||
|
||||
### Function Call Sequence
|
||||
|
||||
```c
|
||||
// In main.c - existing relay branch (no CLI overrides)
|
||||
else {
|
||||
// 1. Database Discovery
|
||||
char** existing_files = find_existing_db_files();
|
||||
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
|
||||
startup_existing_relay(relay_pubkey);
|
||||
|
||||
// 2. Database Initialization
|
||||
init_database(g_database_path);
|
||||
|
||||
// 3. Config Validation
|
||||
validate_config_table_completeness();
|
||||
|
||||
// 4. Cache Initialization (IMMEDIATE - no overrides to apply)
|
||||
refresh_unified_cache_from_table();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### 1. Atomic Config Creation
|
||||
|
||||
**Before:**
|
||||
```c
|
||||
populate_default_config_values(); // Step 1
|
||||
update_config_in_table("relay_port", port_str); // Step 2
|
||||
add_pubkeys_to_config_table(); // Step 3
|
||||
```
|
||||
|
||||
**After:**
|
||||
```c
|
||||
populate_all_config_values_atomic(&cli_options); // Single transaction
|
||||
apply_cli_overrides_atomic(&cli_options); // Separate transaction
|
||||
```
|
||||
|
||||
### 2. Elimination of Race Conditions
|
||||
|
||||
**Before:**
|
||||
- Cache could refresh between steps 1-3
|
||||
- Incomplete config could be read
|
||||
|
||||
**After:**
|
||||
- Config created atomically
|
||||
- Cache only refreshed after complete config exists
|
||||
|
||||
### 3. Unified Code Path
|
||||
|
||||
**Before:**
|
||||
- Different logic for first-time vs restart
|
||||
- `populate_default_config_values()` vs `add_pubkeys_to_config_table()`
|
||||
|
||||
**After:**
|
||||
- Same validation logic for both scenarios
|
||||
- `validate_config_table_completeness()` handles both cases
|
||||
|
||||
### 4. Clear Separation of Concerns
|
||||
|
||||
**Before:**
|
||||
- CLI overrides mixed with default population
|
||||
- Unclear when overrides are applied
|
||||
|
||||
**After:**
|
||||
- Phase 1: Complete config creation
|
||||
- Phase 2: CLI overrides (if any)
|
||||
- Phase 3: Cache initialization
|
||||
|
||||
---
|
||||
|
||||
## Implementation Changes Required
|
||||
|
||||
### 1. New Functions in config.c
|
||||
|
||||
```c
|
||||
// Atomic config population for first-time startup
|
||||
int populate_all_config_values_atomic(const cli_options_t* cli_options);
|
||||
|
||||
// Atomic CLI override application
|
||||
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
|
||||
|
||||
// Config validation for existing databases
|
||||
int validate_config_table_completeness(void);
|
||||
int populate_missing_config_values(void);
|
||||
|
||||
// Helper function
|
||||
int has_cli_overrides(const cli_options_t* cli_options);
|
||||
```
|
||||
|
||||
### 2. Modified Functions in config.c
|
||||
|
||||
```c
|
||||
// Simplify to only generate keys and set database path
|
||||
int first_time_startup_sequence(const cli_options_t* cli_options);
|
||||
|
||||
// Remove config population logic
|
||||
int add_pubkeys_to_config_table(void); // DEPRECATED - logic moved to populate_all_config_values_atomic()
|
||||
```
|
||||
|
||||
### 3. Modified Startup Flow in main.c
|
||||
|
||||
```c
|
||||
// First-time startup
|
||||
if (is_first_time_startup()) {
|
||||
first_time_startup_sequence(&cli_options);
|
||||
init_database(g_database_path);
|
||||
populate_all_config_values_atomic(&cli_options); // NEW
|
||||
apply_cli_overrides_atomic(&cli_options); // NEW
|
||||
store_relay_private_key(relay_privkey);
|
||||
refresh_unified_cache_from_table();
|
||||
}
|
||||
|
||||
// Existing relay
|
||||
else {
|
||||
startup_existing_relay(relay_pubkey);
|
||||
init_database(g_database_path);
|
||||
validate_config_table_completeness(); // NEW
|
||||
if (has_cli_overrides(&cli_options)) {
|
||||
apply_cli_overrides_atomic(&cli_options); // NEW
|
||||
}
|
||||
refresh_unified_cache_from_table();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Atomicity**: Config creation is atomic - no partial states
|
||||
2. **Simplicity**: Clear phases with single responsibility
|
||||
3. **Safety**: Cache only loaded after complete config exists
|
||||
4. **Consistency**: Same validation logic for all scenarios
|
||||
5. **Maintainability**: Easier to understand and modify
|
||||
6. **Testability**: Each phase can be tested independently
|
||||
|
||||
---
|
||||
|
||||
## Migration Path
|
||||
|
||||
1. Implement new functions in config.c
|
||||
2. Update main.c startup flow
|
||||
3. Test first-time startup scenario
|
||||
4. Test restart with CLI overrides
|
||||
5. Test restart without CLI overrides
|
||||
6. Remove deprecated functions
|
||||
7. Update documentation
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Cases
|
||||
|
||||
1. **First-time startup with defaults**
|
||||
- Verify all config values created atomically
|
||||
- Verify cache loads complete config
|
||||
|
||||
2. **First-time startup with port override**
|
||||
- Verify defaults created first
|
||||
- Verify port override applied second
|
||||
- Verify cache reflects override
|
||||
|
||||
3. **Restart with complete config**
|
||||
- Verify no config changes
|
||||
- Verify cache loads immediately
|
||||
|
||||
4. **Restart with missing config keys**
|
||||
- Verify missing keys populated
|
||||
- Verify existing keys unchanged
|
||||
|
||||
5. **Restart with CLI overrides**
|
||||
- Verify overrides applied atomically
|
||||
- Verify cache invalidated and refreshed
|
||||
|
||||
### Validation Points
|
||||
|
||||
- Config table row count after each phase
|
||||
- Cache validity state after each phase
|
||||
- Transaction boundaries (BEGIN/COMMIT)
|
||||
- Error handling for failed transactions
|
||||
746
docs/unified_startup_implementation_plan.md
Normal file
746
docs/unified_startup_implementation_plan.md
Normal file
@@ -0,0 +1,746 @@
|
||||
# Unified Startup Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a detailed implementation plan for refactoring the startup sequence to use atomic config creation followed by CLI overrides. This plan breaks down the work into discrete, testable steps.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Create New Functions in config.c
|
||||
|
||||
### Step 1.1: Implement `populate_all_config_values_atomic()`
|
||||
|
||||
**Location**: `src/config.c`
|
||||
|
||||
**Purpose**: Create complete config table in single transaction for first-time startup
|
||||
|
||||
**Function Signature**:
|
||||
```c
|
||||
int populate_all_config_values_atomic(const cli_options_t* cli_options);
|
||||
```
|
||||
|
||||
**Implementation Details**:
|
||||
```c
|
||||
int populate_all_config_values_atomic(const cli_options_t* cli_options) {
|
||||
if (!g_database) {
|
||||
DEBUG_ERROR("Database not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Begin transaction
|
||||
char* err_msg = NULL;
|
||||
int rc = sqlite3_exec(g_database, "BEGIN TRANSACTION;", NULL, NULL, &err_msg);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to begin transaction: %s", err_msg);
|
||||
sqlite3_free(err_msg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Prepare INSERT statement
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "INSERT INTO config (key, value) VALUES (?, ?)";
|
||||
rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Insert all default config values
|
||||
for (size_t i = 0; i < sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]); i++) {
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_bind_text(stmt, 1, DEFAULT_CONFIG_VALUES[i].key, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, DEFAULT_CONFIG_VALUES[i].value, -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR("Failed to insert config key '%s': %s",
|
||||
DEFAULT_CONFIG_VALUES[i].key, sqlite3_errmsg(g_database));
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert admin_pubkey from cache
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, g_unified_cache.admin_pubkey, -1, SQLITE_STATIC);
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR("Failed to insert admin_pubkey: %s", sqlite3_errmsg(g_database));
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Insert relay_pubkey from cache
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_bind_text(stmt, 1, "relay_pubkey", -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, g_unified_cache.relay_pubkey, -1, SQLITE_STATIC);
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR("Failed to insert relay_pubkey: %s", sqlite3_errmsg(g_database));
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// Commit transaction
|
||||
rc = sqlite3_exec(g_database, "COMMIT;", NULL, NULL, &err_msg);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to commit transaction: %s", err_msg);
|
||||
sqlite3_free(err_msg);
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
DEBUG_INFO("Successfully populated all config values atomically");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
- Verify transaction atomicity (all or nothing)
|
||||
- Verify all DEFAULT_CONFIG_VALUES inserted
|
||||
- Verify admin_pubkey and relay_pubkey inserted
|
||||
- Verify error handling on failure
|
||||
|
||||
---
|
||||
|
||||
### Step 1.2: Implement `apply_cli_overrides_atomic()`
|
||||
|
||||
**Location**: `src/config.c`
|
||||
|
||||
**Purpose**: Apply CLI overrides to existing config table in single transaction
|
||||
|
||||
**Function Signature**:
|
||||
```c
|
||||
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
|
||||
```
|
||||
|
||||
**Implementation Details**:
|
||||
```c
|
||||
int apply_cli_overrides_atomic(const cli_options_t* cli_options) {
|
||||
if (!g_database) {
|
||||
DEBUG_ERROR("Database not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cli_options) {
|
||||
DEBUG_ERROR("CLI options is NULL");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if any overrides exist
|
||||
bool has_overrides = false;
|
||||
if (cli_options->port_override > 0) has_overrides = true;
|
||||
if (cli_options->admin_pubkey_override[0] != '\0') has_overrides = true;
|
||||
if (cli_options->relay_privkey_override[0] != '\0') has_overrides = true;
|
||||
|
||||
if (!has_overrides) {
|
||||
DEBUG_INFO("No CLI overrides to apply");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Begin transaction
|
||||
char* err_msg = NULL;
|
||||
int rc = sqlite3_exec(g_database, "BEGIN TRANSACTION;", NULL, NULL, &err_msg);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to begin transaction: %s", err_msg);
|
||||
sqlite3_free(err_msg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Prepare UPDATE statement
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "UPDATE config SET value = ? WHERE key = ?";
|
||||
rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Apply port override
|
||||
if (cli_options->port_override > 0) {
|
||||
char port_str[16];
|
||||
snprintf(port_str, sizeof(port_str), "%d", cli_options->port_override);
|
||||
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_bind_text(stmt, 1, port_str, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, "relay_port", -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR("Failed to update relay_port: %s", sqlite3_errmsg(g_database));
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
DEBUG_INFO("Applied CLI override: relay_port = %s", port_str);
|
||||
}
|
||||
|
||||
// Apply admin_pubkey override
|
||||
if (cli_options->admin_pubkey_override[0] != '\0') {
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_bind_text(stmt, 1, cli_options->admin_pubkey_override, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, "admin_pubkey", -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR("Failed to update admin_pubkey: %s", sqlite3_errmsg(g_database));
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
DEBUG_INFO("Applied CLI override: admin_pubkey");
|
||||
}
|
||||
|
||||
// Apply relay_privkey override
|
||||
if (cli_options->relay_privkey_override[0] != '\0') {
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_bind_text(stmt, 1, cli_options->relay_privkey_override, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, "relay_privkey", -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR("Failed to update relay_privkey: %s", sqlite3_errmsg(g_database));
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
DEBUG_INFO("Applied CLI override: relay_privkey");
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// Commit transaction
|
||||
rc = sqlite3_exec(g_database, "COMMIT;", NULL, NULL, &err_msg);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to commit transaction: %s", err_msg);
|
||||
sqlite3_free(err_msg);
|
||||
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Invalidate cache to force refresh
|
||||
invalidate_config_cache();
|
||||
|
||||
DEBUG_INFO("Successfully applied CLI overrides atomically");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
- Verify transaction atomicity
|
||||
- Verify each override type (port, admin_pubkey, relay_privkey)
|
||||
- Verify cache invalidation after overrides
|
||||
- Verify no-op when no overrides present
|
||||
|
||||
---
|
||||
|
||||
### Step 1.3: Implement `validate_config_table_completeness()`
|
||||
|
||||
**Location**: `src/config.c`
|
||||
|
||||
**Purpose**: Validate config table has all required keys, populate missing ones
|
||||
|
||||
**Function Signature**:
|
||||
```c
|
||||
int validate_config_table_completeness(void);
|
||||
```
|
||||
|
||||
**Implementation Details**:
|
||||
```c
|
||||
int validate_config_table_completeness(void) {
|
||||
if (!g_database) {
|
||||
DEBUG_ERROR("Database not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
DEBUG_INFO("Validating config table completeness");
|
||||
|
||||
// Check each default config key
|
||||
for (size_t i = 0; i < sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]); i++) {
|
||||
const char* key = DEFAULT_CONFIG_VALUES[i].key;
|
||||
|
||||
// Check if key exists
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT COUNT(*) FROM config WHERE key = ?";
|
||||
int rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
||||
rc = sqlite3_step(stmt);
|
||||
|
||||
int count = 0;
|
||||
if (rc == SQLITE_ROW) {
|
||||
count = sqlite3_column_int(stmt, 0);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// If key missing, populate it
|
||||
if (count == 0) {
|
||||
DEBUG_WARN("Config key '%s' missing, populating with default", key);
|
||||
rc = populate_missing_config_key(key, DEFAULT_CONFIG_VALUES[i].value);
|
||||
if (rc != 0) {
|
||||
DEBUG_ERROR("Failed to populate missing key '%s'", key);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("Config table validation complete");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Helper Function**:
|
||||
```c
|
||||
static int populate_missing_config_key(const char* key, const char* value) {
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "INSERT INTO config (key, value) VALUES (?, ?)";
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR("Failed to insert config key '%s': %s", key, sqlite3_errmsg(g_database));
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
- Verify detection of missing keys
|
||||
- Verify population of missing keys with defaults
|
||||
- Verify no changes when all keys present
|
||||
- Verify error handling
|
||||
|
||||
---
|
||||
|
||||
### Step 1.4: Implement `has_cli_overrides()`
|
||||
|
||||
**Location**: `src/config.c`
|
||||
|
||||
**Purpose**: Check if any CLI overrides are present
|
||||
|
||||
**Function Signature**:
|
||||
```c
|
||||
bool has_cli_overrides(const cli_options_t* cli_options);
|
||||
```
|
||||
|
||||
**Implementation Details**:
|
||||
```c
|
||||
bool has_cli_overrides(const cli_options_t* cli_options) {
|
||||
if (!cli_options) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (cli_options->port_override > 0 ||
|
||||
cli_options->admin_pubkey_override[0] != '\0' ||
|
||||
cli_options->relay_privkey_override[0] != '\0');
|
||||
}
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
- Verify returns true when any override present
|
||||
- Verify returns false when no overrides
|
||||
- Verify NULL safety
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Update Function Declarations in config.h
|
||||
|
||||
### Step 2.1: Add New Function Declarations
|
||||
|
||||
**Location**: `src/config.h`
|
||||
|
||||
**Changes**:
|
||||
```c
|
||||
// Add after existing function declarations
|
||||
|
||||
// Atomic config population for first-time startup
|
||||
int populate_all_config_values_atomic(const cli_options_t* cli_options);
|
||||
|
||||
// Atomic CLI override application
|
||||
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
|
||||
|
||||
// Config validation for existing databases
|
||||
int validate_config_table_completeness(void);
|
||||
|
||||
// Helper function to check for CLI overrides
|
||||
bool has_cli_overrides(const cli_options_t* cli_options);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Refactor Startup Flow in main.c
|
||||
|
||||
### Step 3.1: Update First-Time Startup Branch
|
||||
|
||||
**Location**: `src/main.c` (around lines 1624-1740)
|
||||
|
||||
**Current Code**:
|
||||
```c
|
||||
if (is_first_time_startup()) {
|
||||
first_time_startup_sequence(&cli_options);
|
||||
init_database(g_database_path);
|
||||
|
||||
// Current incremental approach
|
||||
populate_default_config_values();
|
||||
if (cli_options.port_override > 0) {
|
||||
char port_str[16];
|
||||
snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override);
|
||||
update_config_in_table("relay_port", port_str);
|
||||
}
|
||||
add_pubkeys_to_config_table();
|
||||
|
||||
store_relay_private_key(relay_privkey);
|
||||
refresh_unified_cache_from_table();
|
||||
}
|
||||
```
|
||||
|
||||
**New Code**:
|
||||
```c
|
||||
if (is_first_time_startup()) {
|
||||
// 1. Generate keys and set database path
|
||||
first_time_startup_sequence(&cli_options);
|
||||
|
||||
// 2. Create database with schema
|
||||
init_database(g_database_path);
|
||||
|
||||
// 3. Populate ALL config values atomically (defaults + pubkeys)
|
||||
if (populate_all_config_values_atomic(&cli_options) != 0) {
|
||||
DEBUG_ERROR("Failed to populate config values");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// 4. Apply CLI overrides atomically (separate transaction)
|
||||
if (apply_cli_overrides_atomic(&cli_options) != 0) {
|
||||
DEBUG_ERROR("Failed to apply CLI overrides");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// 5. Store relay private key securely
|
||||
store_relay_private_key(relay_privkey);
|
||||
|
||||
// 6. Load complete config into cache
|
||||
refresh_unified_cache_from_table();
|
||||
}
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
- Verify first-time startup creates complete config
|
||||
- Verify CLI overrides applied correctly
|
||||
- Verify cache loads complete config
|
||||
- Verify error handling at each step
|
||||
|
||||
---
|
||||
|
||||
### Step 3.2: Update Existing Relay Startup Branch
|
||||
|
||||
**Location**: `src/main.c` (around lines 1741-1928)
|
||||
|
||||
**Current Code**:
|
||||
```c
|
||||
else {
|
||||
char** existing_files = find_existing_db_files();
|
||||
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
|
||||
startup_existing_relay(relay_pubkey);
|
||||
|
||||
init_database(g_database_path);
|
||||
|
||||
// Current approach - unclear when overrides applied
|
||||
populate_default_config_values();
|
||||
if (cli_options.port_override > 0) {
|
||||
// ... override logic ...
|
||||
}
|
||||
|
||||
refresh_unified_cache_from_table();
|
||||
}
|
||||
```
|
||||
|
||||
**New Code**:
|
||||
```c
|
||||
else {
|
||||
// 1. Discover existing database
|
||||
char** existing_files = find_existing_db_files();
|
||||
if (!existing_files || !existing_files[0]) {
|
||||
DEBUG_ERROR("No existing database files found");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
|
||||
startup_existing_relay(relay_pubkey);
|
||||
|
||||
// 2. Open existing database
|
||||
init_database(g_database_path);
|
||||
|
||||
// 3. Validate config table completeness (populate missing keys)
|
||||
if (validate_config_table_completeness() != 0) {
|
||||
DEBUG_ERROR("Failed to validate config table");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// 4. Apply CLI overrides if present (separate transaction)
|
||||
if (has_cli_overrides(&cli_options)) {
|
||||
if (apply_cli_overrides_atomic(&cli_options) != 0) {
|
||||
DEBUG_ERROR("Failed to apply CLI overrides");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Load complete config into cache
|
||||
refresh_unified_cache_from_table();
|
||||
}
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
- Verify existing relay startup with complete config
|
||||
- Verify missing keys populated
|
||||
- Verify CLI overrides applied when present
|
||||
- Verify no changes when no overrides
|
||||
- Verify cache loads correctly
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Deprecate Old Functions
|
||||
|
||||
### Step 4.1: Mark Functions as Deprecated
|
||||
|
||||
**Location**: `src/config.c`
|
||||
|
||||
**Functions to Deprecate**:
|
||||
1. `populate_default_config_values()` - replaced by `populate_all_config_values_atomic()`
|
||||
2. `add_pubkeys_to_config_table()` - logic moved to `populate_all_config_values_atomic()`
|
||||
|
||||
**Changes**:
|
||||
```c
|
||||
// Mark as deprecated in comments
|
||||
// DEPRECATED: Use populate_all_config_values_atomic() instead
|
||||
// This function will be removed in a future version
|
||||
int populate_default_config_values(void) {
|
||||
// ... existing implementation ...
|
||||
}
|
||||
|
||||
// DEPRECATED: Use populate_all_config_values_atomic() instead
|
||||
// This function will be removed in a future version
|
||||
int add_pubkeys_to_config_table(void) {
|
||||
// ... existing implementation ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
1. **Test `populate_all_config_values_atomic()`**
|
||||
- Test with valid cli_options
|
||||
- Test transaction rollback on error
|
||||
- Test all config keys inserted
|
||||
- Test pubkeys inserted correctly
|
||||
|
||||
2. **Test `apply_cli_overrides_atomic()`**
|
||||
- Test port override
|
||||
- Test admin_pubkey override
|
||||
- Test relay_privkey override
|
||||
- Test multiple overrides
|
||||
- Test no overrides
|
||||
- Test transaction rollback on error
|
||||
|
||||
3. **Test `validate_config_table_completeness()`**
|
||||
- Test with complete config
|
||||
- Test with missing keys
|
||||
- Test population of missing keys
|
||||
|
||||
4. **Test `has_cli_overrides()`**
|
||||
- Test with each override type
|
||||
- Test with no overrides
|
||||
- Test with NULL cli_options
|
||||
|
||||
### Integration Tests
|
||||
|
||||
1. **First-Time Startup**
|
||||
```bash
|
||||
# Clean environment
|
||||
rm -f *.db
|
||||
|
||||
# Start relay with defaults
|
||||
./build/c_relay_x86
|
||||
|
||||
# Verify config table complete
|
||||
sqlite3 <relay_pubkey>.db "SELECT COUNT(*) FROM config;"
|
||||
# Expected: 20+ rows (all defaults + pubkeys)
|
||||
|
||||
# Verify cache loaded
|
||||
# Check relay.log for cache refresh message
|
||||
```
|
||||
|
||||
2. **First-Time Startup with CLI Overrides**
|
||||
```bash
|
||||
# Clean environment
|
||||
rm -f *.db
|
||||
|
||||
# Start relay with port override
|
||||
./build/c_relay_x86 --port 9999
|
||||
|
||||
# Verify port override applied
|
||||
sqlite3 <relay_pubkey>.db "SELECT value FROM config WHERE key='relay_port';"
|
||||
# Expected: 9999
|
||||
```
|
||||
|
||||
3. **Restart with Existing Database**
|
||||
```bash
|
||||
# Start relay (creates database)
|
||||
./build/c_relay_x86
|
||||
|
||||
# Stop relay
|
||||
pkill -f c_relay_
|
||||
|
||||
# Restart relay
|
||||
./build/c_relay_x86
|
||||
|
||||
# Verify config unchanged
|
||||
# Check relay.log for validation message
|
||||
```
|
||||
|
||||
4. **Restart with CLI Overrides**
|
||||
```bash
|
||||
# Start relay (creates database)
|
||||
./build/c_relay_x86
|
||||
|
||||
# Stop relay
|
||||
pkill -f c_relay_
|
||||
|
||||
# Restart with port override
|
||||
./build/c_relay_x86 --port 9999
|
||||
|
||||
# Verify port override applied
|
||||
sqlite3 <relay_pubkey>.db "SELECT value FROM config WHERE key='relay_port';"
|
||||
# Expected: 9999
|
||||
```
|
||||
|
||||
### Regression Tests
|
||||
|
||||
Run existing test suite to ensure no breakage:
|
||||
```bash
|
||||
./tests/run_all_tests.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Documentation Updates
|
||||
|
||||
### Files to Update
|
||||
|
||||
1. **docs/configuration_guide.md**
|
||||
- Update startup sequence description
|
||||
- Document new atomic config creation
|
||||
- Document CLI override behavior
|
||||
|
||||
2. **docs/startup_flows_complete.md**
|
||||
- Update with new flow diagrams
|
||||
- Document new function calls
|
||||
|
||||
3. **README.md**
|
||||
- Update CLI options documentation
|
||||
- Document override behavior
|
||||
|
||||
---
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
### Week 1: Core Functions
|
||||
- Day 1-2: Implement `populate_all_config_values_atomic()`
|
||||
- Day 3-4: Implement `apply_cli_overrides_atomic()`
|
||||
- Day 5: Implement `validate_config_table_completeness()` and `has_cli_overrides()`
|
||||
|
||||
### Week 2: Integration
|
||||
- Day 1-2: Update main.c startup flow
|
||||
- Day 3-4: Testing and bug fixes
|
||||
- Day 5: Documentation updates
|
||||
|
||||
### Week 3: Cleanup
|
||||
- Day 1-2: Deprecate old functions
|
||||
- Day 3-4: Final testing and validation
|
||||
- Day 5: Code review and merge
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Potential Issues
|
||||
|
||||
1. **Database Lock Contention**
|
||||
- Risk: Multiple transactions could cause locks
|
||||
- Mitigation: Use BEGIN IMMEDIATE for write transactions
|
||||
|
||||
2. **Cache Invalidation Timing**
|
||||
- Risk: Cache could be read before overrides applied
|
||||
- Mitigation: Invalidate cache immediately after overrides
|
||||
|
||||
3. **Backward Compatibility**
|
||||
- Risk: Existing databases might have incomplete config
|
||||
- Mitigation: `validate_config_table_completeness()` handles this
|
||||
|
||||
4. **Transaction Rollback**
|
||||
- Risk: Partial config on error
|
||||
- Mitigation: All operations in transactions with proper rollback
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. ✅ All config values created atomically in first-time startup
|
||||
2. ✅ CLI overrides applied in separate atomic transaction
|
||||
3. ✅ Existing databases validated and missing keys populated
|
||||
4. ✅ Cache only loaded after complete config exists
|
||||
5. ✅ All existing tests pass
|
||||
6. ✅ No race conditions in config creation
|
||||
7. ✅ Clear separation between config creation and override phases
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise during implementation:
|
||||
|
||||
1. **Revert main.c changes** - restore original startup flow
|
||||
2. **Keep new functions** - they can coexist with old code
|
||||
3. **Add feature flag** - allow toggling between old and new behavior
|
||||
4. **Gradual migration** - enable new behavior per scenario
|
||||
|
||||
```c
|
||||
// Feature flag approach
|
||||
#define USE_ATOMIC_CONFIG_CREATION 1
|
||||
|
||||
#if USE_ATOMIC_CONFIG_CREATION
|
||||
// New atomic approach
|
||||
populate_all_config_values_atomic(&cli_options);
|
||||
apply_cli_overrides_atomic(&cli_options);
|
||||
#else
|
||||
// Old incremental approach
|
||||
populate_default_config_values();
|
||||
// ... existing code ...
|
||||
#endif
|
||||
```
|
||||
200
docs/websocket_write_queue_design.md
Normal file
200
docs/websocket_write_queue_design.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# WebSocket Write Queue Design
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The current partial write handling implementation uses a single buffer per session, which fails when multiple events need to be sent to the same client in rapid succession. This causes:
|
||||
|
||||
1. First event gets partial write → queued successfully
|
||||
2. Second event tries to write → **FAILS** with "write already pending"
|
||||
3. Subsequent events fail similarly, causing data loss
|
||||
|
||||
### Server Log Evidence
|
||||
```
|
||||
[WARN] WS_FRAME_PARTIAL: EVENT partial write, sub=1 sent=3210 expected=5333
|
||||
[TRACE] Queued partial write: len=2123
|
||||
[WARN] WS_FRAME_PARTIAL: EVENT partial write, sub=1 sent=3210 expected=5333
|
||||
[WARN] queue_websocket_write: write already pending, cannot queue new write
|
||||
[ERROR] Failed to queue partial EVENT write for sub=1
|
||||
```
|
||||
|
||||
## Root Cause
|
||||
|
||||
WebSocket frames must be sent **atomically** - you cannot interleave multiple frames. The current single-buffer approach correctly enforces this, but it rejects new writes instead of queuing them.
|
||||
|
||||
## Solution: Write Queue Architecture
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Frame Atomicity**: Complete one WebSocket frame before starting the next
|
||||
2. **Sequential Processing**: Process queued writes in FIFO order
|
||||
3. **Memory Safety**: Proper cleanup on connection close or errors
|
||||
4. **Thread Safety**: Protect queue operations with existing session lock
|
||||
|
||||
### Data Structures
|
||||
|
||||
#### Write Queue Node
|
||||
```c
|
||||
struct write_queue_node {
|
||||
unsigned char* buffer; // Buffer with LWS_PRE space
|
||||
size_t total_len; // Total length of data to write
|
||||
size_t offset; // How much has been written so far
|
||||
int write_type; // LWS_WRITE_TEXT, etc.
|
||||
struct write_queue_node* next; // Next node in queue
|
||||
};
|
||||
```
|
||||
|
||||
#### Per-Session Write Queue
|
||||
```c
|
||||
struct per_session_data {
|
||||
// ... existing fields ...
|
||||
|
||||
// Write queue for handling multiple pending writes
|
||||
struct write_queue_node* write_queue_head; // First item to write
|
||||
struct write_queue_node* write_queue_tail; // Last item in queue
|
||||
int write_queue_length; // Number of items in queue
|
||||
int write_in_progress; // Flag: 1 if currently writing
|
||||
};
|
||||
```
|
||||
|
||||
### Algorithm Flow
|
||||
|
||||
#### 1. Enqueue Write (`queue_websocket_write`)
|
||||
|
||||
```
|
||||
IF write_queue is empty AND no write in progress:
|
||||
- Attempt immediate write with lws_write()
|
||||
- IF complete:
|
||||
- Return success
|
||||
- ELSE (partial write):
|
||||
- Create queue node with remaining data
|
||||
- Add to queue
|
||||
- Set write_in_progress flag
|
||||
- Request LWS_CALLBACK_SERVER_WRITEABLE
|
||||
ELSE:
|
||||
- Create queue node with full data
|
||||
- Append to queue tail
|
||||
- IF no write in progress:
|
||||
- Request LWS_CALLBACK_SERVER_WRITEABLE
|
||||
```
|
||||
|
||||
#### 2. Process Queue (`process_pending_write`)
|
||||
|
||||
```
|
||||
WHILE write_queue is not empty:
|
||||
- Get head node
|
||||
- Calculate remaining data (total_len - offset)
|
||||
- Attempt write with lws_write()
|
||||
|
||||
IF write fails (< 0):
|
||||
- Log error
|
||||
- Remove and free head node
|
||||
- Continue to next node
|
||||
|
||||
ELSE IF partial write (< remaining):
|
||||
- Update offset
|
||||
- Request LWS_CALLBACK_SERVER_WRITEABLE
|
||||
- Break (wait for next callback)
|
||||
|
||||
ELSE (complete write):
|
||||
- Remove and free head node
|
||||
- Continue to next node
|
||||
|
||||
IF queue is empty:
|
||||
- Clear write_in_progress flag
|
||||
```
|
||||
|
||||
#### 3. Cleanup (`LWS_CALLBACK_CLOSED`)
|
||||
|
||||
```
|
||||
WHILE write_queue is not empty:
|
||||
- Get head node
|
||||
- Free buffer
|
||||
- Free node
|
||||
- Move to next
|
||||
Clear queue pointers
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
1. **Allocation**: Each queue node allocates buffer with `LWS_PRE + data_len`
|
||||
2. **Ownership**: Queue owns all buffers until write completes or connection closes
|
||||
3. **Deallocation**: Free buffer and node when:
|
||||
- Write completes successfully
|
||||
- Write fails with error
|
||||
- Connection closes
|
||||
|
||||
### Thread Safety
|
||||
|
||||
- Use existing `pss->session_lock` to protect queue operations
|
||||
- Lock during:
|
||||
- Enqueue operations
|
||||
- Dequeue operations
|
||||
- Queue traversal for cleanup
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Queue Length Limit**: Implement max queue length (e.g., 100 items) to prevent memory exhaustion
|
||||
2. **Memory Pressure**: Monitor total queued bytes per session
|
||||
3. **Backpressure**: If queue exceeds limit, close connection with NOTICE
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **Allocation Failure**: Return error, log, send NOTICE to client
|
||||
2. **Write Failure**: Remove failed frame, continue with next
|
||||
3. **Queue Overflow**: Close connection with appropriate NOTICE
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Data Structure Changes
|
||||
1. Add `write_queue_node` structure to `websockets.h`
|
||||
2. Update `per_session_data` with queue fields
|
||||
3. Remove old single-buffer fields
|
||||
|
||||
### Phase 2: Queue Operations
|
||||
1. Implement `enqueue_write()` helper
|
||||
2. Implement `dequeue_write()` helper
|
||||
3. Update `queue_websocket_write()` to use queue
|
||||
4. Update `process_pending_write()` to process queue
|
||||
|
||||
### Phase 3: Integration
|
||||
1. Update all `lws_write()` call sites
|
||||
2. Update `LWS_CALLBACK_CLOSED` cleanup
|
||||
3. Add queue length monitoring
|
||||
|
||||
### Phase 4: Testing
|
||||
1. Test with rapid multiple events to same client
|
||||
2. Test with large events (>4KB)
|
||||
3. Test under load with concurrent connections
|
||||
4. Verify no "Invalid frame header" errors
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
1. **No More Rejections**: All writes queued successfully
|
||||
2. **Frame Integrity**: Complete frames sent atomically
|
||||
3. **Memory Safety**: Proper cleanup on all paths
|
||||
4. **Performance**: Minimal overhead for queue management
|
||||
|
||||
## Metrics to Monitor
|
||||
|
||||
1. Average queue length per session
|
||||
2. Maximum queue length observed
|
||||
3. Queue overflow events (if limit implemented)
|
||||
4. Write completion rate
|
||||
5. Partial write frequency
|
||||
|
||||
## Alternative Approaches Considered
|
||||
|
||||
### 1. Larger Single Buffer
|
||||
**Rejected**: Doesn't solve the fundamental problem of multiple concurrent writes
|
||||
|
||||
### 2. Immediate Write Retry
|
||||
**Rejected**: Could cause busy-waiting and CPU waste
|
||||
|
||||
### 3. Drop Frames on Conflict
|
||||
**Rejected**: Violates reliability requirements
|
||||
|
||||
## References
|
||||
|
||||
- libwebsockets documentation on `lws_write()` and `LWS_CALLBACK_SERVER_WRITEABLE`
|
||||
- WebSocket RFC 6455 on frame structure
|
||||
- Nostr NIP-01 on relay-to-client communication
|
||||
@@ -1,140 +0,0 @@
|
||||
# Why MUSL Compilation Fails: Technical Explanation
|
||||
|
||||
## The Core Problem
|
||||
|
||||
**You cannot mix glibc headers/libraries with MUSL's C library.** They are fundamentally incompatible at the ABI (Application Binary Interface) level.
|
||||
|
||||
## What Happens When We Try
|
||||
|
||||
```bash
|
||||
musl-gcc -I/usr/include src/main.c -lwebsockets
|
||||
```
|
||||
|
||||
### Step-by-Step Breakdown:
|
||||
|
||||
1. **musl-gcc includes `<libwebsockets.h>`** from `/usr/include/libwebsockets.h`
|
||||
|
||||
2. **libwebsockets.h includes standard C headers:**
|
||||
```c
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
```
|
||||
|
||||
3. **The system provides glibc's version of these headers** (from `/usr/include/`)
|
||||
|
||||
4. **glibc's `<string.h>` includes glibc-specific internal headers:**
|
||||
```c
|
||||
#include <bits/libc-header-start.h>
|
||||
#include <bits/types.h>
|
||||
```
|
||||
|
||||
5. **MUSL doesn't have these `bits/` headers** - it has a completely different structure:
|
||||
- MUSL uses `/usr/include/x86_64-linux-musl/` for its headers
|
||||
- MUSL's headers are simpler and don't use the `bits/` subdirectory structure
|
||||
|
||||
6. **Compilation fails** with:
|
||||
```
|
||||
fatal error: bits/libc-header-start.h: No such file or directory
|
||||
```
|
||||
|
||||
## Why This Is Fundamental
|
||||
|
||||
### Different C Library Implementations
|
||||
|
||||
**glibc (GNU C Library):**
|
||||
- Complex, feature-rich implementation
|
||||
- Uses `bits/` subdirectories for platform-specific code
|
||||
- Larger binary size
|
||||
- More system-specific optimizations
|
||||
|
||||
**MUSL:**
|
||||
- Minimal, clean implementation
|
||||
- Simpler header structure
|
||||
- Smaller binary size
|
||||
- Designed for static linking and portability
|
||||
|
||||
### ABI Incompatibility
|
||||
|
||||
Even if headers compiled, the **Application Binary Interface (ABI)** is different:
|
||||
- Function calling conventions may differ
|
||||
- Structure layouts may differ
|
||||
- System call wrappers are implemented differently
|
||||
- Thread-local storage mechanisms differ
|
||||
|
||||
## The Solution: Build Everything with MUSL
|
||||
|
||||
To create a true MUSL static binary, you must:
|
||||
|
||||
### 1. Build libwebsockets with musl-gcc
|
||||
|
||||
```bash
|
||||
git clone https://github.com/warmcat/libwebsockets.git
|
||||
cd libwebsockets
|
||||
mkdir build && cd build
|
||||
cmake .. \
|
||||
-DCMAKE_C_COMPILER=musl-gcc \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DLWS_WITH_STATIC=ON \
|
||||
-DLWS_WITH_SHARED=OFF \
|
||||
-DLWS_WITHOUT_TESTAPPS=ON
|
||||
make
|
||||
```
|
||||
|
||||
### 2. Build OpenSSL with MUSL
|
||||
|
||||
```bash
|
||||
wget https://www.openssl.org/source/openssl-3.0.0.tar.gz
|
||||
tar xzf openssl-3.0.0.tar.gz
|
||||
cd openssl-3.0.0
|
||||
CC=musl-gcc ./config no-shared --prefix=/opt/musl-openssl
|
||||
make && make install
|
||||
```
|
||||
|
||||
### 3. Build all other dependencies
|
||||
|
||||
- zlib with musl-gcc
|
||||
- libsecp256k1 with musl-gcc
|
||||
- libcurl with musl-gcc (which itself needs OpenSSL built with MUSL)
|
||||
|
||||
### 4. Build c-relay with all MUSL libraries
|
||||
|
||||
```bash
|
||||
musl-gcc -static \
|
||||
-I/opt/musl-libwebsockets/include \
|
||||
-I/opt/musl-openssl/include \
|
||||
src/*.c \
|
||||
-L/opt/musl-libwebsockets/lib -lwebsockets \
|
||||
-L/opt/musl-openssl/lib -lssl -lcrypto \
|
||||
...
|
||||
```
|
||||
|
||||
## Why We Use glibc Static Instead
|
||||
|
||||
Building the entire dependency chain with MUSL is:
|
||||
- **Time-consuming**: Hours to build all dependencies
|
||||
- **Complex**: Each library has its own build quirks
|
||||
- **Maintenance burden**: Must rebuild when dependencies update
|
||||
- **Unnecessary for most use cases**: glibc static binaries work fine
|
||||
|
||||
### glibc Static Binary Advantages:
|
||||
|
||||
✅ **Still fully static** - no runtime dependencies
|
||||
✅ **Works on virtually all Linux distributions**
|
||||
✅ **Much faster to build** - uses system libraries
|
||||
✅ **Easier to maintain** - no custom dependency builds
|
||||
✅ **Same practical portability** for modern Linux systems
|
||||
|
||||
### glibc Static Binary Limitations:
|
||||
|
||||
⚠️ **Slightly larger** than MUSL (glibc is bigger)
|
||||
⚠️ **May not work on very old systems** (ancient glibc versions)
|
||||
⚠️ **Not as universally portable** as MUSL (but close enough)
|
||||
|
||||
## Conclusion
|
||||
|
||||
**MUSL compilation fails because system libraries are compiled with glibc, and you cannot mix glibc and MUSL.**
|
||||
|
||||
The current approach (glibc static binary) is the pragmatic solution that provides excellent portability without the complexity of building an entire MUSL toolchain.
|
||||
|
||||
If true MUSL binaries are needed in the future, the solution is to use Alpine Linux (which uses MUSL natively) in a Docker container, where all system libraries are already MUSL-compiled.
|
||||
364
increment_and_push.sh
Executable file
364
increment_and_push.sh
Executable file
@@ -0,0 +1,364 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Global variables
|
||||
COMMIT_MESSAGE=""
|
||||
RELEASE_MODE=false
|
||||
|
||||
show_usage() {
|
||||
echo "C-Relay Increment and Push Script"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " $0 \"commit message\" - Default: increment patch, commit & push"
|
||||
echo " $0 -r \"commit message\" - Release: increment minor, create release"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 \"Fixed event validation bug\""
|
||||
echo " $0 --release \"Major release with new features\""
|
||||
echo ""
|
||||
echo "Default Mode (patch increment):"
|
||||
echo " - Increment patch version (v1.2.3 → v1.2.4)"
|
||||
echo " - Git add, commit with message, and push"
|
||||
echo ""
|
||||
echo "Release Mode (-r flag):"
|
||||
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
|
||||
echo " - Git add, commit, push, and create Gitea release"
|
||||
echo ""
|
||||
echo "Requirements for Release Mode:"
|
||||
echo " - Gitea token in ~/.gitea_token for release uploads"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-r|--release)
|
||||
RELEASE_MODE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# First non-flag argument is the commit message
|
||||
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
||||
COMMIT_MESSAGE="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate inputs
|
||||
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
||||
print_error "Commit message is required"
|
||||
echo ""
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we're in a git repository
|
||||
check_git_repo() {
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_error "Not in a git repository"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get current version and increment appropriately
|
||||
increment_version() {
|
||||
local increment_type="$1" # "patch" or "minor"
|
||||
|
||||
print_status "Getting current version..."
|
||||
|
||||
# Get the highest version tag (not chronologically latest)
|
||||
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "")
|
||||
if [[ -z "$LATEST_TAG" ]]; then
|
||||
LATEST_TAG="v0.0.0"
|
||||
print_warning "No version tags found, starting from $LATEST_TAG"
|
||||
fi
|
||||
|
||||
# Extract version components (remove 'v' prefix)
|
||||
VERSION=${LATEST_TAG#v}
|
||||
|
||||
# Parse major.minor.patch using regex
|
||||
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
MAJOR=${BASH_REMATCH[1]}
|
||||
MINOR=${BASH_REMATCH[2]}
|
||||
PATCH=${BASH_REMATCH[3]}
|
||||
else
|
||||
print_error "Invalid version format in tag: $LATEST_TAG"
|
||||
print_error "Expected format: v0.1.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Increment version based on type
|
||||
if [[ "$increment_type" == "minor" ]]; then
|
||||
# Minor release: increment minor, zero patch
|
||||
NEW_MINOR=$((MINOR + 1))
|
||||
NEW_PATCH=0
|
||||
NEW_VERSION="v${MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
print_status "Release mode: incrementing minor version"
|
||||
else
|
||||
# Default: increment patch
|
||||
NEW_PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||
print_status "Default mode: incrementing patch version"
|
||||
fi
|
||||
|
||||
print_status "Current version: $LATEST_TAG"
|
||||
print_status "New version: $NEW_VERSION"
|
||||
|
||||
# Update version in src/main.h
|
||||
update_version_in_header "$NEW_VERSION" "$MAJOR" "${NEW_MINOR:-$MINOR}" "${NEW_PATCH:-$PATCH}"
|
||||
|
||||
# Export for use in other functions
|
||||
export NEW_VERSION
|
||||
}
|
||||
|
||||
# Function to update version macros in src/main.h
|
||||
update_version_in_header() {
|
||||
local new_version="$1"
|
||||
local major="$2"
|
||||
local minor="$3"
|
||||
local patch="$4"
|
||||
|
||||
print_status "Updating version in src/main.h..."
|
||||
|
||||
# Check if src/main.h exists
|
||||
if [[ ! -f "src/main.h" ]]; then
|
||||
print_error "src/main.h not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update VERSION macro
|
||||
sed -i "s/#define VERSION \".*\"/#define VERSION \"$new_version\"/" src/main.h
|
||||
|
||||
# Update VERSION_MAJOR macro
|
||||
sed -i "s/#define VERSION_MAJOR [0-9]\+/#define VERSION_MAJOR $major/" src/main.h
|
||||
|
||||
# Update VERSION_MINOR macro
|
||||
sed -i "s/#define VERSION_MINOR .*/#define VERSION_MINOR $minor/" src/main.h
|
||||
|
||||
# Update VERSION_PATCH macro
|
||||
sed -i "s/#define VERSION_PATCH [0-9]\+/#define VERSION_PATCH $patch/" src/main.h
|
||||
|
||||
print_success "Updated version in src/main.h to $new_version"
|
||||
}
|
||||
|
||||
# Function to commit and push changes
|
||||
git_commit_and_push() {
|
||||
print_status "Preparing git commit..."
|
||||
|
||||
# Stage all changes
|
||||
if git add . > /dev/null 2>&1; then
|
||||
print_success "Staged all changes"
|
||||
else
|
||||
print_error "Failed to stage changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --staged --quiet; then
|
||||
print_warning "No changes to commit"
|
||||
else
|
||||
# Commit changes
|
||||
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
|
||||
print_success "Committed changes"
|
||||
else
|
||||
print_error "Failed to commit changes"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create new git tag
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists"
|
||||
fi
|
||||
|
||||
# Push changes and tags
|
||||
print_status "Pushing to remote repository..."
|
||||
if git push > /dev/null 2>&1; then
|
||||
print_success "Pushed changes"
|
||||
else
|
||||
print_error "Failed to push changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Push only the new tag to avoid conflicts with existing tags
|
||||
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Pushed tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag push failed, trying force push..."
|
||||
if git push --force origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Force-pushed updated tag: $NEW_VERSION"
|
||||
else
|
||||
print_error "Failed to push tag: $NEW_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to commit and push changes without creating a tag (tag already created)
|
||||
git_commit_and_push_no_tag() {
|
||||
print_status "Preparing git commit..."
|
||||
|
||||
# Stage all changes
|
||||
if git add . > /dev/null 2>&1; then
|
||||
print_success "Staged all changes"
|
||||
else
|
||||
print_error "Failed to stage changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --staged --quiet; then
|
||||
print_warning "No changes to commit"
|
||||
else
|
||||
# Commit changes
|
||||
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
|
||||
print_success "Committed changes"
|
||||
else
|
||||
print_error "Failed to commit changes"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Push changes and tags
|
||||
print_status "Pushing to remote repository..."
|
||||
if git push > /dev/null 2>&1; then
|
||||
print_success "Pushed changes"
|
||||
else
|
||||
print_error "Failed to push changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Push only the new tag to avoid conflicts with existing tags
|
||||
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Pushed tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag push failed, trying force push..."
|
||||
if git push --force origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Force-pushed updated tag: $NEW_VERSION"
|
||||
else
|
||||
print_error "Failed to push tag: $NEW_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create Gitea release
|
||||
create_gitea_release() {
|
||||
print_status "Creating Gitea release..."
|
||||
|
||||
# Check for Gitea token
|
||||
if [[ ! -f "$HOME/.gitea_token" ]]; then
|
||||
print_warning "No ~/.gitea_token found. Skipping release creation."
|
||||
print_warning "Create ~/.gitea_token with your Gitea access token to enable releases."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
|
||||
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/c-relay"
|
||||
|
||||
# Create release
|
||||
print_status "Creating release $NEW_VERSION..."
|
||||
local response=$(curl -s -X POST "$api_url/releases" \
|
||||
-H "Authorization: token $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
|
||||
|
||||
if echo "$response" | grep -q '"id"'; then
|
||||
print_success "Created release $NEW_VERSION"
|
||||
return 0
|
||||
elif echo "$response" | grep -q "already exists"; then
|
||||
print_warning "Release $NEW_VERSION already exists"
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to create release $NEW_VERSION"
|
||||
print_error "Response: $response"
|
||||
|
||||
# Try to check if the release exists anyway
|
||||
print_status "Checking if release exists..."
|
||||
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
|
||||
if echo "$check_response" | grep -q '"id"'; then
|
||||
print_warning "Release exists but creation response was unexpected"
|
||||
return 0
|
||||
else
|
||||
print_error "Release does not exist and creation failed"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_status "C-Relay Increment and Push Script"
|
||||
|
||||
# Check prerequisites
|
||||
check_git_repo
|
||||
|
||||
if [[ "$RELEASE_MODE" == true ]]; then
|
||||
print_status "=== RELEASE MODE ==="
|
||||
|
||||
# Increment minor version for releases
|
||||
increment_version "minor"
|
||||
|
||||
# Create new git tag BEFORE compilation so version.h picks it up
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
|
||||
git tag -d "$NEW_VERSION" > /dev/null 2>&1
|
||||
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Commit and push (but skip tag creation since we already did it)
|
||||
git_commit_and_push_no_tag
|
||||
|
||||
# Create Gitea release
|
||||
if create_gitea_release; then
|
||||
print_success "Release $NEW_VERSION completed successfully!"
|
||||
else
|
||||
print_error "Release creation failed"
|
||||
fi
|
||||
|
||||
else
|
||||
print_status "=== DEFAULT MODE ==="
|
||||
|
||||
# Increment patch version for regular commits
|
||||
increment_version "patch"
|
||||
|
||||
# Create new git tag BEFORE compilation so version.h picks it up
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
|
||||
git tag -d "$NEW_VERSION" > /dev/null 2>&1
|
||||
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Commit and push (but skip tag creation since we already did it)
|
||||
git_commit_and_push_no_tag
|
||||
|
||||
print_success "Increment and push completed successfully!"
|
||||
print_status "Version $NEW_VERSION pushed to repository"
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute main function
|
||||
main
|
||||
@@ -12,6 +12,7 @@ USE_TEST_KEYS=false
|
||||
ADMIN_KEY=""
|
||||
RELAY_KEY=""
|
||||
PORT_OVERRIDE=""
|
||||
DEBUG_LEVEL="5"
|
||||
|
||||
# Key validation function
|
||||
validate_hex_key() {
|
||||
@@ -71,6 +72,34 @@ while [[ $# -gt 0 ]]; do
|
||||
USE_TEST_KEYS=true
|
||||
shift
|
||||
;;
|
||||
--debug-level=*)
|
||||
DEBUG_LEVEL="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
-d=*)
|
||||
DEBUG_LEVEL="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--debug-level)
|
||||
if [ -z "$2" ]; then
|
||||
echo "ERROR: Debug level option requires a value"
|
||||
HELP=true
|
||||
shift
|
||||
else
|
||||
DEBUG_LEVEL="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
-d)
|
||||
if [ -z "$2" ]; then
|
||||
echo "ERROR: Debug level option requires a value"
|
||||
HELP=true
|
||||
shift
|
||||
else
|
||||
DEBUG_LEVEL="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
--help|-h)
|
||||
HELP=true
|
||||
shift
|
||||
@@ -104,6 +133,19 @@ if [ -n "$PORT_OVERRIDE" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate strict port flag (only makes sense with port override)
|
||||
if [ "$USE_TEST_KEYS" = true ] && [ -z "$PORT_OVERRIDE" ]; then
|
||||
echo "WARNING: --strict-port is always used with test keys. Consider specifying a custom port with -p."
|
||||
fi
|
||||
|
||||
# Validate debug level if provided
|
||||
if [ -n "$DEBUG_LEVEL" ]; then
|
||||
if ! [[ "$DEBUG_LEVEL" =~ ^[0-5]$ ]]; then
|
||||
echo "ERROR: Debug level must be 0-5, got: $DEBUG_LEVEL"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show help
|
||||
if [ "$HELP" = true ]; then
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
@@ -112,6 +154,7 @@ if [ "$HELP" = true ]; then
|
||||
echo " -a, --admin-key <hex> 64-character hex admin private key"
|
||||
echo " -r, --relay-key <hex> 64-character hex relay private key"
|
||||
echo " -p, --port <port> Custom port override (default: 8888)"
|
||||
echo " -d, --debug-level <0-5> Set debug level: 0=none, 1=errors, 2=warnings, 3=info, 4=debug, 5=trace"
|
||||
echo " --preserve-database Keep existing database files (don't delete for fresh start)"
|
||||
echo " --test-keys, -t Use deterministic test keys for development (admin: all 'a's, relay: all '1's)"
|
||||
echo " --help, -h Show this help message"
|
||||
@@ -125,6 +168,10 @@ if [ "$HELP" = true ]; then
|
||||
echo " $0 # Fresh start with random keys"
|
||||
echo " $0 -a <admin-hex> -r <relay-hex> # Use custom keys"
|
||||
echo " $0 -a <admin-hex> -p 9000 # Custom admin key on port 9000"
|
||||
echo " $0 -p 7777 --strict-port # Fail if port 7777 unavailable (no fallback)"
|
||||
echo " $0 -p 8080 --strict-port -d=3 # Custom port with strict binding and debug"
|
||||
echo " $0 --debug-level=3 # Start with debug level 3 (info)"
|
||||
echo " $0 -d=5 # Start with debug level 5 (trace)"
|
||||
echo " $0 --preserve-database # Preserve existing database and keys"
|
||||
echo " $0 --test-keys # Use test keys for consistent development"
|
||||
echo " $0 -t --preserve-database # Use test keys and preserve database"
|
||||
@@ -137,22 +184,15 @@ fi
|
||||
|
||||
# Handle database file cleanup for fresh start
|
||||
if [ "$PRESERVE_DATABASE" = false ]; then
|
||||
if ls *.db >/dev/null 2>&1 || ls build/*.db >/dev/null 2>&1; then
|
||||
echo "Removing existing database files to trigger fresh key generation..."
|
||||
rm -f *.db build/*.db
|
||||
if ls *.db* >/dev/null 2>&1 || ls build/*.db* >/dev/null 2>&1; then
|
||||
echo "Removing existing database files (including WAL/SHM) to trigger fresh key generation..."
|
||||
rm -f *.db* build/*.db*
|
||||
echo "✓ Database files removed - will generate new keys and database"
|
||||
else
|
||||
echo "No existing database found - will generate fresh setup"
|
||||
fi
|
||||
else
|
||||
echo "Preserving existing database files as requested"
|
||||
# Back up database files before clean build
|
||||
if ls build/*.db >/dev/null 2>&1; then
|
||||
echo "Backing up existing database files..."
|
||||
mkdir -p /tmp/relay_backup_$$
|
||||
cp build/*.db* /tmp/relay_backup_$$/ 2>/dev/null || true
|
||||
echo "Database files backed up to temporary location"
|
||||
fi
|
||||
echo "Preserving existing database files (build process does not touch database files)"
|
||||
fi
|
||||
|
||||
# Clean up legacy files that are no longer used
|
||||
@@ -174,14 +214,6 @@ if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Restore database files if preserving
|
||||
if [ "$PRESERVE_DATABASE" = true ] && [ -d "/tmp/relay_backup_$$" ]; then
|
||||
echo "Restoring preserved database files..."
|
||||
cp /tmp/relay_backup_$$/*.db* build/ 2>/dev/null || true
|
||||
rm -rf /tmp/relay_backup_$$
|
||||
echo "Database files restored to build directory"
|
||||
fi
|
||||
|
||||
# Check if build was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Build failed. Cannot restart relay."
|
||||
@@ -295,19 +327,24 @@ if [ -n "$PORT_OVERRIDE" ]; then
|
||||
echo "Using custom port: $PORT_OVERRIDE"
|
||||
fi
|
||||
|
||||
if [ -n "$DEBUG_LEVEL" ]; then
|
||||
RELAY_ARGS="$RELAY_ARGS --debug-level=$DEBUG_LEVEL"
|
||||
echo "Using debug level: $DEBUG_LEVEL"
|
||||
fi
|
||||
|
||||
# Change to build directory before starting relay so database files are created there
|
||||
cd build
|
||||
# Start relay in background and capture its PID
|
||||
if [ "$USE_TEST_KEYS" = true ]; then
|
||||
echo "Using deterministic test keys for development..."
|
||||
./$(basename $BINARY_PATH) -a 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3 -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 &
|
||||
./$(basename $BINARY_PATH) -a 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3 -r 1111111111111111111111111111111111111111111111111111111111111111 --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
|
||||
elif [ -n "$RELAY_ARGS" ]; then
|
||||
echo "Starting relay with custom configuration..."
|
||||
./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 &
|
||||
./$(basename $BINARY_PATH) $RELAY_ARGS --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
|
||||
else
|
||||
# No command line arguments needed for random key generation
|
||||
echo "Starting relay with random key generation..."
|
||||
./$(basename $BINARY_PATH) --strict-port > ../relay.log 2>&1 &
|
||||
./$(basename $BINARY_PATH) --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
|
||||
fi
|
||||
RELAY_PID=$!
|
||||
# Change back to original directory
|
||||
|
||||
Submodule nostr_core_lib updated: 5066ba8dd0...a8dc2ed046
91
notes.txt
Normal file
91
notes.txt
Normal file
@@ -0,0 +1,91 @@
|
||||
|
||||
Task Completed
|
||||
Perfect! The core dump limit is now set correctly. You can see both:
|
||||
|
||||
LimitCORE=infinity
|
||||
LimitCORESoft=infinity
|
||||
What happens next:
|
||||
The relay is now configured to generate core dumps when it crashes
|
||||
Wait for the next crash to occur (based on your logs, it seems to happen intermittently)
|
||||
When it crashes, the core dump will be automatically captured by systemd
|
||||
After the next crash, analyze it:
|
||||
# List all core dumps (most recent first)
|
||||
sudo coredumpctl list
|
||||
|
||||
# View info about the most recent c-relay crash
|
||||
sudo coredumpctl info c-relay
|
||||
|
||||
# Load the core dump in gdb for detailed analysis
|
||||
sudo coredumpctl gdb c-relay
|
||||
|
||||
Inside gdb, run these commands:
|
||||
(gdb) bt full # Full backtrace with all variables
|
||||
(gdb) info threads # Show all threads
|
||||
(gdb) thread apply all bt # Backtrace for all threads
|
||||
(gdb) frame 0 # Go to crash frame
|
||||
(gdb) info locals # Show local variables
|
||||
(gdb) list # Show source code around crash
|
||||
|
||||
The backtrace will show you exactly which line of code caused the segmentation fault and the state of all variables at that moment, which should pinpoint whether it's the subscription management issue, buffer overflow, or something else.
|
||||
|
||||
You're all set up now - just wait for the next crash and then run the coredumpctl commands to analyze it.
|
||||
|
||||
|
||||
--------------------------
|
||||
|
||||
|
||||
Even simpler: Use this one-liner
|
||||
# Start relay and immediately attach gdb
|
||||
cd /usr/local/bin/c_relay
|
||||
sudo -u c-relay ./c_relay --debug-level=5 & sleep 2 && sudo gdb -p $(pgrep c_relay)
|
||||
|
||||
Inside gdb, after attaching:
|
||||
|
||||
(gdb) continue
|
||||
Or shorter:
|
||||
(gdb) c
|
||||
|
||||
|
||||
How to View the Logs
|
||||
Check systemd journal:
|
||||
# View all c-relay logs
|
||||
sudo journalctl -u c-relay
|
||||
|
||||
# View recent logs (last 50 lines)
|
||||
sudo journalctl -u c-relay -n 50
|
||||
|
||||
# Follow logs in real-time
|
||||
sudo journalctl -u c-relay -f
|
||||
|
||||
# View logs since last boot
|
||||
sudo journalctl -u c-relay -b
|
||||
|
||||
Check if service is running:
|
||||
|
||||
|
||||
|
||||
To immediately trim the syslog file size:
|
||||
|
||||
Safe Syslog Truncation
|
||||
Stop syslog service first:
|
||||
sudo systemctl stop rsyslog
|
||||
|
||||
Truncate the syslog file:
|
||||
sudo truncate -s 0 /var/log/syslog
|
||||
|
||||
Restart syslog service:
|
||||
sudo systemctl start rsyslog
|
||||
sudo systemctl status rsyslog
|
||||
|
||||
|
||||
sudo -u c-relay ./c_relay --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
|
||||
|
||||
./c_relay_static_x86_64 -p 7889 --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
|
||||
|
||||
|
||||
sudo ufw allow 8888/tcp
|
||||
sudo ufw delete allow 8888/tcp
|
||||
|
||||
lsof -i :7777
|
||||
kill $(lsof -t -i :7777)
|
||||
kill -9 $(lsof -t -i :7777)
|
||||
46
src/api.h
46
src/api.h
@@ -1,8 +1,9 @@
|
||||
// API module for serving embedded web content
|
||||
// API module for serving embedded web content and admin API functions
|
||||
#ifndef API_H
|
||||
#define API_H
|
||||
|
||||
#include <libwebsockets.h>
|
||||
#include <cjson/cJSON.h>
|
||||
|
||||
// Embedded file session data structure for managing buffer lifetime
|
||||
struct embedded_file_session_data {
|
||||
@@ -14,10 +15,53 @@ struct embedded_file_session_data {
|
||||
int body_sent;
|
||||
};
|
||||
|
||||
// Configuration change pending structure
|
||||
typedef struct pending_config_change {
|
||||
char admin_pubkey[65]; // Who requested the change
|
||||
char config_key[128]; // What config to change
|
||||
char old_value[256]; // Current value
|
||||
char new_value[256]; // Requested new value
|
||||
time_t timestamp; // When requested
|
||||
char change_id[33]; // Unique ID for this change (first 32 chars of hash)
|
||||
struct pending_config_change* next; // Linked list for concurrent changes
|
||||
} pending_config_change_t;
|
||||
|
||||
// Handle HTTP request for embedded API files
|
||||
int handle_embedded_file_request(struct lws* wsi, const char* requested_uri);
|
||||
|
||||
// Generate stats JSON from database queries
|
||||
char* generate_stats_json(void);
|
||||
|
||||
// Generate human-readable stats text
|
||||
char* generate_stats_text(void);
|
||||
|
||||
// Generate config text from database
|
||||
char* generate_config_text(void);
|
||||
|
||||
// Send admin response with request ID correlation
|
||||
int send_admin_response(const char* sender_pubkey, const char* response_content, const char* request_id,
|
||||
char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
// Configuration change system functions
|
||||
int parse_config_command(const char* message, char* key, char* value);
|
||||
int validate_config_change(const char* key, const char* value);
|
||||
char* store_pending_config_change(const char* admin_pubkey, const char* key,
|
||||
const char* old_value, const char* new_value);
|
||||
pending_config_change_t* find_pending_change(const char* admin_pubkey, const char* change_id);
|
||||
int apply_config_change(const char* key, const char* value);
|
||||
void cleanup_expired_pending_changes(void);
|
||||
int handle_config_confirmation(const char* admin_pubkey, const char* response);
|
||||
char* generate_config_change_confirmation(const char* key, const char* old_value, const char* new_value);
|
||||
int process_config_change_request(const char* admin_pubkey, const char* message);
|
||||
|
||||
// SQL query functions
|
||||
int validate_sql_query(const char* query, char* error_message, size_t error_size);
|
||||
char* execute_sql_query(const char* query, const char* request_id, char* error_message, size_t error_size);
|
||||
int handle_sql_query_unified(cJSON* event, const char* query, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
// Monitoring system functions
|
||||
void monitoring_on_event_stored(void);
|
||||
void monitoring_on_subscription_change(void);
|
||||
int get_monitoring_throttle_seconds(void);
|
||||
|
||||
#endif // API_H
|
||||
1776
src/config.c
1776
src/config.c
File diff suppressed because it is too large
Load Diff
86
src/config.h
86
src/config.h
@@ -27,89 +27,15 @@ struct lws;
|
||||
// Database path for event-based config
|
||||
extern char g_database_path[512];
|
||||
|
||||
// Unified configuration cache structure (consolidates all caching systems)
|
||||
typedef struct {
|
||||
// Critical keys (frequently accessed)
|
||||
char admin_pubkey[65];
|
||||
char relay_pubkey[65];
|
||||
|
||||
// Auth config (from request_validator)
|
||||
int auth_required;
|
||||
long max_file_size;
|
||||
int admin_enabled;
|
||||
int nip42_mode;
|
||||
int nip42_challenge_timeout;
|
||||
int nip42_time_tolerance;
|
||||
int nip70_protected_events_enabled;
|
||||
|
||||
// Static buffer for config values (replaces static buffers in get_config_value functions)
|
||||
char temp_buffer[CONFIG_VALUE_MAX_LENGTH];
|
||||
|
||||
// NIP-11 relay information (migrated from g_relay_info in main.c)
|
||||
struct {
|
||||
char name[RELAY_NAME_MAX_LENGTH];
|
||||
char description[RELAY_DESCRIPTION_MAX_LENGTH];
|
||||
char banner[RELAY_URL_MAX_LENGTH];
|
||||
char icon[RELAY_URL_MAX_LENGTH];
|
||||
char pubkey[RELAY_PUBKEY_MAX_LENGTH];
|
||||
char contact[RELAY_CONTACT_MAX_LENGTH];
|
||||
char software[RELAY_URL_MAX_LENGTH];
|
||||
char version[64];
|
||||
char privacy_policy[RELAY_URL_MAX_LENGTH];
|
||||
char terms_of_service[RELAY_URL_MAX_LENGTH];
|
||||
// Raw string values for parsing into cJSON arrays
|
||||
char supported_nips_str[CONFIG_VALUE_MAX_LENGTH];
|
||||
char language_tags_str[CONFIG_VALUE_MAX_LENGTH];
|
||||
char relay_countries_str[CONFIG_VALUE_MAX_LENGTH];
|
||||
// Parsed cJSON arrays
|
||||
cJSON* supported_nips;
|
||||
cJSON* limitation;
|
||||
cJSON* retention;
|
||||
cJSON* relay_countries;
|
||||
cJSON* language_tags;
|
||||
cJSON* tags;
|
||||
char posting_policy[RELAY_URL_MAX_LENGTH];
|
||||
cJSON* fees;
|
||||
char payments_url[RELAY_URL_MAX_LENGTH];
|
||||
} relay_info;
|
||||
|
||||
// NIP-13 PoW configuration (migrated from g_pow_config in main.c)
|
||||
struct {
|
||||
int enabled;
|
||||
int min_pow_difficulty;
|
||||
int validation_flags;
|
||||
int require_nonce_tag;
|
||||
int reject_lower_targets;
|
||||
int strict_format;
|
||||
int anti_spam_mode;
|
||||
} pow_config;
|
||||
|
||||
// NIP-40 Expiration configuration (migrated from g_expiration_config in main.c)
|
||||
struct {
|
||||
int enabled;
|
||||
int strict_mode;
|
||||
int filter_responses;
|
||||
int delete_expired;
|
||||
long grace_period;
|
||||
} expiration_config;
|
||||
|
||||
// Cache management
|
||||
time_t cache_expires;
|
||||
int cache_valid;
|
||||
pthread_mutex_t cache_lock;
|
||||
} unified_config_cache_t;
|
||||
|
||||
// Command line options structure for first-time startup
|
||||
typedef struct {
|
||||
int port_override; // -1 = not set, >0 = port value
|
||||
char admin_pubkey_override[65]; // Empty string = not set, 64-char hex = override
|
||||
char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override
|
||||
int strict_port; // 0 = allow port increment, 1 = fail if exact port unavailable
|
||||
int debug_level; // 0-5, default 0 (no debug output)
|
||||
} cli_options_t;
|
||||
|
||||
// Global unified configuration cache
|
||||
extern unified_config_cache_t g_unified_cache;
|
||||
|
||||
// Core configuration functions (temporary compatibility)
|
||||
int init_configuration_system(const char* config_dir_override, const char* config_file_override);
|
||||
void cleanup_configuration_system(void);
|
||||
@@ -137,8 +63,8 @@ int get_config_bool(const char* key, int default_value);
|
||||
|
||||
// First-time startup functions
|
||||
int is_first_time_startup(void);
|
||||
int first_time_startup_sequence(const cli_options_t* cli_options);
|
||||
int startup_existing_relay(const char* relay_pubkey);
|
||||
int first_time_startup_sequence(const cli_options_t* cli_options, char* admin_pubkey_out, char* relay_pubkey_out, char* relay_privkey_out);
|
||||
int startup_existing_relay(const char* relay_pubkey, const cli_options_t* cli_options);
|
||||
|
||||
// Configuration application functions
|
||||
int apply_configuration_from_event(const cJSON* event);
|
||||
@@ -168,6 +94,7 @@ int set_config_value_in_table(const char* key, const char* value, const char* da
|
||||
const char* description, const char* category, int requires_restart);
|
||||
int update_config_in_table(const char* key, const char* value);
|
||||
int populate_default_config_values(void);
|
||||
int populate_all_config_values_atomic(const char* admin_pubkey, const char* relay_pubkey);
|
||||
int add_pubkeys_to_config_table(void);
|
||||
|
||||
// Admin event processing functions (updated with WebSocket support)
|
||||
@@ -187,7 +114,7 @@ cJSON* build_query_response(const char* query_type, cJSON* results_array, int to
|
||||
|
||||
// Auth rules management functions
|
||||
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value, const char* action);
|
||||
const char* pattern_value);
|
||||
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value);
|
||||
|
||||
@@ -211,6 +138,9 @@ int populate_config_table_from_event(const cJSON* event);
|
||||
int process_startup_config_event(const cJSON* event);
|
||||
int process_startup_config_event_with_fallback(const cJSON* event);
|
||||
|
||||
// Atomic CLI override application
|
||||
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
|
||||
|
||||
// Dynamic event generation functions for WebSocket configuration fetching
|
||||
cJSON* generate_config_event_from_table(void);
|
||||
int req_filter_requests_config_events(const cJSON* filter);
|
||||
|
||||
@@ -28,6 +28,8 @@ static const struct {
|
||||
{"nip42_auth_required_subscriptions", "false"},
|
||||
{"nip42_auth_required_kinds", "4,14"}, // Default: DM kinds require auth
|
||||
{"nip42_challenge_expiration", "600"}, // 10 minutes
|
||||
{"nip42_challenge_timeout", "600"}, // Challenge timeout (seconds)
|
||||
{"nip42_time_tolerance", "300"}, // Time tolerance (seconds)
|
||||
|
||||
// NIP-70 Protected Events
|
||||
{"nip70_protected_events_enabled", "false"},
|
||||
@@ -70,7 +72,16 @@ static const struct {
|
||||
|
||||
// Performance Settings
|
||||
{"default_limit", "500"},
|
||||
{"max_limit", "5000"}
|
||||
{"max_limit", "5000"},
|
||||
|
||||
// Proxy Settings
|
||||
// Trust proxy headers (X-Forwarded-For, X-Real-IP) for accurate client IP detection
|
||||
// Safe for informational/debugging use. Only becomes a security concern if you implement
|
||||
// IP-based rate limiting or access control (which would require firewall protection anyway)
|
||||
{"trust_proxy_headers", "true"},
|
||||
|
||||
// NIP-59 Gift Wrap Timestamp Configuration
|
||||
{"nip59_timestamp_max_delay_sec", "0"}
|
||||
};
|
||||
|
||||
// Number of default configuration values
|
||||
|
||||
BIN
src/default_config_event.h.gch
Normal file
BIN
src/default_config_event.h.gch
Normal file
Binary file not shown.
1293
src/dm_admin.c
1293
src/dm_admin.c
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
// Note to assistants. dm_admin is only for functions relating to direct messaging
|
||||
|
||||
#ifndef DM_ADMIN_H
|
||||
#define DM_ADMIN_H
|
||||
|
||||
@@ -24,4 +26,11 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content,
|
||||
char* generate_config_text(void);
|
||||
char* generate_stats_text(void);
|
||||
|
||||
// SQL query admin functions
|
||||
int validate_sql_query(const char* query, char* error_message, size_t error_size);
|
||||
char* execute_sql_query(const char* query, const char* request_id, char* error_message, size_t error_size);
|
||||
int handle_sql_query_unified(cJSON* event, const char* query, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int send_admin_response(const char* sender_pubkey, const char* response_content, const char* request_id,
|
||||
char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
#endif // DM_ADMIN_H
|
||||
File diff suppressed because one or more lines are too long
671
src/main.c
671
src/main.c
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,9 @@
|
||||
#define MAIN_H
|
||||
|
||||
// Version information (auto-updated by build system)
|
||||
#define VERSION "v0.4.6"
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 4
|
||||
#define VERSION_PATCH 6
|
||||
#define VERSION_MINOR 8
|
||||
#define VERSION_PATCH 0
|
||||
|
||||
// Relay metadata (authoritative source for NIP-11 information)
|
||||
#define RELAY_NAME "C-Relay"
|
||||
|
||||
10
src/nip009.c
10
src/nip009.c
@@ -6,15 +6,13 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
#include <cjson/cJSON.h>
|
||||
#include "debug.h"
|
||||
#include <sqlite3.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_warning(const char* message);
|
||||
void log_info(const char* message);
|
||||
|
||||
// Forward declaration for database functions
|
||||
int store_event(cJSON* event);
|
||||
@@ -139,7 +137,7 @@ int handle_deletion_request(cJSON* event, char* error_message, size_t error_size
|
||||
|
||||
// Store the deletion request itself (it should be kept according to NIP-09)
|
||||
if (store_event(event) != 0) {
|
||||
log_warning("Failed to store deletion request event");
|
||||
DEBUG_WARN("Failed to store deletion request event");
|
||||
}
|
||||
|
||||
error_message[0] = '\0'; // Success - empty error message
|
||||
@@ -198,7 +196,7 @@ int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids) {
|
||||
sqlite3_finalize(check_stmt);
|
||||
char warning_msg[128];
|
||||
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for event: %.16s...", id);
|
||||
log_warning(warning_msg);
|
||||
DEBUG_WARN(warning_msg);
|
||||
}
|
||||
} else {
|
||||
sqlite3_finalize(check_stmt);
|
||||
@@ -244,7 +242,7 @@ int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, lon
|
||||
free(addr_copy);
|
||||
char warning_msg[128];
|
||||
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for address: %.32s...", addr);
|
||||
log_warning(warning_msg);
|
||||
DEBUG_WARN(warning_msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
568
src/nip011.c
568
src/nip011.c
@@ -1,6 +1,7 @@
|
||||
// NIP-11 Relay Information Document module
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include "debug.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
@@ -8,19 +9,13 @@
|
||||
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||
#include "config.h"
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_info(const char* message);
|
||||
void log_success(const char* message);
|
||||
void log_error(const char* message);
|
||||
void log_warning(const char* message);
|
||||
|
||||
// Forward declarations for configuration functions
|
||||
const char* get_config_value(const char* key);
|
||||
int get_config_int(const char* key, int default_value);
|
||||
int get_config_bool(const char* key, int default_value);
|
||||
|
||||
// Forward declarations for global cache access
|
||||
extern unified_config_cache_t g_unified_cache;
|
||||
// NIP-11 relay information is now managed directly from config table
|
||||
|
||||
// Forward declarations for constants (defined in config.h and other headers)
|
||||
#define HTTP_STATUS_OK 200
|
||||
@@ -79,18 +74,39 @@ cJSON* parse_comma_separated_array(const char* csv_string) {
|
||||
|
||||
// Initialize relay information using configuration system
|
||||
void init_relay_info() {
|
||||
// Get all config values first (without holding mutex to avoid deadlock)
|
||||
// Note: These may be dynamically allocated strings that need to be freed
|
||||
// NIP-11 relay information is now generated dynamically from config table
|
||||
// No initialization needed - data is fetched directly from database when requested
|
||||
}
|
||||
|
||||
// Clean up relay information JSON objects
|
||||
void cleanup_relay_info() {
|
||||
// NIP-11 relay information is now generated dynamically from config table
|
||||
// No cleanup needed - data is fetched directly from database when requested
|
||||
}
|
||||
|
||||
// Generate NIP-11 compliant JSON document
|
||||
cJSON* generate_relay_info_json() {
|
||||
cJSON* info = cJSON_CreateObject();
|
||||
if (!info) {
|
||||
DEBUG_ERROR("Failed to create relay info JSON object");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get all config values directly from database
|
||||
const char* relay_name = get_config_value("relay_name");
|
||||
const char* relay_description = get_config_value("relay_description");
|
||||
const char* relay_banner = get_config_value("relay_banner");
|
||||
const char* relay_icon = get_config_value("relay_icon");
|
||||
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||||
const char* relay_contact = get_config_value("relay_contact");
|
||||
const char* supported_nips_csv = get_config_value("supported_nips");
|
||||
const char* relay_software = get_config_value("relay_software");
|
||||
const char* relay_version = get_config_value("relay_version");
|
||||
const char* relay_contact = get_config_value("relay_contact");
|
||||
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||||
const char* supported_nips_csv = get_config_value("supported_nips");
|
||||
const char* privacy_policy = get_config_value("privacy_policy");
|
||||
const char* terms_of_service = get_config_value("terms_of_service");
|
||||
const char* posting_policy = get_config_value("posting_policy");
|
||||
const char* language_tags_csv = get_config_value("language_tags");
|
||||
const char* relay_countries_csv = get_config_value("relay_countries");
|
||||
const char* posting_policy = get_config_value("posting_policy");
|
||||
const char* payments_url = get_config_value("payments_url");
|
||||
|
||||
// Get config values for limitations
|
||||
@@ -100,416 +116,170 @@ void init_relay_info() {
|
||||
int max_event_tags = get_config_int("max_event_tags", 100);
|
||||
int max_content_length = get_config_int("max_content_length", 8196);
|
||||
int default_limit = get_config_int("default_limit", 500);
|
||||
int min_pow_difficulty = get_config_int("pow_min_difficulty", 0);
|
||||
int admin_enabled = get_config_bool("admin_enabled", 0);
|
||||
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
|
||||
// Update relay information fields
|
||||
if (relay_name) {
|
||||
strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1);
|
||||
free((char*)relay_name); // Free dynamically allocated string
|
||||
} else {
|
||||
strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1);
|
||||
}
|
||||
|
||||
if (relay_description) {
|
||||
strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1);
|
||||
free((char*)relay_description); // Free dynamically allocated string
|
||||
} else {
|
||||
strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1);
|
||||
}
|
||||
|
||||
if (relay_software) {
|
||||
strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1);
|
||||
free((char*)relay_software); // Free dynamically allocated string
|
||||
} else {
|
||||
strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1);
|
||||
}
|
||||
|
||||
if (relay_version) {
|
||||
strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1);
|
||||
free((char*)relay_version); // Free dynamically allocated string
|
||||
} else {
|
||||
strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1);
|
||||
}
|
||||
|
||||
if (relay_contact) {
|
||||
strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1);
|
||||
free((char*)relay_contact); // Free dynamically allocated string
|
||||
}
|
||||
|
||||
if (relay_pubkey) {
|
||||
strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
|
||||
free((char*)relay_pubkey); // Free dynamically allocated string
|
||||
}
|
||||
|
||||
if (posting_policy) {
|
||||
strncpy(g_unified_cache.relay_info.posting_policy, posting_policy, sizeof(g_unified_cache.relay_info.posting_policy) - 1);
|
||||
free((char*)posting_policy); // Free dynamically allocated string
|
||||
}
|
||||
|
||||
if (payments_url) {
|
||||
strncpy(g_unified_cache.relay_info.payments_url, payments_url, sizeof(g_unified_cache.relay_info.payments_url) - 1);
|
||||
free((char*)payments_url); // Free dynamically allocated string
|
||||
}
|
||||
|
||||
// Initialize supported NIPs array from config
|
||||
if (supported_nips_csv) {
|
||||
g_unified_cache.relay_info.supported_nips = parse_comma_separated_array(supported_nips_csv);
|
||||
free((char*)supported_nips_csv); // Free dynamically allocated string
|
||||
} else {
|
||||
// Fallback to default supported NIPs
|
||||
g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
|
||||
if (g_unified_cache.relay_info.supported_nips) {
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize server limitations using configuration
|
||||
g_unified_cache.relay_info.limitation = cJSON_CreateObject();
|
||||
if (g_unified_cache.relay_info.limitation) {
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty);
|
||||
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
|
||||
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False);
|
||||
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit);
|
||||
}
|
||||
|
||||
// Initialize empty retention policies (can be configured later)
|
||||
g_unified_cache.relay_info.retention = cJSON_CreateArray();
|
||||
|
||||
// Initialize language tags from config
|
||||
if (language_tags_csv) {
|
||||
g_unified_cache.relay_info.language_tags = parse_comma_separated_array(language_tags_csv);
|
||||
free((char*)language_tags_csv); // Free dynamically allocated string
|
||||
} else {
|
||||
// Fallback to global
|
||||
g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
|
||||
if (g_unified_cache.relay_info.language_tags) {
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize relay countries from config
|
||||
if (relay_countries_csv) {
|
||||
g_unified_cache.relay_info.relay_countries = parse_comma_separated_array(relay_countries_csv);
|
||||
free((char*)relay_countries_csv); // Free dynamically allocated string
|
||||
} else {
|
||||
// Fallback to global
|
||||
g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
|
||||
if (g_unified_cache.relay_info.relay_countries) {
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize content tags as empty array
|
||||
g_unified_cache.relay_info.tags = cJSON_CreateArray();
|
||||
|
||||
// Initialize fees as empty object (no payment required by default)
|
||||
g_unified_cache.relay_info.fees = cJSON_CreateObject();
|
||||
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
}
|
||||
|
||||
// Clean up relay information JSON objects
|
||||
void cleanup_relay_info() {
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
if (g_unified_cache.relay_info.supported_nips) {
|
||||
cJSON_Delete(g_unified_cache.relay_info.supported_nips);
|
||||
g_unified_cache.relay_info.supported_nips = NULL;
|
||||
}
|
||||
if (g_unified_cache.relay_info.limitation) {
|
||||
cJSON_Delete(g_unified_cache.relay_info.limitation);
|
||||
g_unified_cache.relay_info.limitation = NULL;
|
||||
}
|
||||
if (g_unified_cache.relay_info.retention) {
|
||||
cJSON_Delete(g_unified_cache.relay_info.retention);
|
||||
g_unified_cache.relay_info.retention = NULL;
|
||||
}
|
||||
if (g_unified_cache.relay_info.language_tags) {
|
||||
cJSON_Delete(g_unified_cache.relay_info.language_tags);
|
||||
g_unified_cache.relay_info.language_tags = NULL;
|
||||
}
|
||||
if (g_unified_cache.relay_info.relay_countries) {
|
||||
cJSON_Delete(g_unified_cache.relay_info.relay_countries);
|
||||
g_unified_cache.relay_info.relay_countries = NULL;
|
||||
}
|
||||
if (g_unified_cache.relay_info.tags) {
|
||||
cJSON_Delete(g_unified_cache.relay_info.tags);
|
||||
g_unified_cache.relay_info.tags = NULL;
|
||||
}
|
||||
if (g_unified_cache.relay_info.fees) {
|
||||
cJSON_Delete(g_unified_cache.relay_info.fees);
|
||||
g_unified_cache.relay_info.fees = NULL;
|
||||
}
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
}
|
||||
|
||||
// Generate NIP-11 compliant JSON document
|
||||
cJSON* generate_relay_info_json() {
|
||||
cJSON* info = cJSON_CreateObject();
|
||||
if (!info) {
|
||||
log_error("Failed to create relay info JSON object");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
|
||||
// Defensive reinit: if relay_info appears empty (cache refresh wiped it), rebuild it directly from table
|
||||
if (strlen(g_unified_cache.relay_info.name) == 0 &&
|
||||
strlen(g_unified_cache.relay_info.description) == 0 &&
|
||||
strlen(g_unified_cache.relay_info.software) == 0) {
|
||||
log_warning("NIP-11 relay_info appears empty, rebuilding directly from config table");
|
||||
|
||||
// Rebuild relay_info directly from config table to avoid circular cache dependency
|
||||
// Get values directly from table (similar to init_relay_info but without cache calls)
|
||||
const char* relay_name = get_config_value_from_table("relay_name");
|
||||
if (relay_name) {
|
||||
strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1);
|
||||
free((char*)relay_name);
|
||||
} else {
|
||||
strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1);
|
||||
}
|
||||
|
||||
const char* relay_description = get_config_value_from_table("relay_description");
|
||||
if (relay_description) {
|
||||
strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1);
|
||||
free((char*)relay_description);
|
||||
} else {
|
||||
strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1);
|
||||
}
|
||||
|
||||
const char* relay_software = get_config_value_from_table("relay_software");
|
||||
if (relay_software) {
|
||||
strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1);
|
||||
free((char*)relay_software);
|
||||
} else {
|
||||
strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1);
|
||||
}
|
||||
|
||||
const char* relay_version = get_config_value_from_table("relay_version");
|
||||
if (relay_version) {
|
||||
strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1);
|
||||
free((char*)relay_version);
|
||||
} else {
|
||||
strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1);
|
||||
}
|
||||
|
||||
const char* relay_contact = get_config_value_from_table("relay_contact");
|
||||
if (relay_contact) {
|
||||
strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1);
|
||||
free((char*)relay_contact);
|
||||
}
|
||||
|
||||
const char* relay_pubkey = get_config_value_from_table("relay_pubkey");
|
||||
if (relay_pubkey) {
|
||||
strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
|
||||
free((char*)relay_pubkey);
|
||||
}
|
||||
|
||||
const char* posting_policy = get_config_value_from_table("posting_policy");
|
||||
if (posting_policy) {
|
||||
strncpy(g_unified_cache.relay_info.posting_policy, posting_policy, sizeof(g_unified_cache.relay_info.posting_policy) - 1);
|
||||
free((char*)posting_policy);
|
||||
}
|
||||
|
||||
const char* payments_url = get_config_value_from_table("payments_url");
|
||||
if (payments_url) {
|
||||
strncpy(g_unified_cache.relay_info.payments_url, payments_url, sizeof(g_unified_cache.relay_info.payments_url) - 1);
|
||||
free((char*)payments_url);
|
||||
}
|
||||
|
||||
// Rebuild supported_nips array
|
||||
const char* supported_nips_csv = get_config_value_from_table("supported_nips");
|
||||
if (supported_nips_csv) {
|
||||
g_unified_cache.relay_info.supported_nips = parse_comma_separated_array(supported_nips_csv);
|
||||
free((char*)supported_nips_csv);
|
||||
} else {
|
||||
g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
|
||||
if (g_unified_cache.relay_info.supported_nips) {
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9));
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11));
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13));
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15));
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20));
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40));
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42));
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild limitation object
|
||||
int max_message_length = 16384;
|
||||
const char* max_msg_str = get_config_value_from_table("max_message_length");
|
||||
if (max_msg_str) {
|
||||
max_message_length = atoi(max_msg_str);
|
||||
free((char*)max_msg_str);
|
||||
}
|
||||
|
||||
int max_subscriptions_per_client = 20;
|
||||
const char* max_subs_str = get_config_value_from_table("max_subscriptions_per_client");
|
||||
if (max_subs_str) {
|
||||
max_subscriptions_per_client = atoi(max_subs_str);
|
||||
free((char*)max_subs_str);
|
||||
}
|
||||
|
||||
int max_limit = 5000;
|
||||
const char* max_limit_str = get_config_value_from_table("max_limit");
|
||||
if (max_limit_str) {
|
||||
max_limit = atoi(max_limit_str);
|
||||
free((char*)max_limit_str);
|
||||
}
|
||||
|
||||
int max_event_tags = 100;
|
||||
const char* max_tags_str = get_config_value_from_table("max_event_tags");
|
||||
if (max_tags_str) {
|
||||
max_event_tags = atoi(max_tags_str);
|
||||
free((char*)max_tags_str);
|
||||
}
|
||||
|
||||
int max_content_length = 8196;
|
||||
const char* max_content_str = get_config_value_from_table("max_content_length");
|
||||
if (max_content_str) {
|
||||
max_content_length = atoi(max_content_str);
|
||||
free((char*)max_content_str);
|
||||
}
|
||||
|
||||
int default_limit = 500;
|
||||
const char* default_limit_str = get_config_value_from_table("default_limit");
|
||||
if (default_limit_str) {
|
||||
default_limit = atoi(default_limit_str);
|
||||
free((char*)default_limit_str);
|
||||
}
|
||||
|
||||
int admin_enabled = 0;
|
||||
const char* admin_enabled_str = get_config_value_from_table("admin_enabled");
|
||||
if (admin_enabled_str) {
|
||||
admin_enabled = (strcmp(admin_enabled_str, "true") == 0) ? 1 : 0;
|
||||
free((char*)admin_enabled_str);
|
||||
}
|
||||
|
||||
g_unified_cache.relay_info.limitation = cJSON_CreateObject();
|
||||
if (g_unified_cache.relay_info.limitation) {
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty);
|
||||
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
|
||||
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False);
|
||||
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647);
|
||||
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit);
|
||||
}
|
||||
|
||||
// Rebuild other arrays (empty for now)
|
||||
g_unified_cache.relay_info.retention = cJSON_CreateArray();
|
||||
g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
|
||||
if (g_unified_cache.relay_info.language_tags) {
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
|
||||
}
|
||||
g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
|
||||
if (g_unified_cache.relay_info.relay_countries) {
|
||||
cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
|
||||
}
|
||||
g_unified_cache.relay_info.tags = cJSON_CreateArray();
|
||||
g_unified_cache.relay_info.fees = cJSON_CreateObject();
|
||||
|
||||
}
|
||||
|
||||
// Add basic relay information
|
||||
if (strlen(g_unified_cache.relay_info.name) > 0) {
|
||||
cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name);
|
||||
if (relay_name && strlen(relay_name) > 0) {
|
||||
cJSON_AddStringToObject(info, "name", relay_name);
|
||||
free((char*)relay_name);
|
||||
} else {
|
||||
cJSON_AddStringToObject(info, "name", "C Nostr Relay");
|
||||
}
|
||||
if (strlen(g_unified_cache.relay_info.description) > 0) {
|
||||
cJSON_AddStringToObject(info, "description", g_unified_cache.relay_info.description);
|
||||
|
||||
if (relay_description && strlen(relay_description) > 0) {
|
||||
cJSON_AddStringToObject(info, "description", relay_description);
|
||||
free((char*)relay_description);
|
||||
} else {
|
||||
cJSON_AddStringToObject(info, "description", "A high-performance Nostr relay implemented in C with SQLite storage");
|
||||
}
|
||||
if (strlen(g_unified_cache.relay_info.banner) > 0) {
|
||||
cJSON_AddStringToObject(info, "banner", g_unified_cache.relay_info.banner);
|
||||
|
||||
if (relay_banner && strlen(relay_banner) > 0) {
|
||||
cJSON_AddStringToObject(info, "banner", relay_banner);
|
||||
free((char*)relay_banner);
|
||||
}
|
||||
if (strlen(g_unified_cache.relay_info.icon) > 0) {
|
||||
cJSON_AddStringToObject(info, "icon", g_unified_cache.relay_info.icon);
|
||||
|
||||
if (relay_icon && strlen(relay_icon) > 0) {
|
||||
cJSON_AddStringToObject(info, "icon", relay_icon);
|
||||
free((char*)relay_icon);
|
||||
}
|
||||
if (strlen(g_unified_cache.relay_info.pubkey) > 0) {
|
||||
cJSON_AddStringToObject(info, "pubkey", g_unified_cache.relay_info.pubkey);
|
||||
|
||||
if (relay_pubkey && strlen(relay_pubkey) > 0) {
|
||||
cJSON_AddStringToObject(info, "pubkey", relay_pubkey);
|
||||
free((char*)relay_pubkey);
|
||||
}
|
||||
if (strlen(g_unified_cache.relay_info.contact) > 0) {
|
||||
cJSON_AddStringToObject(info, "contact", g_unified_cache.relay_info.contact);
|
||||
|
||||
if (relay_contact && strlen(relay_contact) > 0) {
|
||||
cJSON_AddStringToObject(info, "contact", relay_contact);
|
||||
free((char*)relay_contact);
|
||||
}
|
||||
|
||||
|
||||
// Add supported NIPs
|
||||
if (g_unified_cache.relay_info.supported_nips) {
|
||||
cJSON_AddItemToObject(info, "supported_nips", cJSON_Duplicate(g_unified_cache.relay_info.supported_nips, 1));
|
||||
if (supported_nips_csv && strlen(supported_nips_csv) > 0) {
|
||||
cJSON* supported_nips = parse_comma_separated_array(supported_nips_csv);
|
||||
if (supported_nips) {
|
||||
cJSON_AddItemToObject(info, "supported_nips", supported_nips);
|
||||
}
|
||||
free((char*)supported_nips_csv);
|
||||
} else {
|
||||
// Default supported NIPs
|
||||
cJSON* supported_nips = cJSON_CreateArray();
|
||||
if (supported_nips) {
|
||||
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
|
||||
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
|
||||
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
|
||||
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
|
||||
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
|
||||
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
|
||||
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
|
||||
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
|
||||
cJSON_AddItemToObject(info, "supported_nips", supported_nips);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add software information
|
||||
if (strlen(g_unified_cache.relay_info.software) > 0) {
|
||||
cJSON_AddStringToObject(info, "software", g_unified_cache.relay_info.software);
|
||||
if (relay_software && strlen(relay_software) > 0) {
|
||||
cJSON_AddStringToObject(info, "software", relay_software);
|
||||
free((char*)relay_software);
|
||||
} else {
|
||||
cJSON_AddStringToObject(info, "software", "https://git.laantungir.net/laantungir/c-relay.git");
|
||||
}
|
||||
if (strlen(g_unified_cache.relay_info.version) > 0) {
|
||||
cJSON_AddStringToObject(info, "version", g_unified_cache.relay_info.version);
|
||||
|
||||
if (relay_version && strlen(relay_version) > 0) {
|
||||
cJSON_AddStringToObject(info, "version", relay_version);
|
||||
free((char*)relay_version);
|
||||
} else {
|
||||
cJSON_AddStringToObject(info, "version", "0.2.0");
|
||||
}
|
||||
|
||||
|
||||
// Add policies
|
||||
if (strlen(g_unified_cache.relay_info.privacy_policy) > 0) {
|
||||
cJSON_AddStringToObject(info, "privacy_policy", g_unified_cache.relay_info.privacy_policy);
|
||||
if (privacy_policy && strlen(privacy_policy) > 0) {
|
||||
cJSON_AddStringToObject(info, "privacy_policy", privacy_policy);
|
||||
free((char*)privacy_policy);
|
||||
}
|
||||
if (strlen(g_unified_cache.relay_info.terms_of_service) > 0) {
|
||||
cJSON_AddStringToObject(info, "terms_of_service", g_unified_cache.relay_info.terms_of_service);
|
||||
|
||||
if (terms_of_service && strlen(terms_of_service) > 0) {
|
||||
cJSON_AddStringToObject(info, "terms_of_service", terms_of_service);
|
||||
free((char*)terms_of_service);
|
||||
}
|
||||
if (strlen(g_unified_cache.relay_info.posting_policy) > 0) {
|
||||
cJSON_AddStringToObject(info, "posting_policy", g_unified_cache.relay_info.posting_policy);
|
||||
|
||||
if (posting_policy && strlen(posting_policy) > 0) {
|
||||
cJSON_AddStringToObject(info, "posting_policy", posting_policy);
|
||||
free((char*)posting_policy);
|
||||
}
|
||||
|
||||
|
||||
// Add server limitations
|
||||
if (g_unified_cache.relay_info.limitation) {
|
||||
cJSON_AddItemToObject(info, "limitation", cJSON_Duplicate(g_unified_cache.relay_info.limitation, 1));
|
||||
cJSON* limitation = cJSON_CreateObject();
|
||||
if (limitation) {
|
||||
cJSON_AddNumberToObject(limitation, "max_message_length", max_message_length);
|
||||
cJSON_AddNumberToObject(limitation, "max_subscriptions", max_subscriptions_per_client);
|
||||
cJSON_AddNumberToObject(limitation, "max_limit", max_limit);
|
||||
cJSON_AddNumberToObject(limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
|
||||
cJSON_AddNumberToObject(limitation, "max_event_tags", max_event_tags);
|
||||
cJSON_AddNumberToObject(limitation, "max_content_length", max_content_length);
|
||||
cJSON_AddNumberToObject(limitation, "min_pow_difficulty", min_pow_difficulty);
|
||||
cJSON_AddBoolToObject(limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
|
||||
cJSON_AddBoolToObject(limitation, "payment_required", cJSON_False);
|
||||
cJSON_AddBoolToObject(limitation, "restricted_writes", cJSON_False);
|
||||
cJSON_AddNumberToObject(limitation, "created_at_lower_limit", 0);
|
||||
cJSON_AddNumberToObject(limitation, "created_at_upper_limit", 2147483647);
|
||||
cJSON_AddNumberToObject(limitation, "default_limit", default_limit);
|
||||
cJSON_AddItemToObject(info, "limitation", limitation);
|
||||
}
|
||||
|
||||
// Add retention policies if configured
|
||||
if (g_unified_cache.relay_info.retention && cJSON_GetArraySize(g_unified_cache.relay_info.retention) > 0) {
|
||||
cJSON_AddItemToObject(info, "retention", cJSON_Duplicate(g_unified_cache.relay_info.retention, 1));
|
||||
|
||||
// Add retention policies (empty array for now)
|
||||
cJSON* retention = cJSON_CreateArray();
|
||||
if (retention) {
|
||||
cJSON_AddItemToObject(info, "retention", retention);
|
||||
}
|
||||
|
||||
|
||||
// Add geographical and language information
|
||||
if (g_unified_cache.relay_info.relay_countries) {
|
||||
cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_unified_cache.relay_info.relay_countries, 1));
|
||||
if (relay_countries_csv && strlen(relay_countries_csv) > 0) {
|
||||
cJSON* relay_countries = parse_comma_separated_array(relay_countries_csv);
|
||||
if (relay_countries) {
|
||||
cJSON_AddItemToObject(info, "relay_countries", relay_countries);
|
||||
}
|
||||
free((char*)relay_countries_csv);
|
||||
} else {
|
||||
cJSON* relay_countries = cJSON_CreateArray();
|
||||
if (relay_countries) {
|
||||
cJSON_AddItemToArray(relay_countries, cJSON_CreateString("*"));
|
||||
cJSON_AddItemToObject(info, "relay_countries", relay_countries);
|
||||
}
|
||||
}
|
||||
if (g_unified_cache.relay_info.language_tags) {
|
||||
cJSON_AddItemToObject(info, "language_tags", cJSON_Duplicate(g_unified_cache.relay_info.language_tags, 1));
|
||||
|
||||
if (language_tags_csv && strlen(language_tags_csv) > 0) {
|
||||
cJSON* language_tags = parse_comma_separated_array(language_tags_csv);
|
||||
if (language_tags) {
|
||||
cJSON_AddItemToObject(info, "language_tags", language_tags);
|
||||
}
|
||||
free((char*)language_tags_csv);
|
||||
} else {
|
||||
cJSON* language_tags = cJSON_CreateArray();
|
||||
if (language_tags) {
|
||||
cJSON_AddItemToArray(language_tags, cJSON_CreateString("*"));
|
||||
cJSON_AddItemToObject(info, "language_tags", language_tags);
|
||||
}
|
||||
}
|
||||
if (g_unified_cache.relay_info.tags && cJSON_GetArraySize(g_unified_cache.relay_info.tags) > 0) {
|
||||
cJSON_AddItemToObject(info, "tags", cJSON_Duplicate(g_unified_cache.relay_info.tags, 1));
|
||||
|
||||
// Add content tags (empty array)
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (tags) {
|
||||
cJSON_AddItemToObject(info, "tags", tags);
|
||||
}
|
||||
|
||||
|
||||
// Add payment information if configured
|
||||
if (strlen(g_unified_cache.relay_info.payments_url) > 0) {
|
||||
cJSON_AddStringToObject(info, "payments_url", g_unified_cache.relay_info.payments_url);
|
||||
if (payments_url && strlen(payments_url) > 0) {
|
||||
cJSON_AddStringToObject(info, "payments_url", payments_url);
|
||||
free((char*)payments_url);
|
||||
}
|
||||
if (g_unified_cache.relay_info.fees && cJSON_GetObjectItem(g_unified_cache.relay_info.fees, "admission")) {
|
||||
cJSON_AddItemToObject(info, "fees", cJSON_Duplicate(g_unified_cache.relay_info.fees, 1));
|
||||
|
||||
// Add fees (empty object - no payment required by default)
|
||||
cJSON* fees = cJSON_CreateObject();
|
||||
if (fees) {
|
||||
cJSON_AddItemToObject(info, "fees", fees);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
@@ -534,7 +304,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
|
||||
}
|
||||
|
||||
if (!accepts_nostr_json) {
|
||||
log_warning("HTTP request without proper Accept header for NIP-11");
|
||||
DEBUG_WARN("HTTP request without proper Accept header for NIP-11");
|
||||
// Return 406 Not Acceptable
|
||||
unsigned char buf[LWS_PRE + 256];
|
||||
unsigned char *p = &buf[LWS_PRE];
|
||||
@@ -560,7 +330,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
|
||||
// Generate relay information JSON
|
||||
cJSON* info_json = generate_relay_info_json();
|
||||
if (!info_json) {
|
||||
log_error("Failed to generate relay info JSON");
|
||||
DEBUG_ERROR("Failed to generate relay info JSON");
|
||||
unsigned char buf[LWS_PRE + 256];
|
||||
unsigned char *p = &buf[LWS_PRE];
|
||||
unsigned char *start = p;
|
||||
@@ -586,7 +356,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
|
||||
cJSON_Delete(info_json);
|
||||
|
||||
if (!json_string) {
|
||||
log_error("Failed to serialize relay info JSON");
|
||||
DEBUG_ERROR("Failed to serialize relay info JSON");
|
||||
unsigned char buf[LWS_PRE + 256];
|
||||
unsigned char *p = &buf[LWS_PRE];
|
||||
unsigned char *start = p;
|
||||
@@ -613,7 +383,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
|
||||
// Allocate session data to manage buffer lifetime across callbacks
|
||||
struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data));
|
||||
if (!session_data) {
|
||||
log_error("Failed to allocate NIP-11 session data");
|
||||
DEBUG_ERROR("Failed to allocate NIP-11 session data");
|
||||
free(json_string);
|
||||
return -1;
|
||||
}
|
||||
|
||||
95
src/nip013.c
95
src/nip013.c
@@ -1,5 +1,6 @@
|
||||
// NIP-13 Proof of Work validation module
|
||||
#include <stdio.h>
|
||||
#include "debug.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
@@ -8,69 +9,39 @@
|
||||
#include "../nostr_core_lib/nostr_core/nip013.h"
|
||||
#include "config.h"
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_info(const char* message);
|
||||
void log_success(const char* message);
|
||||
void log_error(const char* message);
|
||||
void log_warning(const char* message);
|
||||
|
||||
// NIP-13 PoW configuration structure
|
||||
struct pow_config {
|
||||
int enabled; // 0 = disabled, 1 = enabled
|
||||
int min_pow_difficulty; // Minimum required difficulty (0 = no requirement)
|
||||
int validation_flags; // Bitflags for validation options
|
||||
int require_nonce_tag; // 1 = require nonce tag presence
|
||||
int reject_lower_targets; // 1 = reject if committed < actual difficulty
|
||||
int strict_format; // 1 = enforce strict nonce tag format
|
||||
int anti_spam_mode; // 1 = full anti-spam validation
|
||||
};
|
||||
// Configuration functions from config.c
|
||||
extern int get_config_bool(const char* key, int default_value);
|
||||
extern int get_config_int(const char* key, int default_value);
|
||||
extern const char* get_config_value(const char* key);
|
||||
|
||||
// Initialize PoW configuration using configuration system
|
||||
void init_pow_config() {
|
||||
|
||||
// Get all config values first (without holding mutex to avoid deadlock)
|
||||
int pow_enabled = get_config_bool("pow_enabled", 1);
|
||||
int pow_min_difficulty = get_config_int("pow_min_difficulty", 0);
|
||||
const char* pow_mode = get_config_value("pow_mode");
|
||||
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
|
||||
// Load PoW settings from configuration system
|
||||
g_unified_cache.pow_config.enabled = pow_enabled;
|
||||
g_unified_cache.pow_config.min_pow_difficulty = pow_min_difficulty;
|
||||
|
||||
// Configure PoW mode
|
||||
if (pow_mode) {
|
||||
if (strcmp(pow_mode, "strict") == 0) {
|
||||
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
|
||||
g_unified_cache.pow_config.require_nonce_tag = 1;
|
||||
g_unified_cache.pow_config.reject_lower_targets = 1;
|
||||
g_unified_cache.pow_config.strict_format = 1;
|
||||
g_unified_cache.pow_config.anti_spam_mode = 1;
|
||||
} else if (strcmp(pow_mode, "full") == 0) {
|
||||
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL;
|
||||
g_unified_cache.pow_config.require_nonce_tag = 1;
|
||||
} else if (strcmp(pow_mode, "basic") == 0) {
|
||||
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
|
||||
} else if (strcmp(pow_mode, "disabled") == 0) {
|
||||
g_unified_cache.pow_config.enabled = 0;
|
||||
}
|
||||
free((char*)pow_mode); // Free dynamically allocated string
|
||||
} else {
|
||||
// Default to basic mode
|
||||
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
// Configuration is now handled directly through database queries
|
||||
// No cache initialization needed
|
||||
}
|
||||
|
||||
// Validate event Proof of Work according to NIP-13
|
||||
int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
int enabled = g_unified_cache.pow_config.enabled;
|
||||
int min_pow_difficulty = g_unified_cache.pow_config.min_pow_difficulty;
|
||||
int validation_flags = g_unified_cache.pow_config.validation_flags;
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
// Get PoW configuration directly from database
|
||||
int enabled = get_config_bool("pow_enabled", 1);
|
||||
int min_pow_difficulty = get_config_int("pow_min_difficulty", 0);
|
||||
const char* pow_mode = get_config_value("pow_mode");
|
||||
|
||||
// Determine validation flags based on mode
|
||||
int validation_flags = NOSTR_POW_VALIDATE_BASIC; // Default
|
||||
if (pow_mode) {
|
||||
if (strcmp(pow_mode, "strict") == 0) {
|
||||
validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
|
||||
} else if (strcmp(pow_mode, "full") == 0) {
|
||||
validation_flags = NOSTR_POW_VALIDATE_FULL;
|
||||
} else if (strcmp(pow_mode, "basic") == 0) {
|
||||
validation_flags = NOSTR_POW_VALIDATE_BASIC;
|
||||
} else if (strcmp(pow_mode, "disabled") == 0) {
|
||||
enabled = 0;
|
||||
}
|
||||
free((char*)pow_mode);
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
return 0; // PoW validation disabled
|
||||
@@ -121,39 +92,39 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
|
||||
snprintf(error_message, error_size,
|
||||
"pow: insufficient difficulty: %d < %d",
|
||||
pow_result.actual_difficulty, min_pow_difficulty);
|
||||
log_warning("Event rejected: insufficient PoW difficulty");
|
||||
DEBUG_WARN("Event rejected: insufficient PoW difficulty");
|
||||
break;
|
||||
case NOSTR_ERROR_NIP13_NO_NONCE_TAG:
|
||||
// This should not happen with min_difficulty=0 after our check above
|
||||
if (min_pow_difficulty > 0) {
|
||||
snprintf(error_message, error_size, "pow: missing required nonce tag");
|
||||
log_warning("Event rejected: missing nonce tag");
|
||||
DEBUG_WARN("Event rejected: missing nonce tag");
|
||||
} else {
|
||||
return 0; // Allow when min_difficulty=0
|
||||
}
|
||||
break;
|
||||
case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG:
|
||||
snprintf(error_message, error_size, "pow: invalid nonce tag format");
|
||||
log_warning("Event rejected: invalid nonce tag format");
|
||||
DEBUG_WARN("Event rejected: invalid nonce tag format");
|
||||
break;
|
||||
case NOSTR_ERROR_NIP13_TARGET_MISMATCH:
|
||||
snprintf(error_message, error_size,
|
||||
"pow: committed target (%d) lower than minimum (%d)",
|
||||
pow_result.committed_target, min_pow_difficulty);
|
||||
log_warning("Event rejected: committed target too low (anti-spam protection)");
|
||||
DEBUG_WARN("Event rejected: committed target too low (anti-spam protection)");
|
||||
break;
|
||||
case NOSTR_ERROR_NIP13_CALCULATION:
|
||||
snprintf(error_message, error_size, "pow: difficulty calculation failed");
|
||||
log_error("PoW difficulty calculation error");
|
||||
DEBUG_ERROR("PoW difficulty calculation error");
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_ID:
|
||||
snprintf(error_message, error_size, "pow: invalid event ID format");
|
||||
log_warning("Event rejected: invalid event ID for PoW calculation");
|
||||
DEBUG_WARN("Event rejected: invalid event ID for PoW calculation");
|
||||
break;
|
||||
default:
|
||||
snprintf(error_message, error_size, "pow: validation failed - %s",
|
||||
strlen(pow_result.error_detail) > 0 ? pow_result.error_detail : "unknown error");
|
||||
log_warning("Event rejected: PoW validation failed");
|
||||
DEBUG_WARN("Event rejected: PoW validation failed");
|
||||
}
|
||||
return validation_result;
|
||||
}
|
||||
|
||||
10
src/nip040.c
10
src/nip040.c
@@ -1,5 +1,6 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include "debug.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
@@ -28,9 +29,6 @@ struct expiration_config g_expiration_config = {
|
||||
.grace_period = 1 // 1 second grace period for testing (was 300)
|
||||
};
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_info(const char* message);
|
||||
void log_warning(const char* message);
|
||||
|
||||
// Initialize expiration configuration using configuration system
|
||||
void init_expiration_config() {
|
||||
@@ -51,7 +49,7 @@ void init_expiration_config() {
|
||||
|
||||
// Validate grace period bounds
|
||||
if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) {
|
||||
log_warning("Invalid grace period, using default of 300 seconds");
|
||||
DEBUG_WARN("Invalid grace period, using default of 300 seconds");
|
||||
g_expiration_config.grace_period = 300;
|
||||
}
|
||||
|
||||
@@ -94,7 +92,7 @@ long extract_expiration_timestamp(cJSON* tags) {
|
||||
char debug_msg[256];
|
||||
snprintf(debug_msg, sizeof(debug_msg),
|
||||
"Ignoring malformed expiration tag value: '%.32s'", value);
|
||||
log_warning(debug_msg);
|
||||
DEBUG_WARN(debug_msg);
|
||||
continue; // Ignore malformed expiration tag
|
||||
}
|
||||
|
||||
@@ -148,7 +146,7 @@ int validate_event_expiration(cJSON* event, char* error_message, size_t error_si
|
||||
snprintf(error_message, error_size,
|
||||
"invalid: event expired (expiration=%ld, current=%ld, grace=%ld)",
|
||||
expiration_ts, (long)current_time, g_expiration_config.grace_period);
|
||||
log_warning("Event rejected: expired timestamp");
|
||||
DEBUG_WARN("Event rejected: expired timestamp");
|
||||
return -1;
|
||||
} else {
|
||||
// In non-strict mode, allow expired events
|
||||
|
||||
41
src/nip042.c
41
src/nip042.c
@@ -6,17 +6,14 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
#include <pthread.h>
|
||||
#include "debug.h"
|
||||
#include <cjson/cJSON.h>
|
||||
#include <libwebsockets.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include "websockets.h"
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_error(const char* message);
|
||||
void log_info(const char* message);
|
||||
void log_warning(const char* message);
|
||||
void log_success(const char* message);
|
||||
|
||||
// Forward declaration for notice message function
|
||||
void send_notice_message(struct lws* wsi, const char* message);
|
||||
@@ -26,23 +23,7 @@ int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size);
|
||||
int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
|
||||
const char *relay_url, int time_tolerance_seconds);
|
||||
|
||||
// Forward declaration for per_session_data struct (defined in main.c)
|
||||
struct per_session_data {
|
||||
int authenticated;
|
||||
void* subscriptions; // Head of this session's subscription list
|
||||
pthread_mutex_t session_lock; // Per-session thread safety
|
||||
char client_ip[41]; // Client IP for logging
|
||||
int subscription_count; // Number of subscriptions for this session
|
||||
|
||||
// NIP-42 Authentication State
|
||||
char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
|
||||
char active_challenge[65]; // Current challenge for this session (64 hex + null)
|
||||
time_t challenge_created; // When challenge was created
|
||||
time_t challenge_expires; // Challenge expiration time
|
||||
int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
|
||||
int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
|
||||
int auth_challenge_sent; // Whether challenge has been sent (0/1)
|
||||
};
|
||||
// Forward declaration for per_session_data struct (defined in websockets.h)
|
||||
|
||||
|
||||
// Send NIP-42 authentication challenge to client
|
||||
@@ -52,7 +33,7 @@ void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) {
|
||||
// Generate challenge using existing request_validator function
|
||||
char challenge[65];
|
||||
if (nostr_nip42_generate_challenge(challenge, sizeof(challenge)) != 0) {
|
||||
log_error("Failed to generate NIP-42 challenge");
|
||||
DEBUG_ERROR("Failed to generate NIP-42 challenge");
|
||||
send_notice_message(wsi, "Authentication temporarily unavailable");
|
||||
return;
|
||||
}
|
||||
@@ -74,11 +55,9 @@ void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) {
|
||||
char* msg_str = cJSON_Print(auth_msg);
|
||||
if (msg_str) {
|
||||
size_t msg_len = strlen(msg_str);
|
||||
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
||||
if (buf) {
|
||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
||||
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
||||
free(buf);
|
||||
// Use proper message queue system instead of direct lws_write
|
||||
if (queue_message(wsi, pss, msg_str, msg_len, LWS_WRITE_TEXT) != 0) {
|
||||
DEBUG_ERROR("Failed to queue AUTH challenge message");
|
||||
}
|
||||
free(msg_str);
|
||||
}
|
||||
@@ -108,7 +87,7 @@ void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* ps
|
||||
if (current_time > challenge_expires) {
|
||||
free(event_json);
|
||||
send_notice_message(wsi, "Authentication challenge expired, please retry");
|
||||
log_warning("NIP-42 authentication failed: challenge expired");
|
||||
DEBUG_WARN("NIP-42 authentication failed: challenge expired");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,7 +133,7 @@ void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* ps
|
||||
char error_msg[256];
|
||||
snprintf(error_msg, sizeof(error_msg),
|
||||
"NIP-42 authentication failed (error code: %d)", result);
|
||||
log_warning(error_msg);
|
||||
DEBUG_WARN(error_msg);
|
||||
|
||||
send_notice_message(wsi, "NIP-42 authentication failed - invalid signature or challenge");
|
||||
}
|
||||
@@ -166,6 +145,6 @@ void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_da
|
||||
|
||||
// NIP-42 doesn't typically use challenge responses from client to server
|
||||
// This is reserved for potential future use or protocol extensions
|
||||
log_warning("Received unexpected challenge response from client (not part of standard NIP-42 flow)");
|
||||
DEBUG_WARN("Received unexpected challenge response from client (not part of standard NIP-42 flow)");
|
||||
send_notice_message(wsi, "Challenge responses are not supported - please send signed authentication event");
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work
|
||||
#include "../nostr_core_lib/nostr_core/nostr_common.h"
|
||||
#include "../nostr_core_lib/nostr_core/utils.h"
|
||||
#include "debug.h" // C-relay debug system
|
||||
#include "config.h" // C-relay configuration system
|
||||
#include <sqlite3.h>
|
||||
#include <stdio.h>
|
||||
@@ -294,10 +295,26 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// PHASE 3: EVENT KIND SPECIFIC VALIDATION
|
||||
// PHASE 3: ADMIN EVENT BYPASS CHECK
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 8. Handle NIP-42 authentication challenge events (kind 22242)
|
||||
// 8. Check if this is a kind 23456 admin event from authorized admin
|
||||
// This must happen AFTER signature validation but BEFORE auth rules
|
||||
if (event_kind == 23456) {
|
||||
const char* admin_pubkey = get_config_value("admin_pubkey");
|
||||
if (admin_pubkey && strcmp(event_pubkey, admin_pubkey) == 0) {
|
||||
// Valid admin event - bypass remaining validation
|
||||
cJSON_Delete(event);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
// Not from admin - continue with normal validation
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// PHASE 4: EVENT KIND SPECIFIC VALIDATION
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 9. Handle NIP-42 authentication challenge events (kind 22242)
|
||||
if (event_kind == 22242) {
|
||||
// Check NIP-42 mode using unified cache
|
||||
const char* nip42_enabled = get_config_value("nip42_auth_enabled");
|
||||
@@ -315,13 +332,13 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// PHASE 4: AUTHENTICATION RULES (Database Queries)
|
||||
// PHASE 5: AUTHENTICATION RULES (Database Queries)
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 9. Check if authentication rules are enabled
|
||||
// 10. Check if authentication rules are enabled
|
||||
if (!auth_required) {
|
||||
} else {
|
||||
// 10. Check database authentication rules (only if auth enabled)
|
||||
// 11. Check database authentication rules (only if auth enabled)
|
||||
|
||||
// Create operation string with event kind for more specific rule matching
|
||||
char operation_str[64];
|
||||
@@ -340,16 +357,14 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// PHASE 5: ADDITIONAL VALIDATIONS (C-relay specific)
|
||||
// PHASE 6: ADDITIONAL VALIDATIONS (C-relay specific)
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 11. NIP-13 Proof of Work validation
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
int pow_enabled = g_unified_cache.pow_config.enabled;
|
||||
int pow_min_difficulty = g_unified_cache.pow_config.min_pow_difficulty;
|
||||
int pow_validation_flags = g_unified_cache.pow_config.validation_flags;
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
|
||||
// 12. NIP-13 Proof of Work validation
|
||||
int pow_enabled = get_config_bool("pow_enabled", 0);
|
||||
int pow_min_difficulty = get_config_int("pow_min_difficulty", 0);
|
||||
int pow_validation_flags = get_config_int("pow_validation_flags", 1);
|
||||
|
||||
if (pow_enabled && pow_min_difficulty > 0) {
|
||||
nostr_pow_result_t pow_result;
|
||||
int pow_validation_result = nostr_validate_pow(event, pow_min_difficulty,
|
||||
@@ -361,7 +376,7 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
|
||||
}
|
||||
}
|
||||
|
||||
// 12. NIP-40 Expiration validation
|
||||
// 13. NIP-40 Expiration validation
|
||||
// Always check expiration tags if present (following NIP-40 specification)
|
||||
|
||||
cJSON *expiration_tag = NULL;
|
||||
@@ -489,11 +504,10 @@ void nostr_request_result_free_file_data(nostr_request_result_t *result) {
|
||||
|
||||
|
||||
/**
|
||||
* Force cache refresh - use unified cache system
|
||||
* Force cache refresh - cache no longer exists, function kept for compatibility
|
||||
*/
|
||||
void nostr_request_validator_force_cache_refresh(void) {
|
||||
// Use unified cache refresh from config.c
|
||||
force_config_cache_refresh();
|
||||
// Cache no longer exists - direct database queries are used
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -518,6 +532,8 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int rc;
|
||||
|
||||
DEBUG_TRACE("Checking auth rules for pubkey: %s", pubkey);
|
||||
|
||||
if (!pubkey) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
@@ -534,19 +550,21 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
|
||||
|
||||
// Step 1: Check pubkey blacklist (highest priority)
|
||||
const char *blacklist_sql =
|
||||
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
|
||||
"'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
|
||||
"SELECT rule_type FROM auth_rules WHERE rule_type = "
|
||||
"'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
|
||||
DEBUG_TRACE("Blacklist SQL: %s", blacklist_sql);
|
||||
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const char *action = (const char *)sqlite3_column_text(stmt, 1);
|
||||
int step_result = sqlite3_step(stmt);
|
||||
DEBUG_TRACE("Blacklist query result: %s", step_result == SQLITE_ROW ? "FOUND" : "NOT_FOUND");
|
||||
|
||||
if (step_result == SQLITE_ROW) {
|
||||
DEBUG_TRACE("BLACKLIST HIT: Denying access for pubkey: %s", pubkey);
|
||||
// Set specific violation details for status code mapping
|
||||
strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist");
|
||||
sprintf(g_last_rule_violation.reason, "Public key blacklisted: %s",
|
||||
action ? action : "PUBKEY_BLACKLIST");
|
||||
sprintf(g_last_rule_violation.reason, "Public key blacklisted");
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
@@ -558,19 +576,16 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
|
||||
// Step 2: Check hash blacklist
|
||||
if (resource_hash) {
|
||||
const char *hash_blacklist_sql =
|
||||
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
|
||||
"'blacklist' AND pattern_type = 'hash' AND pattern_value = ? LIMIT 1";
|
||||
"SELECT rule_type FROM auth_rules WHERE rule_type = "
|
||||
"'blacklist' AND pattern_type = 'hash' AND pattern_value = ? AND active = 1 LIMIT 1";
|
||||
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const char *action = (const char *)sqlite3_column_text(stmt, 1);
|
||||
|
||||
// Set specific violation details for status code mapping
|
||||
strcpy(g_last_rule_violation.violation_type, "hash_blacklist");
|
||||
sprintf(g_last_rule_violation.reason, "File hash blacklisted: %s",
|
||||
action ? action : "HASH_BLACKLIST");
|
||||
sprintf(g_last_rule_violation.reason, "File hash blacklisted");
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
@@ -582,8 +597,8 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
|
||||
|
||||
// Step 3: Check pubkey whitelist
|
||||
const char *whitelist_sql =
|
||||
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
|
||||
"'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
|
||||
"SELECT rule_type FROM auth_rules WHERE rule_type = "
|
||||
"'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
|
||||
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
|
||||
@@ -599,7 +614,7 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
|
||||
// Step 4: Check if any whitelist rules exist - if yes, deny by default
|
||||
const char *whitelist_exists_sql =
|
||||
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' "
|
||||
"AND pattern_type = 'pubkey' LIMIT 1";
|
||||
"AND pattern_type = 'pubkey' AND active = 1 LIMIT 1";
|
||||
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
|
||||
109
src/sql_schema.h
109
src/sql_schema.h
@@ -1,12 +1,12 @@
|
||||
/* Embedded SQL Schema for C Nostr Relay
|
||||
* Generated from db/schema.sql - Do not edit manually
|
||||
* Schema Version: 7
|
||||
* Schema Version: 8
|
||||
*/
|
||||
#ifndef SQL_SCHEMA_H
|
||||
#define SQL_SCHEMA_H
|
||||
|
||||
/* Schema version constant */
|
||||
#define EMBEDDED_SCHEMA_VERSION "7"
|
||||
#define EMBEDDED_SCHEMA_VERSION "8"
|
||||
|
||||
/* Embedded SQL schema as C string literal */
|
||||
static const char* const EMBEDDED_SCHEMA_SQL =
|
||||
@@ -15,7 +15,7 @@ static const char* const EMBEDDED_SCHEMA_SQL =
|
||||
-- Configuration system using config table\n\
|
||||
\n\
|
||||
-- Schema version tracking\n\
|
||||
PRAGMA user_version = 7;\n\
|
||||
PRAGMA user_version = 8;\n\
|
||||
\n\
|
||||
-- Enable foreign key support\n\
|
||||
PRAGMA foreign_keys = ON;\n\
|
||||
@@ -58,8 +58,8 @@ CREATE TABLE schema_info (\n\
|
||||
\n\
|
||||
-- Insert schema metadata\n\
|
||||
INSERT INTO schema_info (key, value) VALUES\n\
|
||||
('version', '7'),\n\
|
||||
('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),\n\
|
||||
('version', '8'),\n\
|
||||
('description', 'Hybrid Nostr relay schema with subscription deduplication support'),\n\
|
||||
('created_at', strftime('%s', 'now'));\n\
|
||||
\n\
|
||||
-- Helper views for common queries\n\
|
||||
@@ -93,16 +93,6 @@ FROM events\n\
|
||||
WHERE kind = 33334\n\
|
||||
ORDER BY created_at DESC;\n\
|
||||
\n\
|
||||
-- Optimization: Trigger for automatic cleanup of ephemeral events older than 1 hour\n\
|
||||
CREATE TRIGGER cleanup_ephemeral_events\n\
|
||||
AFTER INSERT ON events\n\
|
||||
WHEN NEW.event_type = 'ephemeral'\n\
|
||||
BEGIN\n\
|
||||
DELETE FROM events \n\
|
||||
WHERE event_type = 'ephemeral' \n\
|
||||
AND first_seen < (strftime('%s', 'now') - 3600);\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- Replaceable event handling trigger\n\
|
||||
CREATE TRIGGER handle_replaceable_events\n\
|
||||
AFTER INSERT ON events\n\
|
||||
@@ -142,8 +132,6 @@ CREATE TABLE auth_rules (\n\
|
||||
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required')),\n\
|
||||
pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global')),\n\
|
||||
pattern_value TEXT,\n\
|
||||
action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'require_auth', 'rate_limit')),\n\
|
||||
parameters TEXT, -- JSON parameters for rate limiting, etc.\n\
|
||||
active INTEGER NOT NULL DEFAULT 1,\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||
@@ -180,48 +168,22 @@ BEGIN\n\
|
||||
UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- Insert default configuration values\n\
|
||||
INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES\n\
|
||||
('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),\n\
|
||||
('relay_contact', '', 'string', 'Relay contact information', 'general', 0),\n\
|
||||
('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),\n\
|
||||
('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),\n\
|
||||
('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),\n\
|
||||
('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),\n\
|
||||
('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),\n\
|
||||
('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),\n\
|
||||
('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),\n\
|
||||
('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),\n\
|
||||
('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),\n\
|
||||
('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),\n\
|
||||
('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),\n\
|
||||
('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),\n\
|
||||
('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),\n\
|
||||
('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),\n\
|
||||
('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),\n\
|
||||
('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),\n\
|
||||
('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),\n\
|
||||
('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),\n\
|
||||
('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),\n\
|
||||
('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),\n\
|
||||
('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),\n\
|
||||
('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),\n\
|
||||
('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);\n\
|
||||
\n\
|
||||
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
||||
-- Optional database logging for subscription analytics and debugging\n\
|
||||
\n\
|
||||
-- Subscription events log\n\
|
||||
CREATE TABLE subscription_events (\n\
|
||||
-- Subscriptions log (renamed from subscription_events for clarity)\n\
|
||||
CREATE TABLE subscriptions (\n\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,\n\
|
||||
subscription_id TEXT NOT NULL, -- Subscription ID from client\n\
|
||||
wsi_pointer TEXT NOT NULL, -- WebSocket pointer address (hex string)\n\
|
||||
client_ip TEXT NOT NULL, -- Client IP address\n\
|
||||
event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),\n\
|
||||
filter_json TEXT, -- JSON representation of filters (for created events)\n\
|
||||
events_sent INTEGER DEFAULT 0, -- Number of events sent to this subscription\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
ended_at INTEGER, -- When subscription ended (for closed/expired/disconnected)\n\
|
||||
duration INTEGER -- Computed: ended_at - created_at\n\
|
||||
duration INTEGER, -- Computed: ended_at - created_at\n\
|
||||
UNIQUE(subscription_id, wsi_pointer) -- Prevent duplicate subscriptions per connection\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Subscription metrics summary\n\
|
||||
@@ -237,34 +199,23 @@ CREATE TABLE subscription_metrics (\n\
|
||||
UNIQUE(date)\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Event broadcasting log (optional, for detailed analytics)\n\
|
||||
CREATE TABLE event_broadcasts (\n\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,\n\
|
||||
event_id TEXT NOT NULL, -- Event ID that was broadcast\n\
|
||||
subscription_id TEXT NOT NULL, -- Subscription that received it\n\
|
||||
client_ip TEXT NOT NULL, -- Client IP\n\
|
||||
broadcast_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
FOREIGN KEY (event_id) REFERENCES events(id)\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Indexes for subscription logging performance\n\
|
||||
CREATE INDEX idx_subscription_events_id ON subscription_events(subscription_id);\n\
|
||||
CREATE INDEX idx_subscription_events_type ON subscription_events(event_type);\n\
|
||||
CREATE INDEX idx_subscription_events_created ON subscription_events(created_at DESC);\n\
|
||||
CREATE INDEX idx_subscription_events_client ON subscription_events(client_ip);\n\
|
||||
CREATE INDEX idx_subscriptions_id ON subscriptions(subscription_id);\n\
|
||||
CREATE INDEX idx_subscriptions_type ON subscriptions(event_type);\n\
|
||||
CREATE INDEX idx_subscriptions_created ON subscriptions(created_at DESC);\n\
|
||||
CREATE INDEX idx_subscriptions_client ON subscriptions(client_ip);\n\
|
||||
CREATE INDEX idx_subscriptions_wsi ON subscriptions(wsi_pointer);\n\
|
||||
\n\
|
||||
CREATE INDEX idx_subscription_metrics_date ON subscription_metrics(date DESC);\n\
|
||||
\n\
|
||||
CREATE INDEX idx_event_broadcasts_event ON event_broadcasts(event_id);\n\
|
||||
CREATE INDEX idx_event_broadcasts_sub ON event_broadcasts(subscription_id);\n\
|
||||
CREATE INDEX idx_event_broadcasts_time ON event_broadcasts(broadcast_at DESC);\n\
|
||||
\n\
|
||||
-- Trigger to update subscription duration when ended\n\
|
||||
CREATE TRIGGER update_subscription_duration\n\
|
||||
AFTER UPDATE OF ended_at ON subscription_events\n\
|
||||
AFTER UPDATE OF ended_at ON subscriptions\n\
|
||||
WHEN NEW.ended_at IS NOT NULL AND OLD.ended_at IS NULL\n\
|
||||
BEGIN\n\
|
||||
UPDATE subscription_events\n\
|
||||
UPDATE subscriptions\n\
|
||||
SET duration = NEW.ended_at - NEW.created_at\n\
|
||||
WHERE id = NEW.id;\n\
|
||||
END;\n\
|
||||
@@ -279,24 +230,26 @@ SELECT\n\
|
||||
MAX(events_sent) as max_events_sent,\n\
|
||||
AVG(events_sent) as avg_events_sent,\n\
|
||||
COUNT(DISTINCT client_ip) as unique_clients\n\
|
||||
FROM subscription_events\n\
|
||||
FROM subscriptions\n\
|
||||
GROUP BY date(created_at, 'unixepoch')\n\
|
||||
ORDER BY date DESC;\n\
|
||||
\n\
|
||||
-- View for current active subscriptions (from log perspective)\n\
|
||||
CREATE VIEW active_subscriptions_log AS\n\
|
||||
SELECT\n\
|
||||
subscription_id,\n\
|
||||
client_ip,\n\
|
||||
filter_json,\n\
|
||||
events_sent,\n\
|
||||
created_at,\n\
|
||||
(strftime('%s', 'now') - created_at) as duration_seconds\n\
|
||||
FROM subscription_events\n\
|
||||
WHERE event_type = 'created'\n\
|
||||
AND subscription_id NOT IN (\n\
|
||||
SELECT subscription_id FROM subscription_events\n\
|
||||
WHERE event_type IN ('closed', 'expired', 'disconnected')\n\
|
||||
s.subscription_id,\n\
|
||||
s.client_ip,\n\
|
||||
s.filter_json,\n\
|
||||
s.events_sent,\n\
|
||||
s.created_at,\n\
|
||||
(strftime('%s', 'now') - s.created_at) as duration_seconds\n\
|
||||
FROM subscriptions s\n\
|
||||
WHERE s.event_type = 'created'\n\
|
||||
AND NOT EXISTS (\n\
|
||||
SELECT 1 FROM subscriptions s2\n\
|
||||
WHERE s2.subscription_id = s.subscription_id\n\
|
||||
AND s2.wsi_pointer = s.wsi_pointer\n\
|
||||
AND s2.event_type IN ('closed', 'expired', 'disconnected')\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Database Statistics Views for Admin API\n\
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <cjson/cJSON.h>
|
||||
#include "debug.h"
|
||||
#include <sqlite3.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
@@ -10,9 +11,6 @@
|
||||
#include "subscriptions.h"
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_info(const char* message);
|
||||
void log_error(const char* message);
|
||||
void log_warning(const char* message);
|
||||
|
||||
// Forward declarations for configuration functions
|
||||
const char* get_config_value(const char* key);
|
||||
@@ -27,11 +25,14 @@ int validate_timestamp_range(long since, long until, char* error_message, size_t
|
||||
int validate_numeric_limits(int limit, char* error_message, size_t error_size);
|
||||
int validate_search_term(const char* search_term, char* error_message, size_t error_size);
|
||||
|
||||
// Forward declaration for monitoring function
|
||||
void monitoring_on_subscription_change(void);
|
||||
|
||||
// Global database variable
|
||||
extern sqlite3* g_db;
|
||||
|
||||
// Global unified cache
|
||||
extern unified_config_cache_t g_unified_cache;
|
||||
// Configuration functions from config.c
|
||||
extern int get_config_bool(const char* key, int default_value);
|
||||
|
||||
// Global subscription manager
|
||||
extern subscription_manager_t g_subscription_manager;
|
||||
@@ -52,7 +53,7 @@ subscription_filter_t* create_subscription_filter(cJSON* filter_json) {
|
||||
// Validate filter values before creating the filter
|
||||
char error_message[512] = {0};
|
||||
if (!validate_filter_values(filter_json, error_message, sizeof(error_message))) {
|
||||
log_warning(error_message);
|
||||
DEBUG_WARN(error_message);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -125,7 +126,7 @@ void free_subscription_filter(subscription_filter_t* filter) {
|
||||
}
|
||||
|
||||
// Validate subscription ID format and length
|
||||
static int validate_subscription_id(const char* sub_id) {
|
||||
int validate_subscription_id(const char* sub_id) {
|
||||
if (!sub_id) {
|
||||
return 0; // NULL pointer
|
||||
}
|
||||
@@ -135,11 +136,11 @@ static int validate_subscription_id(const char* sub_id) {
|
||||
return 0; // Empty or too long
|
||||
}
|
||||
|
||||
// Check for valid characters (alphanumeric, underscore, hyphen)
|
||||
// Check for valid characters (alphanumeric, underscore, hyphen, colon, comma)
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = sub_id[i];
|
||||
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-')) {
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == ':' || c == ',')) {
|
||||
return 0; // Invalid character
|
||||
}
|
||||
}
|
||||
@@ -150,19 +151,19 @@ static int validate_subscription_id(const char* sub_id) {
|
||||
// Create a new subscription
|
||||
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) {
|
||||
if (!sub_id || !wsi || !filters_array) {
|
||||
log_error("create_subscription: NULL parameter(s)");
|
||||
DEBUG_ERROR("create_subscription: NULL parameter(s)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Validate subscription ID
|
||||
if (!validate_subscription_id(sub_id)) {
|
||||
log_error("create_subscription: invalid subscription ID format or length");
|
||||
DEBUG_ERROR("create_subscription: invalid subscription ID format or length");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
subscription_t* sub = calloc(1, sizeof(subscription_t));
|
||||
if (!sub) {
|
||||
log_error("create_subscription: failed to allocate subscription");
|
||||
DEBUG_ERROR("create_subscription: failed to allocate subscription");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -199,7 +200,7 @@ subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON*
|
||||
cJSON* filter_json = NULL;
|
||||
cJSON_ArrayForEach(filter_json, filters_array) {
|
||||
if (filter_count >= MAX_FILTERS_PER_SUBSCRIPTION) {
|
||||
log_warning("Maximum filters per subscription exceeded, ignoring excess filters");
|
||||
DEBUG_WARN("Maximum filters per subscription exceeded, ignoring excess filters");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -218,7 +219,7 @@ subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON*
|
||||
}
|
||||
|
||||
if (filter_count == 0) {
|
||||
log_error("No valid filters found for subscription");
|
||||
DEBUG_ERROR("No valid filters found for subscription");
|
||||
free(sub);
|
||||
return NULL;
|
||||
}
|
||||
@@ -240,40 +241,94 @@ void free_subscription(subscription_t* sub) {
|
||||
// Add subscription to global manager (thread-safe)
|
||||
int add_subscription_to_manager(subscription_t* sub) {
|
||||
if (!sub) return -1;
|
||||
|
||||
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
// Check global limits
|
||||
if (g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) {
|
||||
|
||||
// Check for existing subscription with same ID and WebSocket connection
|
||||
// Remove it first to prevent duplicates (implements subscription replacement per NIP-01)
|
||||
subscription_t** current = &g_subscription_manager.active_subscriptions;
|
||||
int found_duplicate = 0;
|
||||
subscription_t* duplicate_old = NULL;
|
||||
|
||||
while (*current) {
|
||||
subscription_t* existing = *current;
|
||||
|
||||
// Match by subscription ID and WebSocket pointer
|
||||
if (strcmp(existing->id, sub->id) == 0 && existing->wsi == sub->wsi) {
|
||||
// Found duplicate: mark inactive and unlink from global list under lock
|
||||
existing->active = 0;
|
||||
*current = existing->next;
|
||||
g_subscription_manager.total_subscriptions--;
|
||||
found_duplicate = 1;
|
||||
duplicate_old = existing; // defer free until after per-session unlink
|
||||
break;
|
||||
}
|
||||
|
||||
current = &(existing->next);
|
||||
}
|
||||
|
||||
// Check global limits (only if not replacing an existing subscription)
|
||||
if (!found_duplicate && g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) {
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
log_error("Maximum total subscriptions reached");
|
||||
DEBUG_ERROR("Maximum total subscriptions reached");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// Add to global list
|
||||
sub->next = g_subscription_manager.active_subscriptions;
|
||||
g_subscription_manager.active_subscriptions = sub;
|
||||
g_subscription_manager.total_subscriptions++;
|
||||
g_subscription_manager.total_created++;
|
||||
|
||||
|
||||
// Only increment total_created if this is a new subscription (not a replacement)
|
||||
if (!found_duplicate) {
|
||||
g_subscription_manager.total_created++;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
// Log subscription creation to database
|
||||
|
||||
// If we replaced an existing subscription, unlink it from the per-session list before freeing
|
||||
if (duplicate_old) {
|
||||
// Obtain per-session data for this wsi
|
||||
struct per_session_data* pss = (struct per_session_data*) lws_wsi_user(duplicate_old->wsi);
|
||||
if (pss) {
|
||||
pthread_mutex_lock(&pss->session_lock);
|
||||
struct subscription** scur = &pss->subscriptions;
|
||||
while (*scur) {
|
||||
if (*scur == duplicate_old) {
|
||||
// Unlink by pointer identity to avoid removing the newly-added one
|
||||
*scur = duplicate_old->session_next;
|
||||
if (pss->subscription_count > 0) {
|
||||
pss->subscription_count--;
|
||||
}
|
||||
break;
|
||||
}
|
||||
scur = &((*scur)->session_next);
|
||||
}
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
}
|
||||
// Now safe to free the old subscription
|
||||
free_subscription(duplicate_old);
|
||||
}
|
||||
|
||||
// Log subscription creation to database (INSERT OR REPLACE handles duplicates)
|
||||
log_subscription_created(sub);
|
||||
|
||||
// Trigger monitoring update for subscription changes
|
||||
monitoring_on_subscription_change();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Remove subscription from global manager (thread-safe)
|
||||
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
|
||||
if (!sub_id) {
|
||||
log_error("remove_subscription_from_manager: NULL subscription ID");
|
||||
DEBUG_ERROR("remove_subscription_from_manager: NULL subscription ID");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate subscription ID format
|
||||
if (!validate_subscription_id(sub_id)) {
|
||||
log_error("remove_subscription_from_manager: invalid subscription ID format");
|
||||
DEBUG_ERROR("remove_subscription_from_manager: invalid subscription ID format");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -308,6 +363,9 @@ int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
|
||||
// Update events sent counter before freeing
|
||||
update_subscription_events_sent(sub_id_copy, events_sent_copy);
|
||||
|
||||
// Trigger monitoring update for subscription changes
|
||||
monitoring_on_subscription_change();
|
||||
|
||||
free_subscription(sub);
|
||||
return 0;
|
||||
}
|
||||
@@ -319,7 +377,7 @@ int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
|
||||
|
||||
char debug_msg[256];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id);
|
||||
log_warning(debug_msg);
|
||||
DEBUG_WARN(debug_msg);
|
||||
|
||||
return -1;
|
||||
}
|
||||
@@ -330,30 +388,48 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Debug: Log event details being tested
|
||||
cJSON* event_kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||
cJSON* event_created_at_obj = cJSON_GetObjectItem(event, "created_at");
|
||||
|
||||
DEBUG_TRACE("FILTER_MATCH: Testing event kind=%d id=%.8s created_at=%ld",
|
||||
event_kind_obj ? (int)cJSON_GetNumberValue(event_kind_obj) : -1,
|
||||
event_id_obj && cJSON_IsString(event_id_obj) ? cJSON_GetStringValue(event_id_obj) : "null",
|
||||
event_created_at_obj ? (long)cJSON_GetNumberValue(event_created_at_obj) : 0);
|
||||
|
||||
// Check kinds filter
|
||||
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
|
||||
DEBUG_TRACE("FILTER_MATCH: Checking kinds filter with %d kinds", cJSON_GetArraySize(filter->kinds));
|
||||
|
||||
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||
if (!event_kind || !cJSON_IsNumber(event_kind)) {
|
||||
DEBUG_WARN("FILTER_MATCH: Event has no valid kind field");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
|
||||
int kind_match = 0;
|
||||
DEBUG_TRACE("FILTER_MATCH: Event kind=%d", event_kind_val);
|
||||
|
||||
int kind_match = 0;
|
||||
cJSON* kind_item = NULL;
|
||||
cJSON_ArrayForEach(kind_item, filter->kinds) {
|
||||
if (cJSON_IsNumber(kind_item)) {
|
||||
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
|
||||
DEBUG_TRACE("FILTER_MATCH: Comparing event kind %d with filter kind %d", event_kind_val, filter_kind);
|
||||
if (filter_kind == event_kind_val) {
|
||||
kind_match = 1;
|
||||
DEBUG_TRACE("FILTER_MATCH: Kind matched!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!kind_match) {
|
||||
DEBUG_TRACE("FILTER_MATCH: No kind match, filter rejected");
|
||||
return 0;
|
||||
}
|
||||
DEBUG_TRACE("FILTER_MATCH: Kinds filter passed");
|
||||
}
|
||||
|
||||
// Check authors filter
|
||||
@@ -414,13 +490,19 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||
if (filter->since > 0) {
|
||||
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
|
||||
DEBUG_WARN("FILTER_MATCH: Event has no valid created_at field");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
|
||||
DEBUG_TRACE("FILTER_MATCH: Checking since filter: event_ts=%ld filter_since=%ld",
|
||||
event_timestamp, filter->since);
|
||||
|
||||
if (event_timestamp < filter->since) {
|
||||
DEBUG_TRACE("FILTER_MATCH: Event too old (before since), filter rejected");
|
||||
return 0;
|
||||
}
|
||||
DEBUG_TRACE("FILTER_MATCH: Since filter passed");
|
||||
}
|
||||
|
||||
// Check until filter
|
||||
@@ -502,6 +584,7 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_TRACE("FILTER_MATCH: All filters passed, event matches!");
|
||||
return 1; // All filters passed
|
||||
}
|
||||
|
||||
@@ -511,14 +594,23 @@ int event_matches_subscription(cJSON* event, subscription_t* subscription) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("SUB_MATCH: Testing subscription '%s'", subscription->id);
|
||||
|
||||
int filter_num = 0;
|
||||
subscription_filter_t* filter = subscription->filters;
|
||||
while (filter) {
|
||||
filter_num++;
|
||||
DEBUG_TRACE("SUB_MATCH: Testing filter #%d", filter_num);
|
||||
|
||||
if (event_matches_filter(event, filter)) {
|
||||
DEBUG_TRACE("SUB_MATCH: Filter #%d matched! Subscription '%s' matches",
|
||||
filter_num, subscription->id);
|
||||
return 1; // Match found (OR logic)
|
||||
}
|
||||
filter = filter->next;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("SUB_MATCH: No filters matched for subscription '%s'", subscription->id);
|
||||
return 0; // No filters matched
|
||||
}
|
||||
|
||||
@@ -529,10 +621,8 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
}
|
||||
|
||||
// Check if event is expired and should not be broadcast (NIP-40)
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
int expiration_enabled = g_unified_cache.expiration_config.enabled;
|
||||
int filter_responses = g_unified_cache.expiration_config.filter_responses;
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
int expiration_enabled = get_config_bool("expiration_enabled", 1);
|
||||
int filter_responses = get_config_bool("expiration_filter", 1);
|
||||
|
||||
if (expiration_enabled && filter_responses) {
|
||||
time_t current_time = time(NULL);
|
||||
@@ -542,7 +632,17 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
}
|
||||
|
||||
int broadcasts = 0;
|
||||
|
||||
|
||||
// Log event details
|
||||
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* event_id = cJSON_GetObjectItem(event, "id");
|
||||
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||
|
||||
DEBUG_TRACE("BROADCAST: Event kind=%d id=%.8s created_at=%ld",
|
||||
event_kind ? (int)cJSON_GetNumberValue(event_kind) : -1,
|
||||
event_id && cJSON_IsString(event_id) ? cJSON_GetStringValue(event_id) : "null",
|
||||
event_created_at ? (long)cJSON_GetNumberValue(event_created_at) : 0);
|
||||
|
||||
// Create a temporary list of matching subscriptions to avoid holding lock during I/O
|
||||
typedef struct temp_sub {
|
||||
struct lws* wsi;
|
||||
@@ -550,13 +650,21 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
char client_ip[CLIENT_IP_MAX_LENGTH];
|
||||
struct temp_sub* next;
|
||||
} temp_sub_t;
|
||||
|
||||
|
||||
temp_sub_t* matching_subs = NULL;
|
||||
int matching_count = 0;
|
||||
|
||||
// First pass: collect matching subscriptions while holding lock
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
int total_subs = 0;
|
||||
subscription_t* count_sub = g_subscription_manager.active_subscriptions;
|
||||
while (count_sub) {
|
||||
total_subs++;
|
||||
count_sub = count_sub->next;
|
||||
}
|
||||
DEBUG_TRACE("BROADCAST: Checking %d active subscriptions", total_subs);
|
||||
|
||||
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||
while (sub) {
|
||||
if (sub->active && sub->wsi && event_matches_subscription(event, sub)) {
|
||||
@@ -584,7 +692,7 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
matching_subs = temp;
|
||||
matching_count++;
|
||||
} else {
|
||||
log_error("broadcast_event_to_subscriptions: failed to allocate temp subscription");
|
||||
DEBUG_ERROR("broadcast_event_to_subscriptions: failed to allocate temp subscription");
|
||||
}
|
||||
}
|
||||
sub = sub->next;
|
||||
@@ -608,30 +716,41 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
if (buf) {
|
||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
||||
|
||||
// Send to WebSocket connection with error checking
|
||||
// Note: lws_write can fail if connection is closed, but won't crash
|
||||
int write_result = lws_write(current_temp->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
||||
if (write_result >= 0) {
|
||||
// DEBUG: Log WebSocket frame details before sending
|
||||
DEBUG_TRACE("WS_FRAME_SEND: type=EVENT sub=%s len=%zu data=%.100s%s",
|
||||
current_temp->id,
|
||||
msg_len,
|
||||
msg_str,
|
||||
msg_len > 100 ? "..." : "");
|
||||
|
||||
// Queue message for proper libwebsockets pattern
|
||||
struct per_session_data* pss = (struct per_session_data*)lws_wsi_user(current_temp->wsi);
|
||||
if (queue_message(current_temp->wsi, pss, msg_str, msg_len, LWS_WRITE_TEXT) == 0) {
|
||||
// Message queued successfully
|
||||
broadcasts++;
|
||||
|
||||
|
||||
// Update events sent counter for this subscription
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
subscription_t* update_sub = g_subscription_manager.active_subscriptions;
|
||||
while (update_sub) {
|
||||
if (update_sub->wsi == current_temp->wsi &&
|
||||
strcmp(update_sub->id, current_temp->id) == 0) {
|
||||
strcmp(update_sub->id, current_temp->id) == 0 &&
|
||||
update_sub->active) { // Add active check to prevent use-after-free
|
||||
update_sub->events_sent++;
|
||||
break;
|
||||
}
|
||||
update_sub = update_sub->next;
|
||||
}
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
|
||||
// Log event broadcast to database (optional - can be disabled for performance)
|
||||
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||
if (event_id_obj && cJSON_IsString(event_id_obj)) {
|
||||
log_event_broadcast(cJSON_GetStringValue(event_id_obj), current_temp->id, current_temp->client_ip);
|
||||
}
|
||||
// NOTE: event_broadcasts table removed due to FOREIGN KEY constraint issues
|
||||
// cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||
// if (event_id_obj && cJSON_IsString(event_id_obj)) {
|
||||
// log_event_broadcast(cJSON_GetStringValue(event_id_obj), current_temp->id, current_temp->client_ip);
|
||||
// }
|
||||
} else {
|
||||
DEBUG_ERROR("Failed to queue EVENT message for sub=%s", current_temp->id);
|
||||
}
|
||||
|
||||
free(buf);
|
||||
@@ -655,9 +774,42 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
g_subscription_manager.total_events_broadcast += broadcasts;
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
DEBUG_LOG("Event broadcast complete: %d subscriptions matched", broadcasts);
|
||||
return broadcasts;
|
||||
}
|
||||
|
||||
// Check if any active subscription exists for a specific event kind (thread-safe)
|
||||
int has_subscriptions_for_kind(int event_kind) {
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||
while (sub) {
|
||||
if (sub->active && sub->filters) {
|
||||
subscription_filter_t* filter = sub->filters;
|
||||
while (filter) {
|
||||
// Check if this filter includes our event kind
|
||||
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
|
||||
cJSON* kind_item = NULL;
|
||||
cJSON_ArrayForEach(kind_item, filter->kinds) {
|
||||
if (cJSON_IsNumber(kind_item)) {
|
||||
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
|
||||
if (filter_kind == event_kind) {
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
return 1; // Found matching subscription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
filter = filter->next;
|
||||
}
|
||||
}
|
||||
sub = sub->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
return 0; // No matching subscriptions
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -669,6 +821,10 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
void log_subscription_created(const subscription_t* sub) {
|
||||
if (!g_db || !sub) return;
|
||||
|
||||
// Convert wsi pointer to string
|
||||
char wsi_str[32];
|
||||
snprintf(wsi_str, sizeof(wsi_str), "%p", (void*)sub->wsi);
|
||||
|
||||
// Create filter JSON for logging
|
||||
char* filter_json = NULL;
|
||||
if (sub->filters) {
|
||||
@@ -715,16 +871,18 @@ void log_subscription_created(const subscription_t* sub) {
|
||||
cJSON_Delete(filters_array);
|
||||
}
|
||||
|
||||
// Use INSERT OR REPLACE to handle duplicates automatically
|
||||
const char* sql =
|
||||
"INSERT INTO subscription_events (subscription_id, client_ip, event_type, filter_json) "
|
||||
"VALUES (?, ?, 'created', ?)";
|
||||
"INSERT OR REPLACE INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type, filter_json) "
|
||||
"VALUES (?, ?, ?, 'created', ?)";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, sub->id, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, sub->client_ip, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, wsi_str, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, sub->client_ip, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 4, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT);
|
||||
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -739,8 +897,8 @@ void log_subscription_closed(const char* sub_id, const char* client_ip, const ch
|
||||
if (!g_db || !sub_id) return;
|
||||
|
||||
const char* sql =
|
||||
"INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
|
||||
"VALUES (?, ?, 'closed')";
|
||||
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
||||
"VALUES (?, '', ?, 'closed')";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
@@ -754,7 +912,7 @@ void log_subscription_closed(const char* sub_id, const char* client_ip, const ch
|
||||
|
||||
// Update the corresponding 'created' entry with end time and events sent
|
||||
const char* update_sql =
|
||||
"UPDATE subscription_events "
|
||||
"UPDATE subscriptions "
|
||||
"SET ended_at = strftime('%s', 'now') "
|
||||
"WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
|
||||
|
||||
@@ -772,7 +930,7 @@ void log_subscription_disconnected(const char* client_ip) {
|
||||
|
||||
// Mark all active subscriptions for this client as disconnected
|
||||
const char* sql =
|
||||
"UPDATE subscription_events "
|
||||
"UPDATE subscriptions "
|
||||
"SET ended_at = strftime('%s', 'now') "
|
||||
"WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
|
||||
|
||||
@@ -787,8 +945,8 @@ void log_subscription_disconnected(const char* client_ip) {
|
||||
if (changes > 0) {
|
||||
// Log a disconnection event
|
||||
const char* insert_sql =
|
||||
"INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
|
||||
"VALUES ('disconnect', ?, 'disconnected')";
|
||||
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
||||
"VALUES ('disconnect', '', ?, 'disconnected')";
|
||||
|
||||
rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
@@ -801,31 +959,32 @@ void log_subscription_disconnected(const char* client_ip) {
|
||||
}
|
||||
|
||||
// Log event broadcast to database (optional, can be resource intensive)
|
||||
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip) {
|
||||
if (!g_db || !event_id || !sub_id || !client_ip) return;
|
||||
|
||||
const char* sql =
|
||||
"INSERT INTO event_broadcasts (event_id, subscription_id, client_ip) "
|
||||
"VALUES (?, ?, ?)";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_STATIC);
|
||||
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
// REMOVED: event_broadcasts table removed due to FOREIGN KEY constraint issues
|
||||
// void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip) {
|
||||
// if (!g_db || !event_id || !sub_id || !client_ip) return;
|
||||
//
|
||||
// const char* sql =
|
||||
// "INSERT INTO event_broadcasts (event_id, subscription_id, client_ip) "
|
||||
// "VALUES (?, ?, ?)";
|
||||
//
|
||||
// sqlite3_stmt* stmt;
|
||||
// int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
// if (rc == SQLITE_OK) {
|
||||
// sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
|
||||
// sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
|
||||
// sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_STATIC);
|
||||
//
|
||||
// sqlite3_step(stmt);
|
||||
// sqlite3_finalize(stmt);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Update events sent counter for a subscription
|
||||
void update_subscription_events_sent(const char* sub_id, int events_sent) {
|
||||
if (!g_db || !sub_id) return;
|
||||
|
||||
const char* sql =
|
||||
"UPDATE subscription_events "
|
||||
"UPDATE subscriptions "
|
||||
"SET events_sent = ? "
|
||||
"WHERE subscription_id = ? AND event_type = 'created'";
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ struct subscription_manager {
|
||||
};
|
||||
|
||||
// Function declarations
|
||||
int validate_subscription_id(const char* sub_id);
|
||||
subscription_filter_t* create_subscription_filter(cJSON* filter_json);
|
||||
void free_subscription_filter(subscription_filter_t* filter);
|
||||
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip);
|
||||
@@ -114,7 +115,9 @@ int get_active_connections_for_ip(const char* client_ip);
|
||||
void log_subscription_created(const subscription_t* sub);
|
||||
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason);
|
||||
void log_subscription_disconnected(const char* client_ip);
|
||||
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip);
|
||||
void update_subscription_events_sent(const char* sub_id, int events_sent);
|
||||
|
||||
// Subscription query functions
|
||||
int has_subscriptions_for_kind(int event_kind);
|
||||
|
||||
#endif // SUBSCRIPTIONS_H
|
||||
675
src/websockets.c
675
src/websockets.c
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,14 @@
|
||||
#define MAX_SEARCH_LENGTH 256
|
||||
#define MAX_TAG_VALUE_LENGTH 1024
|
||||
|
||||
// Message queue node for proper libwebsockets pattern
|
||||
struct message_queue_node {
|
||||
unsigned char* data; // Message data (with LWS_PRE space)
|
||||
size_t length; // Message length (without LWS_PRE)
|
||||
enum lws_write_protocol type; // LWS_WRITE_TEXT, etc.
|
||||
struct message_queue_node* next; // Next node in queue
|
||||
};
|
||||
|
||||
// Enhanced per-session data with subscription management, NIP-42 authentication, and rate limiting
|
||||
struct per_session_data {
|
||||
int authenticated;
|
||||
@@ -38,6 +46,7 @@ struct per_session_data {
|
||||
pthread_mutex_t session_lock; // Per-session thread safety
|
||||
char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP for logging
|
||||
int subscription_count; // Number of subscriptions for this session
|
||||
time_t connection_established; // When WebSocket connection was established
|
||||
|
||||
// NIP-42 Authentication State
|
||||
char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
|
||||
@@ -58,6 +67,12 @@ struct per_session_data {
|
||||
int malformed_request_count; // Count of malformed requests in current hour
|
||||
time_t malformed_request_window_start; // Start of current hour window
|
||||
time_t malformed_request_blocked_until; // Time until blocked for malformed requests
|
||||
|
||||
// Message queue for proper libwebsockets pattern (replaces single buffer)
|
||||
struct message_queue_node* message_queue_head; // Head of message queue
|
||||
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
|
||||
};
|
||||
|
||||
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||
@@ -72,6 +87,10 @@ struct nip11_session_data {
|
||||
// Function declarations
|
||||
int start_websocket_relay(int port_override, int strict_port);
|
||||
|
||||
// Message queue functions for proper libwebsockets pattern
|
||||
int queue_message(struct lws* wsi, struct per_session_data* pss, const char* message, size_t length, enum lws_write_protocol type);
|
||||
int process_message_queue(struct lws* wsi, struct per_session_data* pss);
|
||||
|
||||
// Auth rules checking function from request_validator.c
|
||||
int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash);
|
||||
|
||||
|
||||
40
systemd/c-relay-local.service
Normal file
40
systemd/c-relay-local.service
Normal file
@@ -0,0 +1,40 @@
|
||||
[Unit]
|
||||
Description=C Nostr Relay Server (Local Development)
|
||||
Documentation=https://github.com/your-repo/c-relay
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=teknari
|
||||
WorkingDirectory=/home/teknari/Storage/c_relay
|
||||
Environment=DEBUG_LEVEL=0
|
||||
ExecStart=/home/teknari/Storage/c_relay/crelay --port 7777 --debug-level=$DEBUG_LEVEL
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=c-relay-local
|
||||
|
||||
# Security settings (relaxed for local development)
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/home/teknari/Storage/c_relay
|
||||
PrivateTmp=true
|
||||
|
||||
# Network security
|
||||
PrivateNetwork=false
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65536
|
||||
LimitNPROC=4096
|
||||
|
||||
# Event-based configuration system
|
||||
# No environment variables needed - all configuration is stored as Nostr events
|
||||
# Database files (<relay_pubkey>.db) are created automatically in WorkingDirectory
|
||||
# Admin keys are generated and displayed only during first startup
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -9,7 +9,8 @@ Type=simple
|
||||
User=c-relay
|
||||
Group=c-relay
|
||||
WorkingDirectory=/opt/c-relay
|
||||
ExecStart=/opt/c-relay/c_relay_x86
|
||||
Environment=DEBUG_LEVEL=0
|
||||
ExecStart=/opt/c-relay/c_relay_x86 --debug-level=$DEBUG_LEVEL
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
2025-10-11 10:56:27 - ==========================================
|
||||
2025-10-11 10:56:27 - C-Relay Comprehensive Test Suite Runner
|
||||
2025-10-11 10:56:27 - ==========================================
|
||||
2025-10-11 10:56:27 - Relay URL: ws://127.0.0.1:8888
|
||||
2025-10-11 10:56:27 - Log file: test_results_20251011_105627.log
|
||||
2025-10-11 10:56:27 - Report file: test_report_20251011_105627.html
|
||||
2025-10-11 10:56:27 -
|
||||
2025-10-11 10:56:27 - Checking relay status at ws://127.0.0.1:8888...
|
||||
2025-10-11 10:56:27 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
|
||||
2025-10-11 10:56:27 -
|
||||
2025-10-11 10:56:27 - Starting comprehensive test execution...
|
||||
2025-10-11 10:56:27 -
|
||||
2025-10-11 10:56:27 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
|
||||
2025-10-11 10:56:27 - ==========================================
|
||||
2025-10-11 10:56:27 - Running Test Suite: SQL Injection Tests
|
||||
2025-10-11 10:56:27 - Description: Comprehensive SQL injection vulnerability testing
|
||||
2025-10-11 10:56:27 - ==========================================
|
||||
==========================================
|
||||
C-Relay SQL Injection Test Suite
|
||||
==========================================
|
||||
Testing against relay at ws://127.0.0.1:8888
|
||||
|
||||
=== Basic Connectivity Test ===
|
||||
Testing Basic connectivity... [0;32mPASSED[0m - Valid query works
|
||||
|
||||
=== Authors Filter SQL Injection Tests ===
|
||||
Testing Authors filter with payload: '; DROP TABLE events; --... [1;33mUNCERTAIN[0m - Connection timeout (may indicate crash)
|
||||
2025-10-11 10:56:32 - \033[0;31m✗ SQL Injection Tests FAILED\033[0m (Duration: 5s)
|
||||
@@ -28,7 +28,7 @@ echo "✓ nak command found"
|
||||
|
||||
# Check if relay is running by testing connection
|
||||
echo "Testing relay connection..."
|
||||
if ! timeout 5 bash -c "</dev/tcp/localhost/8888" 2>/dev/null; then
|
||||
if ! timeout 5 nc -z localhost 8888 2>/dev/null; then
|
||||
echo "ERROR: Relay does not appear to be running on localhost:8888"
|
||||
echo "Please start the relay first with: ./make_and_restart_relay.sh"
|
||||
exit 1
|
||||
|
||||
@@ -32,9 +32,7 @@ test_auth_challenge() {
|
||||
|
||||
# Send a REQ message that should trigger auth
|
||||
local response
|
||||
response=$(timeout 10 bash -c "
|
||||
echo '[\"REQ\",\"auth_test_'$(date +%s)'\",{}]' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -3
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
response=$(echo "[\"REQ\",\"auth_test_$(date +%s)\",{}]" | timeout 10 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -3 || echo 'TIMEOUT')
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Connection timeout"
|
||||
@@ -80,9 +78,7 @@ EOF
|
||||
|
||||
# Send auth list modification
|
||||
local response
|
||||
response=$(timeout 10 bash -c "
|
||||
echo '$admin_event' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
response=$(echo "$admin_event" | timeout 10 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'TIMEOUT')
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Connection timeout"
|
||||
|
||||
@@ -47,9 +47,7 @@ EOF
|
||||
|
||||
# Send config query event
|
||||
local response
|
||||
response=$(timeout 10 bash -c "
|
||||
echo '$admin_event' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -3
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
response=$(echo "$admin_event" | timeout 10 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -3 || echo 'TIMEOUT')
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Connection timeout"
|
||||
@@ -94,9 +92,7 @@ EOF
|
||||
|
||||
# Send config setting event
|
||||
local response
|
||||
response=$(timeout 10 bash -c "
|
||||
echo '$admin_event' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -3
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
response=$(echo "$admin_event" | timeout 10 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -3 || echo 'TIMEOUT')
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Connection timeout"
|
||||
|
||||
12
tests/debug.log
Normal file
12
tests/debug.log
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
=== NOSTR WebSocket Debug Log Started ===
|
||||
[14:13:42.079] SEND localhost:8888: ["EVENT", {
|
||||
"pubkey": "e74e808f64b82fe4671b92cdf83f6dd5f5f44dbcb67fbd0e044f34a6193e0994",
|
||||
"created_at": 1761499244,
|
||||
"kind": 1059,
|
||||
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
|
||||
"content": "ApTb8y2oD3/TtVCV73Szhgfh5ODlluGd5zjsH44g5BBwaGB1NshOJ/5kF/XN0TfYJKQBe07UTpnOYMZ4l2ppU6SrR8Tor+ZEiAF/kpCpa/x6LDDIvf4mueQicDKjOf8Y6oEbsxYjtFrpuSC0LLMgLaVhcZjAgVD0YQTo+8nHOzHZD5RBr305vdnrxIe4ubEficAHCpnKq9L3A46AIyb+aHjjTbSYmB061cf6hzLSnmdh5xeACExjhxwsX9ivSvqGYcDNsH1JCM8EYQyRX9xAPDBYM1yuS8PpadqMluOcqOd/FFYyjYNpFrardblPsjUzZTz/TDSLyrYFDUKNa7pWIhW1asc1ZaY0ry0AoWnbl/QyMxqBjDFXd3mJfWccYsOI/Yrx3sxbZdL+ayRlQeQuDk/M9rQkH8GN/5+GE1aN5I6eVl0F37Axc/lLuIt/AIpoTwZYAEi9j/BYGLP6sYkjUp0foz91QximOTgu8evynu+nfAv330HVkipTIGOjEZea7QNSK0Fylxs8fanHlmiqWGyfyBeoWpxGslHZVu6K9k7GC8ABEIdNRa8vlqlphPfWPCS70Lnq3LgeKOj1C3sNF9ST8g7pth/0FEZgXruzhpx/EyjsasNbdLZg3iX1QwRS0P4L341Flrztovt8npyP9ytTiukkYIQzXCX8XuWjiaUuzXiLkVazjh0Nl03ikKKu2+7nuaBB92geBjbGT76zZ6HeXBgcmC7dWn7pHhzqu+QTonZK0oCl427Fs0eXiYsILjxFFQkmk7OHXgdZF9jquNXloz5lgwY9S3xj4JyRwLN/9xfh16awxLZNEFvX10X97bXsmNMRUDrJJPkKMTSxZpvuTbd+Lx2iB++4NyGZibNa6nOWOJG9d2LwEzIcIHS0uQpEIPl7Ccz6+rmkVh9kLbB2rda2fYp9GCOcn6XbfaXZZXJM+HAQwPJgrtDiuQex0tEIcQcB9CYCN4ze9HCt1kb23TUgEDAipz/RqYP4dOCYmRZ7vaYk/irJ+iRDfnvPK0Id1TrSeo5kaVc7py2zWZRVdndpTM8RvW0SLwdldXDIv+ym/mS0L7bchoaYjoNeuTNKQ6AOoc0E7f4ySr65FUKYd2FTvIsP2Avsa3S+D0za30ensxr733l80AQlVmUPrhsgOzzjEuOW1hGlGus38X+CDDEuMSJnq3hvz/CxVtAk71Zkbyr5lc1BPi758Y4rlZFQnhaKYKv5nSFJc7GtDykv+1cwxNGC6AxGKprnYMDVxuAIFYBztFitdO5BsjWvvKzAbleszewtGfjE2NgltIJk+gQlTpWvLNxd3gvb+qHarfEv7BPnPfsKktDpEfuNMKXdJPANyACq5gXj854o/X8iO2iLm7JSdMhEQgIIyHNyLCCQdLDnqDWIfcdyIzAfRilSCwImt3CVJBGD7HoXRbwGRR3vgEBcoVPmsYzaU9vr62I=",
|
||||
"id": "75c178ee47aac3ab9e984ddb85bdf9d8c68ade0d97e9cd86bb39e3110218a589",
|
||||
"sig": "aba8382cc8d6ba6bba467109d2ddc19718732fe803d71e73fd2db62c1cbbb1b4527447240906e01755139067a71c75d8c03271826ca5d0226c818cb7fb495fe2"
|
||||
}]
|
||||
[14:13:42.083] RECV localhost:8888: ["OK", "75c178ee47aac3ab9e984ddb85bdf9d8c68ade0d97e9cd86bb39e3110218a589", true, ""]
|
||||
48
tests/debug_perf.sh
Executable file
48
tests/debug_perf.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Debug script for performance_benchmarks.sh
|
||||
|
||||
source ./performance_benchmarks.sh
|
||||
|
||||
echo "Testing benchmark_request function..."
|
||||
result=$(benchmark_request '["REQ","test",{}]')
|
||||
echo "Result: $result"
|
||||
|
||||
echo "Testing full client subprocess..."
|
||||
(
|
||||
client_start=$(date +%s)
|
||||
client_requests=0
|
||||
client_total_response_time=0
|
||||
client_successful_requests=0
|
||||
client_min_time=999999
|
||||
client_max_time=0
|
||||
|
||||
while [[ $(($(date +%s) - client_start)) -lt 3 ]]; do
|
||||
result=$(benchmark_request '["REQ","test",{}]')
|
||||
IFS=':' read -r response_time success <<< "$result"
|
||||
|
||||
client_total_response_time=$((client_total_response_time + response_time))
|
||||
client_requests=$((client_requests + 1))
|
||||
|
||||
if [[ "$success" == "1" ]]; then
|
||||
client_successful_requests=$((client_successful_requests + 1))
|
||||
fi
|
||||
|
||||
if [[ $response_time -lt client_min_time ]]; then
|
||||
client_min_time=$response_time
|
||||
fi
|
||||
|
||||
if [[ $response_time -gt client_max_time ]]; then
|
||||
client_max_time=$response_time
|
||||
fi
|
||||
|
||||
echo "Request $client_requests: ${response_time}ms, success=$success"
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
echo "$client_requests:$client_successful_requests:$client_total_response_time:$client_min_time:$client_max_time"
|
||||
) &
|
||||
pid=$!
|
||||
echo "Waiting for client..."
|
||||
wait "$pid"
|
||||
echo "Client finished."
|
||||
35
tests/ephemeral_test.sh
Executable file
35
tests/ephemeral_test.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Simplified Ephemeral Event Test
|
||||
# Tests that ephemeral events are broadcast to active subscriptions
|
||||
|
||||
echo "=== Generating Ephemeral Event (kind 20000) ==="
|
||||
event=$(nak event --kind 20000 --content "test ephemeral event")
|
||||
echo "$event"
|
||||
echo ""
|
||||
|
||||
echo "=== Testing Ephemeral Event Broadcast ==="
|
||||
subscription='["REQ","test_sub",{"kinds":[20000],"limit":10}]'
|
||||
echo "Subscription Filter:"
|
||||
echo "$subscription"
|
||||
echo ""
|
||||
|
||||
event_msg='["EVENT",'"$event"']'
|
||||
echo "Event Message:"
|
||||
echo "$event_msg"
|
||||
echo ""
|
||||
|
||||
echo "=== Relay Responses ==="
|
||||
(
|
||||
# Send subscription
|
||||
printf "%s\n" "$subscription"
|
||||
# Wait for subscription to establish
|
||||
sleep 1
|
||||
# Send ephemeral event on same connection
|
||||
printf "%s\n" "$event_msg"
|
||||
# Wait for responses
|
||||
sleep 2
|
||||
) | timeout 5 websocat ws://127.0.0.1:8888
|
||||
|
||||
echo ""
|
||||
echo "Test complete!"
|
||||
@@ -34,9 +34,7 @@ test_websocket_message() {
|
||||
|
||||
# Send message via websocat and capture response
|
||||
local response
|
||||
response=$(timeout $TEST_TIMEOUT bash -c "
|
||||
echo '$message' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null || echo 'CONNECTION_FAILED'
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
response=$(echo "$message" | timeout $TEST_TIMEOUT websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null || echo 'CONNECTION_FAILED')
|
||||
|
||||
if [[ "$response" == "CONNECTION_FAILED" ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Could not connect to relay"
|
||||
@@ -73,9 +71,7 @@ test_valid_message() {
|
||||
|
||||
# Send message via websocat and capture response
|
||||
local response
|
||||
response=$(timeout $TEST_TIMEOUT bash -c "
|
||||
echo '$message' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
response=$(echo "$message" | timeout $TEST_TIMEOUT websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'TIMEOUT')
|
||||
|
||||
if [[ "$response" == "TIMEOUT" ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Connection timeout"
|
||||
|
||||
File diff suppressed because one or more lines are too long
63
tests/large_event_test.sh
Executable file
63
tests/large_event_test.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for posting large events (>4KB) to test partial write handling
|
||||
# Uses nak to properly sign events with large content
|
||||
|
||||
RELAY_URL="ws://localhost:8888"
|
||||
|
||||
# Check if nak is installed
|
||||
if ! command -v nak &> /dev/null; then
|
||||
echo "Error: nak is not installed. Install with: go install github.com/fiatjaf/nak@latest"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate a test private key if not set
|
||||
if [ -z "$NOSTR_PRIVATE_KEY" ]; then
|
||||
echo "Generating temporary test key..."
|
||||
export NOSTR_PRIVATE_KEY=$(nak key generate)
|
||||
fi
|
||||
|
||||
echo "=== Large Event Test ==="
|
||||
echo "Testing partial write handling with events >4KB"
|
||||
echo "Relay: $RELAY_URL"
|
||||
echo ""
|
||||
|
||||
# Test 1: 5KB event
|
||||
echo "Test 1: Posting 5KB event..."
|
||||
CONTENT_5KB=$(python3 -c "print('A' * 5000)")
|
||||
echo "$CONTENT_5KB" | nak event -k 1 --content - $RELAY_URL
|
||||
sleep 1
|
||||
|
||||
# Test 2: 10KB event
|
||||
echo ""
|
||||
echo "Test 2: Posting 10KB event..."
|
||||
CONTENT_10KB=$(python3 -c "print('B' * 10000)")
|
||||
echo "$CONTENT_10KB" | nak event -k 1 --content - $RELAY_URL
|
||||
sleep 1
|
||||
|
||||
# Test 3: 20KB event
|
||||
echo ""
|
||||
echo "Test 3: Posting 20KB event..."
|
||||
CONTENT_20KB=$(python3 -c "print('C' * 20000)")
|
||||
echo "$CONTENT_20KB" | nak event -k 1 --content - $RELAY_URL
|
||||
sleep 1
|
||||
|
||||
# Test 4: 50KB event (very large)
|
||||
echo ""
|
||||
echo "Test 4: Posting 50KB event..."
|
||||
CONTENT_50KB=$(python3 -c "print('D' * 50000)")
|
||||
echo "$CONTENT_50KB" | nak event -k 1 --content - $RELAY_URL
|
||||
|
||||
echo ""
|
||||
echo "=== Test Complete ==="
|
||||
echo ""
|
||||
echo "Check relay.log for:"
|
||||
echo " - 'Queued partial write' messages (indicates buffering is working)"
|
||||
echo " - 'write completed' messages (indicates retry succeeded)"
|
||||
echo " - No 'Invalid frame header' errors"
|
||||
echo ""
|
||||
echo "To view logs in real-time:"
|
||||
echo " tail -f relay.log | grep -E '(partial|write completed|Invalid frame)'"
|
||||
echo ""
|
||||
echo "To check if events were stored:"
|
||||
echo " sqlite3 build/*.db 'SELECT id, length(content) as content_size FROM events ORDER BY created_at DESC LIMIT 4;'"
|
||||
@@ -50,20 +50,13 @@ run_client() {
|
||||
done
|
||||
# Send CLOSE message
|
||||
echo '["CLOSE","load_test_'"$client_id"'_*"]'
|
||||
) | timeout 60 websocat -B 1048576 "ws://$RELAY_HOST:$RELAY_PORT" > "$temp_file" 2>/dev/null &
|
||||
) | timeout 30 websocat -B 1048576 "ws://$RELAY_HOST:$RELAY_PORT" > "$temp_file" 2>/dev/null
|
||||
|
||||
local client_pid=$!
|
||||
local exit_code=$?
|
||||
|
||||
# Wait a bit for the client to complete
|
||||
sleep 2
|
||||
|
||||
# Check if client is still running (good sign)
|
||||
if kill -0 "$client_pid" 2>/dev/null; then
|
||||
# Check if connection was successful (exit code 0 means successful)
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
connection_successful=true
|
||||
((SUCCESSFUL_CONNECTIONS++))
|
||||
else
|
||||
wait "$client_pid" 2>/dev/null || true
|
||||
((FAILED_CONNECTIONS++))
|
||||
fi
|
||||
|
||||
# Count messages sent
|
||||
@@ -131,52 +124,61 @@ run_load_test() {
|
||||
TOTAL_MESSAGES_SENT=0
|
||||
TOTAL_MESSAGES_RECEIVED=0
|
||||
|
||||
# Start resource monitoring in background
|
||||
monitor_resources 30 &
|
||||
local monitor_pid=$!
|
||||
|
||||
# Launch clients
|
||||
local client_pids=()
|
||||
# Launch clients sequentially for now (simpler debugging)
|
||||
local client_results=()
|
||||
|
||||
echo "Launching $concurrent_clients concurrent clients..."
|
||||
echo "Launching $concurrent_clients clients..."
|
||||
|
||||
for i in $(seq 1 "$concurrent_clients"); do
|
||||
run_client "$i" "$messages_per_client" &
|
||||
client_pids+=($!)
|
||||
local result
|
||||
result=$(run_client "$i" "$messages_per_client")
|
||||
client_results+=("$result")
|
||||
TOTAL_CONNECTIONS=$((TOTAL_CONNECTIONS + 1))
|
||||
done
|
||||
|
||||
# Wait for all clients to complete
|
||||
echo "Waiting for clients to complete..."
|
||||
for pid in "${client_pids[@]}"; do
|
||||
wait "$pid" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Stop monitoring
|
||||
kill "$monitor_pid" 2>/dev/null || true
|
||||
wait "$monitor_pid" 2>/dev/null || true
|
||||
echo "All clients completed. Processing results..."
|
||||
|
||||
END_TIME=$(date +%s)
|
||||
local duration=$((END_TIME - START_TIME))
|
||||
|
||||
# Process client results
|
||||
local successful_connections=0
|
||||
local failed_connections=0
|
||||
local total_messages_sent=0
|
||||
local total_messages_received=0
|
||||
|
||||
for result in "${client_results[@]}"; do
|
||||
messages_sent=$(echo "$result" | cut -d: -f1)
|
||||
messages_received=$(echo "$result" | cut -d: -f2)
|
||||
connection_successful=$(echo "$result" | cut -d: -f3)
|
||||
if [[ "$connection_successful" == "true" ]]; then
|
||||
successful_connections=$((successful_connections + 1))
|
||||
else
|
||||
failed_connections=$((failed_connections + 1))
|
||||
fi
|
||||
total_messages_sent=$((total_messages_sent + messages_sent))
|
||||
total_messages_received=$((total_messages_received + messages_received))
|
||||
done
|
||||
|
||||
# Calculate metrics
|
||||
local total_messages_expected=$((concurrent_clients * messages_per_client))
|
||||
local connection_success_rate=0
|
||||
local total_connections=$((SUCCESSFUL_CONNECTIONS + FAILED_CONNECTIONS))
|
||||
|
||||
if [[ $total_connections -gt 0 ]]; then
|
||||
connection_success_rate=$((SUCCESSFUL_CONNECTIONS * 100 / total_connections))
|
||||
if [[ $TOTAL_CONNECTIONS -gt 0 ]]; then
|
||||
connection_success_rate=$((successful_connections * 100 / TOTAL_CONNECTIONS))
|
||||
fi
|
||||
|
||||
# Report results
|
||||
echo ""
|
||||
echo "=== Load Test Results ==="
|
||||
echo "Test duration: ${duration}s"
|
||||
echo "Total connections attempted: $total_connections"
|
||||
echo "Successful connections: $SUCCESSFUL_CONNECTIONS"
|
||||
echo "Failed connections: $FAILED_CONNECTIONS"
|
||||
echo "Total connections attempted: $TOTAL_CONNECTIONS"
|
||||
echo "Successful connections: $successful_connections"
|
||||
echo "Failed connections: $failed_connections"
|
||||
echo "Connection success rate: ${connection_success_rate}%"
|
||||
echo "Messages expected: $total_messages_expected"
|
||||
echo "Messages sent: $total_messages_sent"
|
||||
echo "Messages received: $total_messages_received"
|
||||
|
||||
# Performance assessment
|
||||
if [[ $connection_success_rate -ge 95 ]]; then
|
||||
@@ -190,9 +192,7 @@ run_load_test() {
|
||||
# Check if relay is still responsive
|
||||
echo ""
|
||||
echo -n "Checking relay responsiveness... "
|
||||
if timeout 5 bash -c "
|
||||
echo 'ping' | websocat -n1 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1
|
||||
" 2>/dev/null; then
|
||||
if echo 'ping' | timeout 5 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ Relay is still responsive${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Relay became unresponsive after load test${NC}"
|
||||
@@ -200,39 +200,40 @@ run_load_test() {
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "C-Relay Load Testing Suite"
|
||||
echo "=========================================="
|
||||
echo "Testing against relay at ws://$RELAY_HOST:$RELAY_PORT"
|
||||
echo ""
|
||||
# Only run main code if script is executed directly (not sourced)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
echo "=========================================="
|
||||
echo "C-Relay Load Testing Suite"
|
||||
echo "=========================================="
|
||||
echo "Testing against relay at ws://$RELAY_HOST:$RELAY_PORT"
|
||||
echo ""
|
||||
|
||||
# Test basic connectivity first
|
||||
echo "=== Basic Connectivity Test ==="
|
||||
if timeout 5 bash -c "
|
||||
echo 'ping' | websocat -n1 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1
|
||||
" 2>/dev/null; then
|
||||
echo -e "${GREEN}✓ Relay is accessible${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Cannot connect to relay. Aborting tests.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
# Test basic connectivity first
|
||||
echo "=== Basic Connectivity Test ==="
|
||||
if echo 'ping' | timeout 5 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ Relay is accessible${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Cannot connect to relay. Aborting tests.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Run different load scenarios
|
||||
run_load_test "Light Load Test" "Basic load test with moderate concurrent connections" 10 5
|
||||
echo ""
|
||||
# Run different load scenarios
|
||||
run_load_test "Light Load Test" "Basic load test with moderate concurrent connections" 10 5
|
||||
echo ""
|
||||
|
||||
run_load_test "Medium Load Test" "Moderate load test with higher concurrency" 25 10
|
||||
echo ""
|
||||
run_load_test "Medium Load Test" "Moderate load test with higher concurrency" 25 10
|
||||
echo ""
|
||||
|
||||
run_load_test "Heavy Load Test" "Heavy load test with high concurrency" 50 20
|
||||
echo ""
|
||||
run_load_test "Heavy Load Test" "Heavy load test with high concurrency" 50 20
|
||||
echo ""
|
||||
|
||||
run_load_test "Stress Test" "Maximum load test to find breaking point" 100 50
|
||||
echo ""
|
||||
run_load_test "Stress Test" "Maximum load test to find breaking point" 100 50
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "Load Testing Complete"
|
||||
echo "=========================================="
|
||||
echo "All load tests completed. Check individual test results above."
|
||||
echo "If any tests failed, the relay may need optimization or have resource limits."
|
||||
echo "=========================================="
|
||||
echo "Load Testing Complete"
|
||||
echo "=========================================="
|
||||
echo "All load tests completed. Check individual test results above."
|
||||
echo "If any tests failed, the relay may need optimization or have resource limits."
|
||||
fi
|
||||
@@ -35,16 +35,17 @@ test_memory_safety() {
|
||||
# Send message and monitor for crashes or memory issues
|
||||
local start_time=$(date +%s%N)
|
||||
local response
|
||||
response=$(timeout $TEST_TIMEOUT bash -c "
|
||||
echo '$message' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null
|
||||
" 2>/dev/null || echo 'CONNECTION_FAILED')
|
||||
response=$(echo "$message" | timeout $TEST_TIMEOUT websocat ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'CONNECTION_FAILED')
|
||||
local end_time=$(date +%s%N)
|
||||
|
||||
# Check if relay is still responsive after the test
|
||||
local relay_status
|
||||
relay_status=$(timeout 2 bash -c "
|
||||
echo 'ping' | websocat -n1 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1 && echo 'OK' || echo 'DOWN'
|
||||
" 2>/dev/null || echo 'DOWN')
|
||||
local ping_response=$(echo '["REQ","ping_test_'$RANDOM'",{}]' | timeout 2 websocat ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1)
|
||||
if [[ -n "$ping_response" ]]; then
|
||||
relay_status="OK"
|
||||
else
|
||||
relay_status="DOWN"
|
||||
fi
|
||||
|
||||
# Calculate response time (rough indicator of processing issues)
|
||||
local response_time=$(( (end_time - start_time) / 1000000 )) # Convert to milliseconds
|
||||
@@ -97,9 +98,7 @@ test_concurrent_access() {
|
||||
for i in $(seq 1 $concurrent_count); do
|
||||
(
|
||||
local response
|
||||
response=$(timeout $TEST_TIMEOUT bash -c "
|
||||
echo '$message' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1
|
||||
" 2>/dev/null || echo 'FAILED')
|
||||
response=$(echo "$message" | timeout $TEST_TIMEOUT websocat ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'FAILED')
|
||||
echo "$response"
|
||||
) &
|
||||
pids+=($!)
|
||||
@@ -113,9 +112,12 @@ test_concurrent_access() {
|
||||
|
||||
# Check if relay is still responsive
|
||||
local relay_status
|
||||
relay_status=$(timeout 2 bash -c "
|
||||
echo 'ping' | websocat -n1 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1 && echo 'OK' || echo 'DOWN'
|
||||
" 2>/dev/null || echo 'DOWN')
|
||||
local ping_response=$(echo '["REQ","ping_test_'$RANDOM'",{}]' | timeout 2 websocat ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1)
|
||||
if [[ -n "$ping_response" ]]; then
|
||||
relay_status="OK"
|
||||
else
|
||||
relay_status="DOWN"
|
||||
fi
|
||||
|
||||
if [[ "$relay_status" != "OK" ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Relay crashed during concurrent access"
|
||||
|
||||
@@ -31,32 +31,21 @@ benchmark_request() {
|
||||
local start_time
|
||||
local end_time
|
||||
local response_time
|
||||
local success=0
|
||||
|
||||
start_time=$(date +%s%N)
|
||||
local response
|
||||
response=$(timeout 5 bash -c "
|
||||
echo '$message' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
response=$(echo "$message" | timeout 5 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'TIMEOUT')
|
||||
end_time=$(date +%s%N)
|
||||
|
||||
response_time=$(( (end_time - start_time) / 1000000 )) # Convert to milliseconds
|
||||
|
||||
TOTAL_REQUESTS=$((TOTAL_REQUESTS + 1))
|
||||
TOTAL_RESPONSE_TIME=$((TOTAL_RESPONSE_TIME + response_time))
|
||||
|
||||
if [[ $response_time -lt MIN_RESPONSE_TIME ]]; then
|
||||
MIN_RESPONSE_TIME=$response_time
|
||||
fi
|
||||
|
||||
if [[ $response_time -gt MAX_RESPONSE_TIME ]]; then
|
||||
MAX_RESPONSE_TIME=$response_time
|
||||
fi
|
||||
|
||||
if [[ "$response" == *"EOSE"* ]] || [[ "$response" == *"EVENT"* ]] || [[ "$response" == *"OK"* ]]; then
|
||||
SUCCESSFUL_REQUESTS=$((SUCCESSFUL_REQUESTS + 1))
|
||||
else
|
||||
FAILED_REQUESTS=$((FAILED_REQUESTS + 1))
|
||||
success=1
|
||||
fi
|
||||
|
||||
# Return: response_time:success
|
||||
echo "$response_time:$success"
|
||||
}
|
||||
|
||||
# Function to run throughput benchmark
|
||||
@@ -84,62 +73,113 @@ run_throughput_benchmark() {
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
|
||||
# Launch concurrent clients
|
||||
# Launch concurrent clients and collect results
|
||||
local pids=()
|
||||
local client_results=()
|
||||
|
||||
for i in $(seq 1 "$concurrent_clients"); do
|
||||
(
|
||||
local client_start
|
||||
client_start=$(date +%s)
|
||||
local client_requests=0
|
||||
local client_total_response_time=0
|
||||
local client_successful_requests=0
|
||||
local client_min_time=999999
|
||||
local client_max_time=0
|
||||
|
||||
while [[ $(($(date +%s) - client_start)) -lt test_duration ]]; do
|
||||
benchmark_request "$message"
|
||||
((client_requests++))
|
||||
local result
|
||||
result=$(benchmark_request "$message")
|
||||
local response_time success
|
||||
IFS=':' read -r response_time success <<< "$result"
|
||||
|
||||
client_total_response_time=$((client_total_response_time + response_time))
|
||||
client_requests=$((client_requests + 1))
|
||||
|
||||
if [[ "$success" == "1" ]]; then
|
||||
client_successful_requests=$((client_successful_requests + 1))
|
||||
fi
|
||||
|
||||
if [[ $response_time -lt client_min_time ]]; then
|
||||
client_min_time=$response_time
|
||||
fi
|
||||
|
||||
if [[ $response_time -gt client_max_time ]]; then
|
||||
client_max_time=$response_time
|
||||
fi
|
||||
|
||||
# Small delay to prevent overwhelming
|
||||
sleep 0.01
|
||||
done
|
||||
|
||||
echo "client_${i}_requests:$client_requests"
|
||||
# Return client results: requests:successful:total_response_time:min_time:max_time
|
||||
echo "$client_requests:$client_successful_requests:$client_total_response_time:$client_min_time:$client_max_time"
|
||||
) &
|
||||
pids+=($!)
|
||||
done
|
||||
|
||||
# Wait for all clients to complete
|
||||
local client_results=()
|
||||
# Wait for all clients to complete and collect results
|
||||
for pid in "${pids[@]}"; do
|
||||
client_results+=("$(wait "$pid")")
|
||||
local result
|
||||
result=$(wait "$pid")
|
||||
client_results+=("$result")
|
||||
done
|
||||
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
local actual_duration=$((end_time - start_time))
|
||||
|
||||
# Process client results
|
||||
local total_requests=0
|
||||
local successful_requests=0
|
||||
local total_response_time=0
|
||||
local min_response_time=999999
|
||||
local max_response_time=0
|
||||
|
||||
for client_result in "${client_results[@]}"; do
|
||||
IFS=':' read -r client_requests client_successful client_total_time client_min_time client_max_time <<< "$client_result"
|
||||
|
||||
total_requests=$((total_requests + client_requests))
|
||||
successful_requests=$((successful_requests + client_successful))
|
||||
total_response_time=$((total_response_time + client_total_time))
|
||||
|
||||
if [[ $client_min_time -lt min_response_time ]]; then
|
||||
min_response_time=$client_min_time
|
||||
fi
|
||||
|
||||
if [[ $client_max_time -gt max_response_time ]]; then
|
||||
max_response_time=$client_max_time
|
||||
fi
|
||||
done
|
||||
|
||||
# Calculate metrics
|
||||
local avg_response_time="N/A"
|
||||
if [[ $SUCCESSFUL_REQUESTS -gt 0 ]]; then
|
||||
avg_response_time="$((TOTAL_RESPONSE_TIME / SUCCESSFUL_REQUESTS))ms"
|
||||
if [[ $successful_requests -gt 0 ]]; then
|
||||
avg_response_time="$((total_response_time / successful_requests))ms"
|
||||
fi
|
||||
|
||||
local requests_per_second="N/A"
|
||||
if [[ $actual_duration -gt 0 ]]; then
|
||||
requests_per_second="$((TOTAL_REQUESTS / actual_duration))"
|
||||
requests_per_second="$((total_requests / actual_duration))"
|
||||
fi
|
||||
|
||||
local success_rate="N/A"
|
||||
if [[ $TOTAL_REQUESTS -gt 0 ]]; then
|
||||
success_rate="$((SUCCESSFUL_REQUESTS * 100 / TOTAL_REQUESTS))%"
|
||||
if [[ $total_requests -gt 0 ]]; then
|
||||
success_rate="$((successful_requests * 100 / total_requests))%"
|
||||
fi
|
||||
|
||||
local failed_requests=$((total_requests - successful_requests))
|
||||
|
||||
# Report results
|
||||
echo "=== Benchmark Results ==="
|
||||
echo "Total requests: $TOTAL_REQUESTS"
|
||||
echo "Successful requests: $SUCCESSFUL_REQUESTS"
|
||||
echo "Failed requests: $FAILED_REQUESTS"
|
||||
echo "Total requests: $total_requests"
|
||||
echo "Successful requests: $successful_requests"
|
||||
echo "Failed requests: $failed_requests"
|
||||
echo "Success rate: $success_rate"
|
||||
echo "Requests per second: $requests_per_second"
|
||||
echo "Average response time: $avg_response_time"
|
||||
echo "Min response time: ${MIN_RESPONSE_TIME}ms"
|
||||
echo "Max response time: ${MAX_RESPONSE_TIME}ms"
|
||||
echo "Min response time: ${min_response_time}ms"
|
||||
echo "Max response time: ${max_response_time}ms"
|
||||
echo "Actual duration: ${actual_duration}s"
|
||||
echo ""
|
||||
|
||||
@@ -172,9 +212,7 @@ benchmark_memory_usage() {
|
||||
|
||||
# Create subscriptions
|
||||
for j in $(seq 1 "$i"); do
|
||||
timeout 2 bash -c "
|
||||
echo '[\"REQ\",\"mem_test_'${j}'\",{}]' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1
|
||||
" 2>/dev/null &
|
||||
echo "[\"REQ\",\"mem_test_${j}\",{}]" | timeout 2 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1 &
|
||||
done
|
||||
|
||||
sleep 2
|
||||
@@ -187,9 +225,7 @@ benchmark_memory_usage() {
|
||||
|
||||
# Clean up subscriptions
|
||||
for j in $(seq 1 "$i"); do
|
||||
timeout 2 bash -c "
|
||||
echo '[\"CLOSE\",\"mem_test_'${j}'\"]' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1
|
||||
" 2>/dev/null &
|
||||
echo "[\"CLOSE\",\"mem_test_${j}\"]" | timeout 2 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1 &
|
||||
done
|
||||
|
||||
sleep 1
|
||||
@@ -200,40 +236,44 @@ benchmark_memory_usage() {
|
||||
echo "Final memory usage: ${final_memory}KB"
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "C-Relay Performance Benchmarking Suite"
|
||||
echo "=========================================="
|
||||
echo "Benchmarking relay at ws://$RELAY_HOST:$RELAY_PORT"
|
||||
echo ""
|
||||
# Only run main code if script is executed directly (not sourced)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
echo "=========================================="
|
||||
echo "C-Relay Performance Benchmarking Suite"
|
||||
echo "=========================================="
|
||||
echo "Benchmarking relay at ws://$RELAY_HOST:$RELAY_PORT"
|
||||
echo ""
|
||||
|
||||
# Test basic connectivity
|
||||
echo "=== Connectivity Test ==="
|
||||
benchmark_request '["REQ","bench_test",{}]'
|
||||
if [[ $SUCCESSFUL_REQUESTS -eq 0 ]]; then
|
||||
echo -e "${RED}Cannot connect to relay. Aborting benchmarks.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Relay is accessible${NC}"
|
||||
echo ""
|
||||
# Test basic connectivity
|
||||
echo "=== Connectivity Test ==="
|
||||
connectivity_result=$(benchmark_request '["REQ","bench_test",{}]')
|
||||
IFS=':' read -r response_time success <<< "$connectivity_result"
|
||||
if [[ "$success" != "1" ]]; then
|
||||
echo -e "${RED}Cannot connect to relay. Aborting benchmarks.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Relay is accessible${NC}"
|
||||
echo ""
|
||||
|
||||
# Run throughput benchmarks
|
||||
run_throughput_benchmark "Simple REQ Throughput" '["REQ","throughput_'$(date +%s%N)'",{}]' 10 15
|
||||
echo ""
|
||||
# Run throughput benchmarks
|
||||
run_throughput_benchmark "Simple REQ Throughput" '["REQ","throughput_'$(date +%s%N)'",{}]' 10 15
|
||||
echo ""
|
||||
|
||||
run_throughput_benchmark "Complex Filter Throughput" '["REQ","complex_'$(date +%s%N)'",{"kinds":[1,2,3],"#e":["test"],"limit":10}]' 10 15
|
||||
echo ""
|
||||
run_throughput_benchmark "Complex Filter Throughput" '["REQ","complex_'$(date +%s%N)'",{"kinds":[1,2,3],"#e":["test"],"limit":10}]' 10 15
|
||||
echo ""
|
||||
|
||||
run_throughput_benchmark "COUNT Message Throughput" '["COUNT","count_'$(date +%s%N)'",{}]' 10 15
|
||||
echo ""
|
||||
run_throughput_benchmark "COUNT Message Throughput" '["REQ","count_'$(date +%s%N)'",{}]' 10 15
|
||||
echo ""
|
||||
|
||||
run_throughput_benchmark "High Load Throughput" '["REQ","high_load_'$(date +%s%N)'",{}]' 25 20
|
||||
echo ""
|
||||
run_throughput_benchmark "High Load Throughput" '["REQ","high_load_'$(date +%s%N)'",{}]' 25 20
|
||||
echo ""
|
||||
|
||||
# Memory usage benchmark
|
||||
benchmark_memory_usage
|
||||
echo ""
|
||||
# Memory usage benchmark
|
||||
benchmark_memory_usage
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "Benchmarking Complete"
|
||||
echo "=========================================="
|
||||
echo "Performance benchmarks completed. Review results above for optimization opportunities."
|
||||
echo "=========================================="
|
||||
echo "Benchmarking Complete"
|
||||
echo "=========================================="
|
||||
echo "Performance benchmarks completed. Review results above for optimization opportunities."
|
||||
fi
|
||||
53
tests/post_events.sh
Executable file
53
tests/post_events.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script to post kind 1 events to the relay every second
|
||||
# Cycles through three different secret keys
|
||||
# Content includes current timestamp
|
||||
#
|
||||
# Usage: ./post_events.sh <relay_url>
|
||||
# Example: ./post_events.sh ws://localhost:8888
|
||||
# Example: ./post_events.sh wss://relay.laantungir.net
|
||||
|
||||
# Check if relay URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "Error: Relay URL is required"
|
||||
echo "Usage: $0 <relay_url>"
|
||||
echo "Example: $0 ws://localhost:8888"
|
||||
echo "Example: $0 wss://relay.laantungir.net"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Array of secret keys to cycle through
|
||||
SECRET_KEYS=(
|
||||
"3fdd8227a920c2385559400b2b14e464f22e80df312a73cc7a86e1d7e91d608f"
|
||||
"a156011cd65b71f84b4a488ac81687f2aed57e490b31c28f58195d787030db60"
|
||||
"1618aaa21f5bd45c5ffede0d9a60556db67d4a046900e5f66b0bae5c01c801fb"
|
||||
)
|
||||
|
||||
RELAY_URL="$1"
|
||||
KEY_INDEX=0
|
||||
|
||||
echo "Starting event posting test to $RELAY_URL"
|
||||
echo "Press Ctrl+C to stop"
|
||||
|
||||
while true; do
|
||||
# Get current timestamp
|
||||
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
# Get current secret key
|
||||
CURRENT_KEY=${SECRET_KEYS[$KEY_INDEX]}
|
||||
|
||||
# Create content with timestamp
|
||||
CONTENT="Test event at $TIMESTAMP"
|
||||
|
||||
echo "[$TIMESTAMP] Posting event with key ${KEY_INDEX}: ${CURRENT_KEY:0:16}..."
|
||||
|
||||
# Post event using nak
|
||||
nak event -c "$CONTENT" --sec "$CURRENT_KEY" "$RELAY_URL"
|
||||
|
||||
# Cycle to next key
|
||||
KEY_INDEX=$(( (KEY_INDEX + 1) % ${#SECRET_KEYS[@]} ))
|
||||
|
||||
# Wait 1 second
|
||||
sleep .2
|
||||
done
|
||||
@@ -1,213 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Rate Limiting Test Suite for C-Relay
|
||||
# Tests rate limiting and abuse prevention mechanisms
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
RELAY_HOST="127.0.0.1"
|
||||
RELAY_PORT="8888"
|
||||
TEST_TIMEOUT=15
|
||||
|
||||
# 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
|
||||
|
||||
# Function to test rate limiting
|
||||
test_rate_limiting() {
|
||||
local description="$1"
|
||||
local message="$2"
|
||||
local burst_count="${3:-10}"
|
||||
local expected_limited="${4:-false}"
|
||||
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
|
||||
echo -n "Testing $description... "
|
||||
|
||||
local rate_limited=false
|
||||
local success_count=0
|
||||
local error_count=0
|
||||
|
||||
# Send burst of messages
|
||||
for i in $(seq 1 "$burst_count"); do
|
||||
local response
|
||||
response=$(timeout 2 bash -c "
|
||||
echo '$message' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
|
||||
if [[ "$response" == *"rate limit"* ]] || [[ "$response" == *"too many"* ]] || [[ "$response" == *"TOO_MANY"* ]]; then
|
||||
rate_limited=true
|
||||
elif [[ "$response" == *"EOSE"* ]] || [[ "$response" == *"EVENT"* ]] || [[ "$response" == *"OK"* ]]; then
|
||||
((success_count++))
|
||||
else
|
||||
((error_count++))
|
||||
fi
|
||||
|
||||
# Small delay between requests
|
||||
sleep 0.05
|
||||
done
|
||||
|
||||
if [[ "$expected_limited" == "true" ]]; then
|
||||
if [[ "$rate_limited" == "true" ]]; then
|
||||
echo -e "${GREEN}PASSED${NC} - Rate limiting triggered as expected"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}FAILED${NC} - Rate limiting not triggered (expected)"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
if [[ "$rate_limited" == "false" ]]; then
|
||||
echo -e "${GREEN}PASSED${NC} - No rate limiting for normal traffic"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
echo -e "${YELLOW}UNCERTAIN${NC} - Unexpected rate limiting"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1)) # Count as passed since it's conservative
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to test sustained load
|
||||
test_sustained_load() {
|
||||
local description="$1"
|
||||
local message="$2"
|
||||
local duration="${3:-10}"
|
||||
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
|
||||
echo -n "Testing $description... "
|
||||
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
local rate_limited=false
|
||||
local total_requests=0
|
||||
local successful_requests=0
|
||||
|
||||
while [[ $(($(date +%s) - start_time)) -lt duration ]]; do
|
||||
((total_requests++))
|
||||
local response
|
||||
response=$(timeout 1 bash -c "
|
||||
echo '$message' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
|
||||
if [[ "$response" == *"rate limit"* ]] || [[ "$response" == *"too many"* ]] || [[ "$response" == *"TOO_MANY"* ]]; then
|
||||
rate_limited=true
|
||||
elif [[ "$response" == *"EOSE"* ]] || [[ "$response" == *"EVENT"* ]] || [[ "$response" == *"OK"* ]]; then
|
||||
((successful_requests++))
|
||||
fi
|
||||
|
||||
# Small delay to avoid overwhelming
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
local success_rate=0
|
||||
if [[ $total_requests -gt 0 ]]; then
|
||||
success_rate=$((successful_requests * 100 / total_requests))
|
||||
fi
|
||||
|
||||
if [[ "$rate_limited" == "true" ]]; then
|
||||
echo -e "${GREEN}PASSED${NC} - Rate limiting activated under sustained load (${success_rate}% success rate)"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
echo -e "${YELLOW}UNCERTAIN${NC} - No rate limiting detected (${success_rate}% success rate)"
|
||||
# This might be acceptable if rate limiting is very permissive
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "C-Relay Rate Limiting Test Suite"
|
||||
echo "=========================================="
|
||||
echo "Testing rate limiting against relay at ws://$RELAY_HOST:$RELAY_PORT"
|
||||
echo ""
|
||||
|
||||
# Test basic connectivity first
|
||||
echo "=== Basic Connectivity Test ==="
|
||||
test_rate_limiting "Basic connectivity" '["REQ","rate_test",{}]' 1 false
|
||||
echo ""
|
||||
|
||||
echo "=== Burst Request Testing ==="
|
||||
# Test rapid succession of requests
|
||||
test_rate_limiting "Rapid REQ messages" '["REQ","burst_req_'$(date +%s%N)'",{}]' 20 true
|
||||
test_rate_limiting "Rapid COUNT messages" '["COUNT","burst_count_'$(date +%s%N)'",{}]' 20 true
|
||||
test_rate_limiting "Rapid CLOSE messages" '["CLOSE","burst_close"]' 20 true
|
||||
echo ""
|
||||
|
||||
echo "=== Malformed Message Rate Limiting ==="
|
||||
# Test if malformed messages trigger rate limiting faster
|
||||
test_rate_limiting "Malformed JSON burst" '["REQ","malformed"' 15 true
|
||||
test_rate_limiting "Invalid message type burst" '["INVALID","test",{}]' 15 true
|
||||
test_rate_limiting "Empty message burst" '[]' 15 true
|
||||
echo ""
|
||||
|
||||
echo "=== Sustained Load Testing ==="
|
||||
# Test sustained moderate load
|
||||
test_sustained_load "Sustained REQ load" '["REQ","sustained_'$(date +%s%N)'",{}]' 10
|
||||
test_sustained_load "Sustained COUNT load" '["COUNT","sustained_count_'$(date +%s%N)'",{}]' 10
|
||||
echo ""
|
||||
|
||||
echo "=== Filter Complexity Testing ==="
|
||||
# Test if complex filters trigger rate limiting
|
||||
test_rate_limiting "Complex filter burst" '["REQ","complex_'$(date +%s%N)'",{"authors":["a","b","c"],"kinds":[1,2,3],"#e":["x","y","z"],"#p":["m","n","o"],"since":1000000000,"until":2000000000,"limit":100}]' 10 true
|
||||
echo ""
|
||||
|
||||
echo "=== Subscription Management Testing ==="
|
||||
# Test subscription creation/deletion rate limiting
|
||||
echo -n "Testing subscription churn... "
|
||||
local churn_test_passed=true
|
||||
for i in $(seq 1 25); do
|
||||
# Create subscription
|
||||
timeout 1 bash -c "
|
||||
echo '[\"REQ\",\"churn_'${i}'_'$(date +%s%N)'\",{}]' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1
|
||||
" 2>/dev/null || true
|
||||
|
||||
# Close subscription
|
||||
timeout 1 bash -c "
|
||||
echo '[\"CLOSE\",\"churn_'${i}'_*\"]' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1
|
||||
" 2>/dev/null || true
|
||||
|
||||
sleep 0.05
|
||||
done
|
||||
|
||||
# Check if relay is still responsive
|
||||
if timeout 2 bash -c "
|
||||
echo 'ping' | websocat -n1 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1
|
||||
" 2>/dev/null; then
|
||||
echo -e "${GREEN}PASSED${NC} - Subscription churn handled"
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
else
|
||||
echo -e "${RED}FAILED${NC} - Relay unresponsive after subscription churn"
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "=== Test Results ==="
|
||||
echo "Total tests: $TOTAL_TESTS"
|
||||
echo -e "Passed: ${GREEN}$PASSED_TESTS${NC}"
|
||||
echo -e "Failed: ${RED}$FAILED_TESTS${NC}"
|
||||
|
||||
if [[ $FAILED_TESTS -eq 0 ]]; then
|
||||
echo -e "${GREEN}✓ All rate limiting tests passed!${NC}"
|
||||
echo "Rate limiting appears to be working correctly."
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Some rate limiting tests failed!${NC}"
|
||||
echo "Rate limiting may not be properly configured."
|
||||
exit 1
|
||||
fi
|
||||
@@ -211,9 +211,7 @@ run_monitored_load_test() {
|
||||
# Run a simple load test (create multiple subscriptions)
|
||||
echo "Running load test..."
|
||||
for i in {1..20}; do
|
||||
timeout 3 bash -c "
|
||||
echo '[\"REQ\",\"monitor_test_'${i}'\",{}]' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1
|
||||
" 2>/dev/null &
|
||||
echo "[\"REQ\",\"monitor_test_${i}\",{}]" | timeout 3 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1 &
|
||||
done
|
||||
|
||||
# Let the load run for a bit
|
||||
@@ -222,9 +220,7 @@ run_monitored_load_test() {
|
||||
# Clean up subscriptions
|
||||
echo "Cleaning up test subscriptions..."
|
||||
for i in {1..20}; do
|
||||
timeout 3 bash -c "
|
||||
echo '[\"CLOSE\",\"monitor_test_'${i}'\"]' | websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1
|
||||
" 2>/dev/null &
|
||||
echo "[\"CLOSE\",\"monitor_test_${i}\"]" | timeout 3 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1 &
|
||||
done
|
||||
|
||||
# Wait for monitoring to complete
|
||||
|
||||
@@ -112,9 +112,7 @@ check_relay_status() {
|
||||
fi
|
||||
|
||||
# Fallback: Try WebSocket connection
|
||||
if timeout 5 bash -c "
|
||||
echo '[\"REQ\",\"status_check\",{}]' | websocat -B 1048576 --no-close '$RELAY_URL' >/dev/null 2>&1
|
||||
" 2>/dev/null; then
|
||||
if echo '["REQ","status_check",{}]' | timeout 5 websocat -B 1048576 --no-close "$RELAY_URL" >/dev/null 2>&1; then
|
||||
log "${GREEN}✓ Relay WebSocket endpoint is accessible${NC}"
|
||||
return 0
|
||||
else
|
||||
@@ -236,35 +234,35 @@ OVERALL_START_TIME=$(date +%s)
|
||||
# Run Security Test Suites
|
||||
log "${BLUE}=== SECURITY TEST SUITES ===${NC}"
|
||||
|
||||
run_test_suite "SQL Injection Tests" "tests/sql_injection_tests.sh" "Comprehensive SQL injection vulnerability testing"
|
||||
run_test_suite "Filter Validation Tests" "tests/filter_validation_test.sh" "Input validation for REQ and COUNT messages"
|
||||
run_test_suite "Subscription Validation Tests" "tests/subscription_validation.sh" "Subscription ID and message validation"
|
||||
run_test_suite "Memory Corruption Tests" "tests/memory_corruption_tests.sh" "Buffer overflow and memory safety testing"
|
||||
run_test_suite "Input Validation Tests" "tests/input_validation_tests.sh" "Comprehensive input boundary testing"
|
||||
run_test_suite "SQL Injection Tests" "sql_injection_tests.sh" "Comprehensive SQL injection vulnerability testing"
|
||||
run_test_suite "Filter Validation Tests" "filter_validation_test.sh" "Input validation for REQ and COUNT messages"
|
||||
run_test_suite "Subscription Validation Tests" "subscription_validation.sh" "Subscription ID and message validation"
|
||||
run_test_suite "Memory Corruption Tests" "memory_corruption_tests.sh" "Buffer overflow and memory safety testing"
|
||||
run_test_suite "Input Validation Tests" "input_validation_tests.sh" "Comprehensive input boundary testing"
|
||||
|
||||
# Run Performance Test Suites
|
||||
log ""
|
||||
log "${BLUE}=== PERFORMANCE TEST SUITES ===${NC}"
|
||||
|
||||
run_test_suite "Subscription Limit Tests" "tests/subscription_limits.sh" "Subscription limit enforcement testing"
|
||||
run_test_suite "Load Testing" "tests/load_tests.sh" "High concurrent connection testing"
|
||||
run_test_suite "Stress Testing" "tests/stress_tests.sh" "Resource usage and stability testing"
|
||||
run_test_suite "Rate Limiting Tests" "tests/rate_limiting_tests.sh" "Rate limiting and abuse prevention"
|
||||
run_test_suite "Subscription Limit Tests" "subscription_limits.sh" "Subscription limit enforcement testing"
|
||||
run_test_suite "Load Testing" "load_tests.sh" "High concurrent connection testing"
|
||||
run_test_suite "Stress Testing" "stress_tests.sh" "Resource usage and stability testing"
|
||||
run_test_suite "Rate Limiting Tests" "rate_limiting_tests.sh" "Rate limiting and abuse prevention"
|
||||
|
||||
# Run Integration Test Suites
|
||||
log ""
|
||||
log "${BLUE}=== INTEGRATION TEST SUITES ===${NC}"
|
||||
|
||||
run_test_suite "NIP Protocol Tests" "tests/run_nip_tests.sh" "All NIP protocol compliance tests"
|
||||
run_test_suite "Configuration Tests" "tests/config_tests.sh" "Configuration management and persistence"
|
||||
run_test_suite "Authentication Tests" "tests/auth_tests.sh" "NIP-42 authentication testing"
|
||||
run_test_suite "NIP Protocol Tests" "run_nip_tests.sh" "All NIP protocol compliance tests"
|
||||
run_test_suite "Configuration Tests" "config_tests.sh" "Configuration management and persistence"
|
||||
run_test_suite "Authentication Tests" "auth_tests.sh" "NIP-42 authentication testing"
|
||||
|
||||
# Run Benchmarking Suites
|
||||
log ""
|
||||
log "${BLUE}=== BENCHMARKING SUITES ===${NC}"
|
||||
|
||||
run_test_suite "Performance Benchmarks" "tests/performance_benchmarks.sh" "Performance metrics and benchmarking"
|
||||
run_test_suite "Resource Monitoring" "tests/resource_monitoring.sh" "Memory and CPU usage monitoring"
|
||||
run_test_suite "Performance Benchmarks" "performance_benchmarks.sh" "Performance metrics and benchmarking"
|
||||
run_test_suite "Resource Monitoring" "resource_monitoring.sh" "Memory and CPU usage monitoring"
|
||||
|
||||
# Calculate total duration
|
||||
OVERALL_END_TIME=$(date +%s)
|
||||
|
||||
BIN
tests/sendDM
Executable file
BIN
tests/sendDM
Executable file
Binary file not shown.
296
tests/sendDM.c
Normal file
296
tests/sendDM.c
Normal file
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* NIP-17 Private Direct Messages - Command Line Application
|
||||
*
|
||||
* This example demonstrates how to send NIP-17 private direct messages
|
||||
* using the Nostr Core Library.
|
||||
*
|
||||
* Usage:
|
||||
* ./send_nip17_dm -r <recipient> -s <sender> [-R <relay>]... <message>
|
||||
*
|
||||
* Options:
|
||||
* -r <recipient>: The recipient's public key (npub or hex)
|
||||
* -s <sender>: The sender's private key (nsec or hex)
|
||||
* -R <relay>: Relay URL to send to (can be specified multiple times)
|
||||
* <message>: The message to send (must be the last argument)
|
||||
*
|
||||
* If no relays are specified, uses default relay.
|
||||
* If no sender key is provided, uses a default test key.
|
||||
*
|
||||
* Examples:
|
||||
* ./send_nip17_dm -r npub1example... -s nsec1test... -R wss://relay1.com "Hello from NIP-17!"
|
||||
* ./send_nip17_dm -r 4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa -s aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -R ws://localhost:8888 "config"
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <getopt.h>
|
||||
|
||||
// Default test private key (for demonstration - DO NOT USE IN PRODUCTION)
|
||||
#define DEFAULT_SENDER_NSEC "nsec12kgt0dv2k2safv6s32w8f89z9uw27e68hjaa0d66c5xvk70ezpwqncd045"
|
||||
|
||||
// Default relay for sending DMs
|
||||
#define DEFAULT_RELAY "wss://relay.laantungir.net"
|
||||
|
||||
// Progress callback for publishing
|
||||
void publish_progress_callback(const char* relay_url, const char* status,
|
||||
const char* message, int success_count,
|
||||
int total_relays, int completed_relays, void* user_data) {
|
||||
(void)user_data;
|
||||
|
||||
if (relay_url) {
|
||||
printf("📡 [%s]: %s", relay_url, status);
|
||||
if (message) {
|
||||
printf(" - %s", message);
|
||||
}
|
||||
printf(" (%d/%d completed, %d successful)\n", completed_relays, total_relays, success_count);
|
||||
} else {
|
||||
printf("📡 PUBLISH COMPLETE: %d/%d successful\n", success_count, total_relays);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert npub or hex pubkey to hex format
|
||||
*/
|
||||
int convert_pubkey_to_hex(const char* input_pubkey, char* output_hex) {
|
||||
// Check if it's already hex (64 characters)
|
||||
if (strlen(input_pubkey) == 64) {
|
||||
// Assume it's already hex
|
||||
strcpy(output_hex, input_pubkey);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if it's an npub (starts with "npub1")
|
||||
if (strncmp(input_pubkey, "npub1", 5) == 0) {
|
||||
// Convert npub to hex
|
||||
unsigned char pubkey_bytes[32];
|
||||
if (nostr_decode_npub(input_pubkey, pubkey_bytes) != 0) {
|
||||
fprintf(stderr, "Error: Invalid npub format\n");
|
||||
return -1;
|
||||
}
|
||||
nostr_bytes_to_hex(pubkey_bytes, 32, output_hex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Error: Public key must be 64-character hex or valid npub\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert nsec to private key bytes if needed
|
||||
*/
|
||||
int convert_nsec_to_private_key(const char* input_nsec, unsigned char* private_key) {
|
||||
// Check if it's already hex (64 characters)
|
||||
if (strlen(input_nsec) == 64) {
|
||||
// Convert hex to bytes
|
||||
if (nostr_hex_to_bytes(input_nsec, private_key, 32) != 0) {
|
||||
fprintf(stderr, "Error: Invalid hex private key\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if it's an nsec (starts with "nsec1")
|
||||
if (strncmp(input_nsec, "nsec1", 5) == 0) {
|
||||
// Convert nsec directly to private key bytes
|
||||
if (nostr_decode_nsec(input_nsec, private_key) != 0) {
|
||||
fprintf(stderr, "Error: Invalid nsec format\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Error: Private key must be 64-character hex or valid nsec\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
int main(int argc, char* argv[]) {
|
||||
char* recipient_key = NULL;
|
||||
char* sender_key = NULL;
|
||||
char** relays = NULL;
|
||||
int relay_count = 0;
|
||||
char* message = NULL;
|
||||
|
||||
// Parse command line options
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "r:s:R:")) != -1) {
|
||||
switch (opt) {
|
||||
case 'r':
|
||||
recipient_key = optarg;
|
||||
break;
|
||||
case 's':
|
||||
sender_key = optarg;
|
||||
break;
|
||||
case 'R':
|
||||
relays = realloc(relays, (relay_count + 1) * sizeof(char*));
|
||||
relays[relay_count] = optarg;
|
||||
relay_count++;
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Usage: %s -r <recipient> -s <sender> [-R <relay>]... <message>\n", argv[0]);
|
||||
fprintf(stderr, "Options:\n");
|
||||
fprintf(stderr, " -r <recipient>: The recipient's public key (npub or hex)\n");
|
||||
fprintf(stderr, " -s <sender>: The sender's private key (nsec or hex)\n");
|
||||
fprintf(stderr, " -R <relay>: Relay URL to send to (can be specified multiple times)\n");
|
||||
fprintf(stderr, " <message>: The message to send (must be the last argument)\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for required arguments
|
||||
if (!recipient_key) {
|
||||
fprintf(stderr, "Error: Recipient key (-r) is required\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get message from remaining arguments
|
||||
if (optind >= argc) {
|
||||
fprintf(stderr, "Error: Message is required\n");
|
||||
return 1;
|
||||
}
|
||||
message = argv[optind];
|
||||
|
||||
// Use default values if not provided
|
||||
if (!sender_key) {
|
||||
sender_key = DEFAULT_SENDER_NSEC;
|
||||
}
|
||||
if (relay_count == 0) {
|
||||
relays = malloc(sizeof(char*));
|
||||
relays[0] = DEFAULT_RELAY;
|
||||
relay_count = 1;
|
||||
}
|
||||
|
||||
printf("🧪 NIP-17 Private Direct Message Sender\n");
|
||||
printf("======================================\n\n");
|
||||
|
||||
// Initialize crypto
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize crypto\n");
|
||||
free(relays);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Convert recipient pubkey
|
||||
char recipient_pubkey_hex[65];
|
||||
if (convert_pubkey_to_hex(recipient_key, recipient_pubkey_hex) != 0) {
|
||||
free(relays);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Convert sender private key
|
||||
unsigned char sender_privkey[32];
|
||||
if (convert_nsec_to_private_key(sender_key, sender_privkey) != 0) {
|
||||
free(relays);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Derive sender public key for display
|
||||
unsigned char sender_pubkey_bytes[32];
|
||||
char sender_pubkey_hex[65];
|
||||
if (nostr_ec_public_key_from_private_key(sender_privkey, sender_pubkey_bytes) != 0) {
|
||||
fprintf(stderr, "Failed to derive sender public key\n");
|
||||
return 1;
|
||||
}
|
||||
nostr_bytes_to_hex(sender_pubkey_bytes, 32, sender_pubkey_hex);
|
||||
|
||||
printf("📤 Sender: %s\n", sender_pubkey_hex);
|
||||
printf("📥 Recipient: %s\n", recipient_pubkey_hex);
|
||||
printf("💬 Message: %s\n", message);
|
||||
printf("🌐 Relays: ");
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
printf("%s", relays[i]);
|
||||
if (i < relay_count - 1) printf(", ");
|
||||
}
|
||||
printf("\n\n");
|
||||
|
||||
// Create DM event
|
||||
printf("💬 Creating DM event...\n");
|
||||
const char* recipient_pubkeys[] = {recipient_pubkey_hex};
|
||||
cJSON* dm_event = nostr_nip17_create_chat_event(
|
||||
message,
|
||||
recipient_pubkeys,
|
||||
1,
|
||||
"NIP-17 CLI", // subject
|
||||
NULL, // no reply
|
||||
relays[0], // relay hint (use first relay)
|
||||
sender_pubkey_hex
|
||||
);
|
||||
|
||||
if (!dm_event) {
|
||||
fprintf(stderr, "Failed to create DM event\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Created DM event (kind 14)\n");
|
||||
|
||||
// Send DM (create gift wraps)
|
||||
printf("🎁 Creating gift wraps...\n");
|
||||
cJSON* gift_wraps[10]; // Max 10 gift wraps
|
||||
int gift_wrap_count = nostr_nip17_send_dm(
|
||||
dm_event,
|
||||
recipient_pubkeys,
|
||||
1,
|
||||
sender_privkey,
|
||||
gift_wraps,
|
||||
10
|
||||
);
|
||||
|
||||
cJSON_Delete(dm_event); // Original DM event no longer needed
|
||||
|
||||
if (gift_wrap_count <= 0) {
|
||||
fprintf(stderr, "Failed to create gift wraps\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Created %d gift wrap(s)\n", gift_wrap_count);
|
||||
|
||||
// Publish the gift wrap to relays
|
||||
printf("\n📤 Publishing gift wrap to %d relay(s)...\n", relay_count);
|
||||
|
||||
int success_count = 0;
|
||||
publish_result_t* publish_results = synchronous_publish_event_with_progress(
|
||||
(const char**)relays,
|
||||
relay_count,
|
||||
gift_wraps[0], // Send the first gift wrap
|
||||
&success_count,
|
||||
10, // 10 second timeout
|
||||
publish_progress_callback,
|
||||
NULL, // no user data
|
||||
0, // NIP-42 disabled
|
||||
NULL // no private key for auth
|
||||
);
|
||||
|
||||
if (!publish_results || success_count == 0) {
|
||||
fprintf(stderr, "\n❌ Failed to publish gift wrap to any relay (success_count: %d/%d)\n", success_count, relay_count);
|
||||
// Clean up gift wraps
|
||||
for (int i = 0; i < gift_wrap_count; i++) {
|
||||
cJSON_Delete(gift_wraps[i]);
|
||||
}
|
||||
if (publish_results) free(publish_results);
|
||||
free(relays);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("\n✅ Successfully published NIP-17 DM to %d/%d relay(s)!\n", success_count, relay_count);
|
||||
|
||||
// Clean up
|
||||
free(publish_results);
|
||||
for (int i = 0; i < gift_wrap_count; i++) {
|
||||
cJSON_Delete(gift_wraps[i]);
|
||||
}
|
||||
free(relays);
|
||||
|
||||
nostr_cleanup();
|
||||
|
||||
printf("\n🎉 DM sent successfully! The recipient can now decrypt it using their private key.\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -32,27 +32,40 @@ test_sql_injection() {
|
||||
echo -n "Testing $description... "
|
||||
|
||||
# Send message via websocat and capture response
|
||||
# For now, we'll test without authentication since the relay may not require it for basic queries
|
||||
local response
|
||||
response=$(timeout 5 bash -c "
|
||||
echo '$message' | websocat -B 1048576 --no-close ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -3
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
response=$(echo "$message" | timeout 2 websocat ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'TIMEOUT')
|
||||
|
||||
# Check if the response indicates successful query execution (which would be bad)
|
||||
# Look for signs that SQL injection worked (like database errors or unexpected results)
|
||||
if [[ "$response" == *"SQL"* ]] || [[ "$response" == *"syntax"* ]] || [[ "$response" == *"error"* && ! "$response" == *"error: "* ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Potential SQL injection vulnerability detected"
|
||||
echo " Response: $response"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
elif [[ "$response" == "TIMEOUT" ]]; then
|
||||
if [[ "$response" == "TIMEOUT" ]]; then
|
||||
echo -e "${YELLOW}UNCERTAIN${NC} - Connection timeout (may indicate crash)"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
else
|
||||
echo -e "${GREEN}PASSED${NC} - SQL injection blocked"
|
||||
elif [[ -z "$response" ]]; then
|
||||
# Empty response - relay silently rejected malformed input
|
||||
echo -e "${GREEN}PASSED${NC} - SQL injection blocked (silently rejected)"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
elif [[ "$response" == *"NOTICE"* ]] && [[ "$response" == *"error:"* ]]; then
|
||||
# Relay properly rejected the input with a NOTICE error message
|
||||
echo -e "${GREEN}PASSED${NC} - SQL injection blocked (rejected with error)"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
elif [[ "$response" == *"EOSE"* ]] || [[ "$response" == *"COUNT"* ]] || [[ "$response" == *"EVENT"* ]]; then
|
||||
# Query completed normally - this is expected for properly sanitized input
|
||||
echo -e "${GREEN}PASSED${NC} - SQL injection blocked (query sanitized)"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
elif [[ "$response" == *"SQL"* ]] || [[ "$response" == *"syntax"* ]]; then
|
||||
# Database error leaked - potential vulnerability
|
||||
echo -e "${RED}FAILED${NC} - SQL error leaked: $response"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
else
|
||||
# Unknown response
|
||||
echo -e "${YELLOW}UNCERTAIN${NC} - Unexpected response: $response"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -66,9 +79,7 @@ test_valid_query() {
|
||||
echo -n "Testing $description... "
|
||||
|
||||
local response
|
||||
response=$(timeout 5 bash -c "
|
||||
echo '$message' | websocat -B 1048576 --no-close ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -3
|
||||
" 2>/dev/null || echo 'TIMEOUT')
|
||||
response=$(echo "$message" | timeout 2 websocat ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'TIMEOUT')
|
||||
|
||||
if [[ "$response" == *"EOSE"* ]] || [[ "$response" == *"EVENT"* ]]; then
|
||||
echo -e "${GREEN}PASSED${NC} - Valid query works"
|
||||
@@ -160,9 +171,10 @@ done
|
||||
echo
|
||||
|
||||
echo "=== Kinds Filter SQL Injection Tests ==="
|
||||
# Test numeric kinds with SQL injection
|
||||
test_sql_injection "Kinds filter with UNION injection" "[\"REQ\",\"sql_test_kinds_$RANDOM\",{\"kinds\":[0 UNION SELECT 1,2,3]}]"
|
||||
test_sql_injection "Kinds filter with stacked query" "[\"REQ\",\"sql_test_kinds_$RANDOM\",{\"kinds\":[0; DROP TABLE events; --]}]"
|
||||
# Test numeric kinds with SQL injection attempts (these will fail JSON parsing, which is expected)
|
||||
test_sql_injection "Kinds filter with string injection" "[\"REQ\",\"sql_test_kinds_$RANDOM\",{\"kinds\":[\"1' OR '1'='1\"]}]"
|
||||
test_sql_injection "Kinds filter with negative value" "[\"REQ\",\"sql_test_kinds_$RANDOM\",{\"kinds\":[-1]}]"
|
||||
test_sql_injection "Kinds filter with very large value" "[\"REQ\",\"sql_test_kinds_$RANDOM\",{\"kinds\":[999999999]}]"
|
||||
echo
|
||||
|
||||
echo "=== Search Filter SQL Injection Tests ==="
|
||||
|
||||
448
tests/sql_test.sh
Executable file
448
tests/sql_test.sh
Executable file
@@ -0,0 +1,448 @@
|
||||
#!/bin/bash
|
||||
|
||||
# SQL Query Admin API Test Script
|
||||
# Tests the sql_query command functionality
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
RELAY_URL="ws://localhost:8888"
|
||||
ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
|
||||
RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
|
||||
|
||||
# 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
|
||||
|
||||
# Helper functions
|
||||
print_test() {
|
||||
echo -e "${YELLOW}TEST: $1${NC}"
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
}
|
||||
|
||||
print_pass() {
|
||||
echo -e "${GREEN}✓ PASS: $1${NC}"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
}
|
||||
|
||||
print_fail() {
|
||||
echo -e "${RED}✗ FAIL: $1${NC}"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
}
|
||||
|
||||
# Check if nak is installed
|
||||
check_nak() {
|
||||
if ! command -v nak &> /dev/null; then
|
||||
echo -e "${RED}ERROR: nak command not found. Please install nak first.${NC}"
|
||||
echo -e "${RED}Visit: https://github.com/fiatjaf/nak${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ nak is available${NC}"
|
||||
}
|
||||
|
||||
# Send SQL query command via WebSocket using nak
|
||||
send_sql_query() {
|
||||
local query="$1"
|
||||
local description="$2"
|
||||
|
||||
echo -n "Testing $description... "
|
||||
|
||||
# Create the admin command
|
||||
COMMAND="[\"sql_query\", \"$query\"]"
|
||||
|
||||
# Encrypt the command using NIP-44
|
||||
ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" \
|
||||
--sec "$ADMIN_PRIVKEY" \
|
||||
--recipient-pubkey "$RELAY_PUBKEY" 2>/dev/null)
|
||||
|
||||
if [ -z "$ENCRYPTED_COMMAND" ]; then
|
||||
echo -e "${RED}FAILED${NC} - Failed to encrypt admin command"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create admin event
|
||||
ADMIN_EVENT=$(nak event \
|
||||
--kind 23456 \
|
||||
--content "$ENCRYPTED_COMMAND" \
|
||||
--sec "$ADMIN_PRIVKEY" \
|
||||
--tag "p=$RELAY_PUBKEY" 2>/dev/null)
|
||||
|
||||
if [ -z "$ADMIN_EVENT" ]; then
|
||||
echo -e "${RED}FAILED${NC} - Failed to create admin event"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "=== SENT EVENT ==="
|
||||
echo "$ADMIN_EVENT"
|
||||
echo "==================="
|
||||
|
||||
# Send SQL query event via WebSocket
|
||||
local response
|
||||
response=$(echo "$ADMIN_EVENT" | timeout 10 websocat -B 1048576 "$RELAY_URL" 2>/dev/null | head -3 || echo 'TIMEOUT')
|
||||
|
||||
echo "=== RECEIVED RESPONSE ==="
|
||||
echo "$response"
|
||||
echo "=========================="
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Connection timeout"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$response" # Return the response for further processing
|
||||
}
|
||||
|
||||
# Test functions
|
||||
test_valid_select() {
|
||||
print_test "Valid SELECT query"
|
||||
local response=$(send_sql_query "SELECT * FROM events LIMIT 1" "valid SELECT query")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"query_type":"sql_query"' && echo "$response" | grep -q '"row_count"'; then
|
||||
print_pass "Valid SELECT accepted and executed"
|
||||
else
|
||||
print_fail "Valid SELECT failed: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_select_count() {
|
||||
print_test "SELECT COUNT(*) query"
|
||||
local response=$(send_sql_query "SELECT COUNT(*) FROM events" "COUNT query")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"query_type":"sql_query"' && echo "$response" | grep -q '"row_count"'; then
|
||||
print_pass "COUNT query executed successfully"
|
||||
else
|
||||
print_fail "COUNT query failed: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_blocked_insert() {
|
||||
print_test "INSERT statement blocked"
|
||||
local response=$(send_sql_query "INSERT INTO events VALUES ('id', 'pubkey', 1234567890, 1, 'content', 'sig')" "INSERT blocking")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"status":"error"' && echo "$response" | grep -q '"error_type":"blocked_statement"'; then
|
||||
print_pass "INSERT correctly blocked"
|
||||
else
|
||||
print_fail "INSERT not blocked: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_blocked_update() {
|
||||
print_test "UPDATE statement blocked"
|
||||
local response=$(send_sql_query "UPDATE events SET content = 'test' WHERE id = 'abc123'" "UPDATE blocking")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"status":"error"' && echo "$response" | grep -q '"error_type":"blocked_statement"'; then
|
||||
print_pass "UPDATE correctly blocked"
|
||||
else
|
||||
print_fail "UPDATE not blocked: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_blocked_delete() {
|
||||
print_test "DELETE statement blocked"
|
||||
local response=$(send_sql_query "DELETE FROM events WHERE id = 'abc123'" "DELETE blocking")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"status":"error"' && echo "$response" | grep -q '"error_type":"blocked_statement"'; then
|
||||
print_pass "DELETE correctly blocked"
|
||||
else
|
||||
print_fail "DELETE not blocked: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_blocked_drop() {
|
||||
print_test "DROP statement blocked"
|
||||
local response=$(send_sql_query "DROP TABLE events" "DROP blocking")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"status":"error"' && echo "$response" | grep -q '"error_type":"blocked_statement"'; then
|
||||
print_pass "DROP correctly blocked"
|
||||
else
|
||||
print_fail "DROP not blocked: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_blocked_create() {
|
||||
print_test "CREATE statement blocked"
|
||||
local response=$(send_sql_query "CREATE TABLE test (id TEXT)" "CREATE blocking")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"status":"error"' && echo "$response" | grep -q '"error_type":"blocked_statement"'; then
|
||||
print_pass "CREATE correctly blocked"
|
||||
else
|
||||
print_fail "CREATE not blocked: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_blocked_alter() {
|
||||
print_test "ALTER statement blocked"
|
||||
local response=$(send_sql_query "ALTER TABLE events ADD COLUMN test TEXT" "ALTER blocking")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"status":"error"' && echo "$response" | grep -q '"error_type":"blocked_statement"'; then
|
||||
print_pass "ALTER correctly blocked"
|
||||
else
|
||||
print_fail "ALTER not blocked: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_blocked_pragma() {
|
||||
print_test "PRAGMA statement blocked"
|
||||
local response=$(send_sql_query "PRAGMA table_info(events)" "PRAGMA blocking")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"status":"error"' && echo "$response" | grep -q '"error_type":"blocked_statement"'; then
|
||||
print_pass "PRAGMA correctly blocked"
|
||||
else
|
||||
print_fail "PRAGMA not blocked: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_select_with_where() {
|
||||
print_test "SELECT with WHERE clause"
|
||||
local response=$(send_sql_query "SELECT id, kind FROM events WHERE kind = 1 LIMIT 5" "WHERE clause query")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"query_type":"sql_query"'; then
|
||||
print_pass "WHERE clause query executed"
|
||||
else
|
||||
print_fail "WHERE clause query failed: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_select_with_join() {
|
||||
print_test "SELECT with JOIN"
|
||||
local response=$(send_sql_query "SELECT e.id, e.kind, s.events_sent FROM events e LEFT JOIN active_subscriptions_log s ON e.id = s.subscription_id LIMIT 3" "JOIN query")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"query_type":"sql_query"'; then
|
||||
print_pass "JOIN query executed"
|
||||
else
|
||||
print_fail "JOIN query failed: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_select_views() {
|
||||
print_test "SELECT from views"
|
||||
local response=$(send_sql_query "SELECT * FROM event_kinds_view LIMIT 5" "view query")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"query_type":"sql_query"'; then
|
||||
print_pass "View query executed"
|
||||
else
|
||||
print_fail "View query failed: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_nonexistent_table() {
|
||||
print_test "Query nonexistent table"
|
||||
local response=$(send_sql_query "SELECT * FROM nonexistent_table" "nonexistent table")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"status":"error"'; then
|
||||
print_pass "Nonexistent table error handled correctly"
|
||||
else
|
||||
print_fail "Nonexistent table error not handled: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_invalid_syntax() {
|
||||
print_test "Invalid SQL syntax"
|
||||
local response=$(send_sql_query "SELECT * FROM events WHERE" "invalid syntax")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"status":"error"'; then
|
||||
print_pass "Invalid syntax error handled"
|
||||
else
|
||||
print_fail "Invalid syntax not handled: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_request_id_correlation() {
|
||||
print_test "Request ID correlation"
|
||||
local response=$(send_sql_query "SELECT * FROM events LIMIT 1" "request ID correlation")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"request_id"'; then
|
||||
print_pass "Request ID included in response"
|
||||
else
|
||||
print_fail "Request ID missing from response: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_response_format() {
|
||||
print_test "Response format validation"
|
||||
local response=$(send_sql_query "SELECT * FROM events LIMIT 1" "response format")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"query_type":"sql_query"' &&
|
||||
echo "$response" | grep -q '"timestamp"' &&
|
||||
echo "$response" | grep -q '"execution_time_ms"' &&
|
||||
echo "$response" | grep -q '"row_count"' &&
|
||||
echo "$response" | grep -q '"columns"' &&
|
||||
echo "$response" | grep -q '"rows"'; then
|
||||
print_pass "Response format is valid"
|
||||
else
|
||||
print_fail "Response format invalid: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
test_empty_result() {
|
||||
print_test "Empty result set"
|
||||
local response=$(send_sql_query "SELECT * FROM events WHERE kind = 99999" "empty result")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q '"query_type":"sql_query"'; then
|
||||
print_pass "Empty result handled correctly"
|
||||
else
|
||||
print_fail "Empty result not handled: $response"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "C-Relay SQL Query Admin API Testing Suite"
|
||||
echo "=========================================="
|
||||
echo "Testing SQL query functionality at $RELAY_URL"
|
||||
echo ""
|
||||
|
||||
# Check prerequisites
|
||||
check_nak
|
||||
|
||||
# Test basic connectivity first
|
||||
echo "=== Basic Connectivity Test ==="
|
||||
print_test "Basic connectivity"
|
||||
response=$(send_sql_query "SELECT 1" "basic connectivity")
|
||||
|
||||
if [[ "$response" == *"TIMEOUT"* ]]; then
|
||||
echo -e "${RED}FAILED${NC} - Cannot connect to relay at $RELAY_URL"
|
||||
echo "Make sure the relay is running and accessible."
|
||||
exit 1
|
||||
else
|
||||
print_pass "Relay connection established"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Run test suites
|
||||
echo "=== Query Validation Tests ==="
|
||||
test_valid_select
|
||||
test_select_count
|
||||
test_blocked_insert
|
||||
test_blocked_update
|
||||
test_blocked_delete
|
||||
test_blocked_drop
|
||||
test_blocked_create
|
||||
test_blocked_alter
|
||||
test_blocked_pragma
|
||||
echo ""
|
||||
|
||||
echo "=== Query Execution Tests ==="
|
||||
test_select_with_where
|
||||
test_select_with_join
|
||||
test_select_views
|
||||
test_empty_result
|
||||
echo ""
|
||||
|
||||
echo "=== Error Handling Tests ==="
|
||||
test_nonexistent_table
|
||||
test_invalid_syntax
|
||||
echo ""
|
||||
|
||||
echo "=== Response Format Tests ==="
|
||||
test_request_id_correlation
|
||||
test_response_format
|
||||
echo ""
|
||||
|
||||
echo "=== Test Results ==="
|
||||
echo "Total tests: $TOTAL_TESTS"
|
||||
echo -e "Passed: ${GREEN}$PASSED_TESTS${NC}"
|
||||
echo -e "Failed: ${RED}$FAILED_TESTS${NC}"
|
||||
|
||||
if [[ $FAILED_TESTS -eq 0 ]]; then
|
||||
echo -e "${GREEN}✓ All SQL query tests passed!${NC}"
|
||||
echo "SQL query admin API is working correctly."
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Some SQL query tests failed!${NC}"
|
||||
echo "SQL query admin API may have issues."
|
||||
exit 1
|
||||
fi
|
||||
@@ -34,24 +34,36 @@ echo "[INFO] Testing subscription limits by creating multiple subscriptions..."
|
||||
success_count=0
|
||||
limit_hit=false
|
||||
|
||||
# Create multiple subscriptions in sequence (each in its own connection)
|
||||
for i in {1..30}; do
|
||||
echo "[INFO] Creating subscription $i..."
|
||||
sub_id="limit_test_$i_$(date +%s%N)"
|
||||
response=$(echo "[\"REQ\",\"$sub_id\",{}]" | timeout 5 websocat -n1 "$RELAY_URL" 2>/dev/null || echo "TIMEOUT")
|
||||
# Create multiple subscriptions within a single WebSocket connection
|
||||
echo "[INFO] Creating multiple subscriptions within a single connection..."
|
||||
|
||||
if echo "$response" | grep -q "CLOSED.*$sub_id.*exceeded"; then
|
||||
echo "[INFO] Hit subscription limit at subscription $i"
|
||||
# Build a sequence of REQ messages
|
||||
req_messages=""
|
||||
for i in {1..30}; do
|
||||
sub_id="limit_test_$i"
|
||||
req_messages="${req_messages}[\"REQ\",\"$sub_id\",{}]\n"
|
||||
done
|
||||
|
||||
# Send all messages through a single websocat connection and save to temp file
|
||||
temp_file=$(mktemp)
|
||||
echo -e "$req_messages" | timeout 10 websocat -B 1048576 "$RELAY_URL" 2>/dev/null > "$temp_file" || echo "TIMEOUT" >> "$temp_file"
|
||||
|
||||
# Parse the response to check for subscription limit enforcement
|
||||
subscription_count=0
|
||||
while read -r line; do
|
||||
if [[ "$line" == *"CLOSED"* && "$line" == *"exceeded"* ]]; then
|
||||
echo "[INFO] Hit subscription limit at subscription $((subscription_count + 1))"
|
||||
limit_hit=true
|
||||
break
|
||||
elif echo "$response" | grep -q "EOSE\|EVENT"; then
|
||||
((success_count++))
|
||||
else
|
||||
echo "[WARN] Unexpected response for subscription $i: $response"
|
||||
elif [[ "$line" == *"EOSE"* ]]; then
|
||||
subscription_count=$((subscription_count + 1))
|
||||
fi
|
||||
done < "$temp_file"
|
||||
|
||||
sleep 0.1
|
||||
done
|
||||
success_count=$subscription_count
|
||||
|
||||
# Clean up temp file
|
||||
rm -f "$temp_file"
|
||||
|
||||
if [ "$limit_hit" = true ]; then
|
||||
echo "[PASS] Subscription limit enforcement working (limit hit after $success_count subscriptions)"
|
||||
|
||||
1
text_graph
Submodule
1
text_graph
Submodule
Submodule text_graph added at bf1785f372
Reference in New Issue
Block a user