Compare commits

...

12 Commits

Author SHA1 Message Date
e1da199858 Bump version to v0.8.5.
Some checks failed
Go / build (push) Has been cancelled
2025-09-30 18:08:57 +01:00
45b4f82995 Enable additional NIP support, improve tag handling validation, and simplify WebSocket message processing. 2025-09-30 18:07:42 +01:00
e58eb1d3e3 Remove commented-out debug logs and update rules for Go version and Nostr protocol documentation. 2025-09-30 13:11:41 +01:00
72d6ddff15 Merge remote-tracking branch 'origin/main' 2025-09-30 13:11:00 +01:00
a50ef55d8e Remove commented-out debug logs and update rules for Go version and Nostr protocol documentation. 2025-09-30 13:10:45 +01:00
c2d5d2a165 Merge pull request #1 from Silberengel/docker-deployment-setup
Add Docker deployment and Apache reverse proxy setup

lgtm 👍
2025-09-25 19:44:07 +01:00
05b13399e3 Expand README with follows ACL and relay sync spider documentation. 2025-09-23 16:05:32 +01:00
0dea0ca791 Expand README with detailed build instructions, dependency setup, stress testing, and performance benchmarking. 2025-09-23 16:00:30 +01:00
ff017b45d2 Add relay identity pubkey and subscription-based profile updates; bump version to v0.8.4.
Some checks failed
Go / build (push) Has been cancelled
- Included relay identity public key in `relayinfo` response.
- Added `UpdateRelayProfile` function to dynamically create/update relay's subscription profile.
- Incremented version from v0.8.3 to v0.8.4.
2025-09-23 15:08:30 +01:00
50179e44ed Add dashboard URL to relay description and bump version to v0.8.3.
Some checks failed
Go / build (push) Has been cancelled
- Updated relay description to include a dynamically constructed dashboard URL.
- Incremented version from v0.8.2 to v0.8.3.
2025-09-23 14:55:25 +01:00
34a3b1ba69 Add dynamic relay dashboard URL support and version increment to v0.8.2.
Some checks failed
Go / build (push) Has been cancelled
- Introduced configuration option `RelayURL` for relay dashboard base URL.
- Added dynamic dashboard URL functionality in `PaymentProcessor`.
- Updated payment notifications to include dashboard access link.
- Incremented version to v0.8.2.
2025-09-23 14:49:08 +01:00
Silberengel
42273ab2fa Add Docker deployment and Apache reverse proxy setup
🐳 Docker Implementation:
- Add Dockerfile with Alpine Linux (46MB image)
- Add docker-compose.yml with production-ready config
- Add manage-relay.sh for easy local management
- Add stella-relay.service for systemd auto-start
- Published images: silberengel/orly-relay:latest, :v1, :v2

🔧 Apache Reverse Proxy:
- Add comprehensive Apache proxy guide for Plesk and standard Apache
- Add working WebSocket proxy configuration (ws:// not http://)
- Add troubleshooting guide based on real deployment experience
- Add debug-websocket.sh script for systematic diagnosis
2025-09-21 08:57:27 +02:00
21 changed files with 1656 additions and 85 deletions

View File

@@ -94,4 +94,6 @@ use the source of the relay-tester to help guide what expectations the test has,
and use context7 for information about the nostr protocol, and use additional
log statements to help locate the cause of bugs
always use Go v1.25.1 for everything involving Go
always use Go v1.25.1 for everything involving Go
always use the nips repository that is available at /nips in the root of the repository for documentation about nostr protocol

364
APACHE-PROXY-GUIDE.md Normal file
View File

@@ -0,0 +1,364 @@
# Apache Reverse Proxy Guide for Docker Apps
**Complete guide for WebSocket-enabled applications - covers both Plesk and Standard Apache**
**Updated with real-world troubleshooting solutions**
## 🎯 **What This Solves**
- WebSocket connection failures (`NS_ERROR_WEBSOCKET_CONNECTION_REFUSED`)
- Nostr relay connectivity issues (`HTTP 426` instead of WebSocket upgrade)
- Docker container proxy configuration
- SSL certificate integration
- Plesk configuration conflicts and virtual host precedence issues
## 🐳 **Step 1: Deploy Your Docker Application**
### **For Stella's Orly Relay:**
```bash
# Pull and run the relay
docker run -d \
--name stella-relay \
--restart unless-stopped \
-p 127.0.0.1:7777:7777 \
-v /data/orly-relay:/data \
-e ORLY_OWNERS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx \
-e ORLY_ADMINS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx,npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z \
silberengel/orly-relay:latest
# Test the relay
curl -I http://127.0.0.1:7777
# Should return: HTTP/1.1 426 Upgrade Required
```
### **For Web Apps (like Jumble):**
```bash
# Run with fixed port for easier proxy setup
docker run -d \
--name jumble-app \
--restart unless-stopped \
-p 127.0.0.1:3000:80 \
-e NODE_ENV=production \
silberengel/imwald-jumble:latest
# Test the app
curl -I http://127.0.0.1:3000
```
## 🔧 **Step 2A: PLESK Configuration**
### **For Your Friend's Standard Apache Setup:**
**Tell your friend to create `/etc/apache2/sites-available/domain.conf`:**
```apache
<VirtualHost *:443>
ServerName your-domain.com
# SSL Configuration (Let's Encrypt)
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
# Enable required modules first:
# sudo a2enmod proxy proxy_http proxy_wstunnel rewrite headers ssl
# Proxy settings
ProxyPreserveHost On
ProxyRequests Off
# WebSocket upgrade handling - CRITICAL for apps with WebSockets
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:PORT/$1" [P,L]
# Regular HTTP proxy
ProxyPass / http://127.0.0.1:PORT/
ProxyPassReverse / http://127.0.0.1:PORT/
# Headers for modern web apps
Header always set X-Forwarded-Proto "https"
Header always set X-Forwarded-Port "443"
Header always set X-Forwarded-For %{REMOTE_ADDR}s
# Security headers
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options SAMEORIGIN
</VirtualHost>
# Redirect HTTP to HTTPS
<VirtualHost *:80>
ServerName your-domain.com
Redirect permanent / https://your-domain.com/
</VirtualHost>
```
**Then enable it:**
```bash
sudo a2ensite domain.conf
sudo systemctl reload apache2
```
### **For Plesk Users (You):**
⚠️ **Important**: Plesk often doesn't apply Apache directives correctly through the interface. If the interface method fails, use the "Direct Apache Override" method below.
#### **Method 1: Plesk Interface (Try First)**
1. **Go to Plesk** → Websites & Domains → **your-domain.com**
2. **Click "Apache & nginx Settings"**
3. **DISABLE nginx** (uncheck "Proxy mode" and "Smart static files processing")
4. **Clear HTTP section** (leave empty)
5. **In HTTPS section, add:**
**For Nostr Relay (port 7777):**
```apache
ProxyRequests Off
ProxyPreserveHost On
ProxyPass / ws://127.0.0.1:7777/
ProxyPassReverse / ws://127.0.0.1:7777/
Header always set Access-Control-Allow-Origin "*"
```
6. **Click "Apply"** and wait 60 seconds
#### **Method 2: Direct Apache Override (If Plesk Interface Fails)**
If Plesk doesn't apply your configuration (common issue), bypass it entirely:
```bash
# Create direct Apache override
sudo tee /etc/apache2/conf-available/relay-override.conf << 'EOF'
<VirtualHost YOUR_SERVER_IP:443>
ServerName your-domain.com
ServerAlias www.your-domain.com
ServerAlias ipv4.your-domain.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
DocumentRoot /var/www/relay
# For Nostr relay - proxy everything to WebSocket
ProxyRequests Off
ProxyPreserveHost On
ProxyPass / ws://127.0.0.1:7777/
ProxyPassReverse / ws://127.0.0.1:7777/
# CORS headers
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization"
# Logging
ErrorLog /var/log/apache2/relay-error.log
CustomLog /var/log/apache2/relay-access.log combined
</VirtualHost>
EOF
# Enable the override
sudo a2enconf relay-override
sudo mkdir -p /var/www/relay
sudo systemctl restart apache2
# Remove Plesk config if it conflicts
sudo rm /etc/apache2/plesk.conf.d/vhosts/your-domain.com.conf
```
#### **Method 3: Debugging Plesk Issues**
If configurations aren't being applied:
```bash
# Check if Plesk applied your config
grep -E "(ProxyPass|proxy)" /etc/apache2/plesk.conf.d/vhosts/your-domain.com.conf
# Check virtual host precedence
apache2ctl -S | grep your-domain.com
# Check Apache modules
apache2ctl -M | grep -E "(proxy|rewrite)"
```
#### **For Web Apps (port 3000 or 32768):**
```apache
ProxyPreserveHost On
ProxyRequests Off
# WebSocket upgrade handling
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:32768/$1" [P,L]
# Regular HTTP proxy
ProxyPass / http://127.0.0.1:32768/
ProxyPassReverse / http://127.0.0.1:32768/
# Headers
ProxyAddHeaders On
Header always set X-Forwarded-Proto "https"
Header always set X-Forwarded-Port "443"
```
### **Method B: Direct Apache Override (RECOMMENDED for Plesk)**
⚠️ **Use this if Plesk interface doesn't work** (common issue):
```bash
# Create direct Apache override with your server's IP
sudo tee /etc/apache2/conf-available/relay-override.conf << 'EOF'
<VirtualHost YOUR_SERVER_IP:443>
ServerName your-domain.com
ServerAlias www.your-domain.com
ServerAlias ipv4.your-domain.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
DocumentRoot /var/www/relay
# For Nostr relay - proxy everything to WebSocket
ProxyRequests Off
ProxyPreserveHost On
ProxyPass / ws://127.0.0.1:7777/
ProxyPassReverse / ws://127.0.0.1:7777/
# CORS headers
Header always set Access-Control-Allow-Origin "*"
# Logging
ErrorLog /var/log/apache2/relay-error.log
CustomLog /var/log/apache2/relay-access.log combined
</VirtualHost>
EOF
# Enable override and create directory
sudo a2enconf relay-override
sudo mkdir -p /var/www/relay
sudo systemctl restart apache2
# Remove conflicting Plesk config if needed
sudo rm /etc/apache2/plesk.conf.d/vhosts/your-domain.com.conf
```
## ⚡ **Step 3: Enable Required Modules**
In Plesk, you might need to enable modules. SSH to your server:
```bash
# Enable Apache modules
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel
sudo a2enmod rewrite
sudo systemctl restart apache2
```
## ⚡ **Step 4: Alternative - Nginx in Plesk**
If Apache keeps giving issues, switch to Nginx in Plesk:
1. Go to Plesk → Websites & Domains → orly-relay.imwald.eu
2. Click "Apache & nginx Settings"
3. Enable "nginx" and set it to serve static files
4. In "Additional nginx directives" add:
```nginx
location / {
proxy_pass http://127.0.0.1:7777;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
## 🧪 **Testing**
After making changes:
1. **Apply settings** in Plesk
2. **Wait 30 seconds** for changes to take effect
3. **Test WebSocket**:
```bash
# From your server
echo '["REQ","test",{}]' | websocat wss://orly-relay.imwald.eu/
```
## 🎯 **Expected Result**
- ✅ No more "websocket error" in browser console
- ✅ `wss://orly-relay.imwald.eu/` connects successfully
- ✅ Jumble app can publish notes
## 🚨 **Real-World Troubleshooting Guide**
*Based on actual deployment experience with Plesk and WebSocket issues*
### **Critical Issues & Solutions:**
#### **🔴 HTTP 503 Service Unavailable**
- **Cause**: Docker container not running
- **Check**: `docker ps | grep relay`
- **Fix**: `docker start container-name`
#### **🔴 HTTP 426 Instead of WebSocket Upgrade**
- **Cause**: Apache using `http://` proxy instead of `ws://`
- **Fix**: Use `ProxyPass / ws://127.0.0.1:7777/` (not `http://`)
#### **🔴 Plesk Configuration Not Applied**
- **Symptom**: Config not in `/etc/apache2/plesk.conf.d/vhosts/domain.conf`
- **Solution**: Use Direct Apache Override method (bypass Plesk interface)
#### **🔴 Virtual Host Conflicts**
- **Check**: `apache2ctl -S | grep domain.com`
- **Fix**: Remove Plesk config: `sudo rm /etc/apache2/plesk.conf.d/vhosts/domain.conf`
#### **🔴 Nginx Intercepting (Plesk)**
- **Symptom**: Response shows `Server: nginx`
- **Fix**: Disable nginx in Plesk settings
### **Debug Commands:**
```bash
# Essential debugging
docker ps | grep relay # Container running?
curl -I http://127.0.0.1:7777 # Local relay (should return 426)
apache2ctl -S | grep domain.com # Virtual host precedence
grep ProxyPass /etc/apache2/plesk.conf.d/vhosts/domain.conf # Config applied?
# WebSocket testing
echo '["REQ","test",{}]' | websocat wss://domain.com/ # Root path
echo '["REQ","test",{}]' | websocat wss://domain.com/ws/ # /ws/ path
```
### **Working Solution (Proven):**
```apache
<VirtualHost SERVER_IP:443>
ServerName domain.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/domain.com/privkey.pem
DocumentRoot /var/www/relay
# Direct WebSocket proxy - this is the key!
ProxyRequests Off
ProxyPreserveHost On
ProxyPass / ws://127.0.0.1:7777/
ProxyPassReverse / ws://127.0.0.1:7777/
Header always set Access-Control-Allow-Origin "*"
</VirtualHost>
```
---
**Key Lessons**:
1. Plesk interface often fails to apply Apache directives
2. Use `ws://` proxy for Nostr relays, not `http://`
3. Direct Apache config files are more reliable than Plesk interface
4. Always check virtual host precedence with `apache2ctl -S`

188
DOCKER.md Normal file
View File

@@ -0,0 +1,188 @@
# Docker Deployment Guide
## Quick Start
### 1. Basic Relay Setup
```bash
# Build and start the relay
docker-compose up -d
# View logs
docker-compose logs -f stella-relay
# Stop the relay
docker-compose down
```
### 2. With Nginx Proxy (for SSL/domain setup)
```bash
# Start relay with nginx proxy
docker-compose --profile proxy up -d
# Configure SSL certificates in nginx/ssl/
# Then update nginx/nginx.conf to enable HTTPS
```
## Configuration
### Environment Variables
Copy `env.example` to `.env` and customize:
```bash
cp env.example .env
# Edit .env with your settings
```
Key settings:
- `ORLY_OWNERS`: Owner npubs (comma-separated, full control)
- `ORLY_ADMINS`: Admin npubs (comma-separated, deletion permissions)
- `ORLY_PORT`: Port to listen on (default: 7777)
- `ORLY_MAX_CONNECTIONS`: Max concurrent connections
- `ORLY_CONCURRENT_WORKERS`: CPU cores for concurrent processing (0 = auto)
### Data Persistence
The relay data is stored in `./data` directory which is mounted as a volume.
### Performance Tuning
Based on the v0.4.8 optimizations:
- Concurrent event publishing using all CPU cores
- Optimized BadgerDB access patterns
- Configurable batch sizes and cache settings
## Development
### Local Build
```bash
# Pull the latest image (recommended)
docker pull silberengel/orly-relay:latest
# Or build locally if needed
docker build -t silberengel/orly-relay:latest .
# Run with custom settings
docker run -p 7777:7777 -v $(pwd)/data:/data silberengel/orly-relay:latest
```
### Testing
```bash
# Test WebSocket connection
websocat ws://localhost:7777
# Run stress tests (if available in cmd/stresstest)
go run ./cmd/stresstest -relay ws://localhost:7777
```
## Production Deployment
### SSL Setup
1. Get SSL certificates (Let's Encrypt recommended)
2. Place certificates in `nginx/ssl/`
3. Update `nginx/nginx.conf` to enable HTTPS
4. Start with proxy profile: `docker-compose --profile proxy up -d`
### Monitoring
- Health checks are configured for both services
- Logs are rotated (max 10MB, 3 files)
- Resource limits are set to prevent runaway processes
### Security
- Runs as non-root user (uid 1000)
- Rate limiting configured in nginx
- Configurable authentication and event size limits
## Troubleshooting
### Common Issues (Real-World Experience)
#### **Container Issues:**
1. **Port already in use**: Change `ORLY_PORT` in docker-compose.yml
2. **Permission denied**: Ensure `./data` directory is writable
3. **Container won't start**: Check logs with `docker logs container-name`
#### **WebSocket Issues:**
4. **HTTP 426 instead of WebSocket upgrade**:
- Use `ws://127.0.0.1:7777` in proxy config, not `http://`
- Ensure `proxy_wstunnel` module is enabled
5. **Connection refused in browser but works with websocat**:
- Clear browser cache and service workers
- Try incognito mode
- Add CORS headers to Apache/nginx config
#### **Plesk-Specific Issues:**
6. **Plesk not applying Apache directives**:
- Check if config appears in `/etc/apache2/plesk.conf.d/vhosts/domain.conf`
- Use direct Apache override if Plesk interface fails
7. **Virtual host conflicts**:
- Check precedence with `apache2ctl -S`
- Remove conflicting Plesk configs if needed
#### **SSL Certificate Issues:**
8. **Self-signed certificate after Let's Encrypt**:
- Plesk might not be using the correct certificate
- Import Let's Encrypt certs into Plesk or use direct Apache config
### Debug Commands
```bash
# Container debugging
docker ps | grep relay
docker logs stella-relay
curl -I http://127.0.0.1:7777 # Should return HTTP 426
# WebSocket testing
echo '["REQ","test",{}]' | websocat wss://domain.com/
echo '["REQ","test",{}]' | websocat wss://domain.com/ws/
# Apache debugging (for reverse proxy issues)
apache2ctl -S | grep domain.com
apache2ctl -M | grep -E "(proxy|rewrite)"
grep ProxyPass /etc/apache2/plesk.conf.d/vhosts/domain.conf
```
### Logs
```bash
# View relay logs
docker-compose logs -f stella-relay
# View nginx logs (if using proxy)
docker-compose logs -f nginx
# Apache logs (for reverse proxy debugging)
sudo tail -f /var/log/apache2/error.log
sudo tail -f /var/log/apache2/domain-error.log
```
### Working Reverse Proxy Config
**For Apache (direct config file):**
```apache
<VirtualHost SERVER_IP:443>
ServerName domain.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/domain.com/privkey.pem
# Direct WebSocket proxy for Nostr relay
ProxyRequests Off
ProxyPreserveHost On
ProxyPass / ws://127.0.0.1:7777/
ProxyPassReverse / ws://127.0.0.1:7777/
Header always set Access-Control-Allow-Origin "*"
</VirtualHost>
```
---
*Crafted for Stella's digital forest* 🌲

78
Dockerfile Normal file
View File

@@ -0,0 +1,78 @@
# Dockerfile for Stella's Nostr Relay (next.orly.dev)
# Owner: npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx
FROM golang:alpine AS builder
# Install build dependencies
RUN apk add --no-cache \
git \
build-base \
autoconf \
automake \
libtool \
pkgconfig
# Install secp256k1 library from Alpine packages
RUN apk add --no-cache libsecp256k1-dev
# Set working directory
WORKDIR /build
# Copy go modules first (for better caching)
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build the relay with optimizations from v0.4.8
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags "-w -s" -o relay .
# Create non-root user for security
RUN adduser -D -u 1000 stella && \
chown -R 1000:1000 /build
# Final stage - minimal runtime image
FROM alpine:latest
# Install only runtime dependencies
RUN apk add --no-cache \
ca-certificates \
curl \
libsecp256k1 \
libsecp256k1-dev
WORKDIR /app
# Copy binary from builder
COPY --from=builder /build/relay /app/relay
# Create runtime user and directories
RUN adduser -D -u 1000 stella && \
mkdir -p /data /profiles /app && \
chown -R 1000:1000 /data /profiles /app
# Expose the relay port
EXPOSE 7777
# Set environment variables for Stella's relay
ENV ORLY_DATA_DIR=/data
ENV ORLY_LISTEN=0.0.0.0
ENV ORLY_PORT=7777
ENV ORLY_LOG_LEVEL=info
ENV ORLY_MAX_CONNECTIONS=1000
ENV ORLY_OWNERS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx
ENV ORLY_ADMINS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx,npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z
# Health check to ensure relay is responding
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD sh -c "code=\$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:7777 || echo 000); echo \$code | grep -E '^(101|200|400|404|426)$' >/dev/null || exit 1"
# Create volume for persistent data
VOLUME ["/data"]
# Drop privileges and run as stella user
USER 1000:1000
# Run Stella's Nostr relay
CMD ["/app/relay"]

101
SERVICE-WORKER-FIX.md Normal file
View File

@@ -0,0 +1,101 @@
# Service Worker Certificate Caching Fix
## 🚨 **Problem**
When accessing Jumble from the ImWald landing page, the service worker serves a cached self-signed certificate instead of the new Let's Encrypt certificate.
## ⚡ **Solutions**
### **Option 1: Force Service Worker Update**
Add this to your Jumble app's service worker or main JavaScript:
```javascript
// Force service worker update and certificate refresh
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function(registrations) {
for(let registration of registrations) {
registration.update(); // Force update
}
});
}
// Clear all caches on certificate update
if ('caches' in window) {
caches.keys().then(function(names) {
for (let name of names) {
caches.delete(name);
}
});
}
```
### **Option 2: Update Service Worker Cache Strategy**
In your service worker file, add cache busting for SSL-sensitive requests:
```javascript
// In your service worker
self.addEventListener('fetch', function(event) {
// Don't cache HTTPS requests that might have certificate issues
if (event.request.url.startsWith('https://') &&
event.request.url.includes('imwald.eu')) {
event.respondWith(
fetch(event.request, { cache: 'no-store' })
);
return;
}
// Your existing fetch handling...
});
```
### **Option 3: Version Your Service Worker**
Update your service worker with a new version number:
```javascript
// At the top of your service worker
const CACHE_VERSION = 'v2.0.1'; // Increment this when certificates change
const CACHE_NAME = `jumble-cache-${CACHE_VERSION}`;
// Clear old caches
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
```
### **Option 4: Add Cache Headers**
In your Plesk Apache config for Jumble, add:
```apache
# Prevent service worker from caching SSL-sensitive content
Header always set Cache-Control "no-cache, no-store, must-revalidate"
Header always set Pragma "no-cache"
Header always set Expires "0"
# Only for service worker file
<Files "sw.js">
Header always set Cache-Control "no-cache, no-store, must-revalidate"
</Files>
```
## 🧹 **Immediate User Fix**
For users experiencing the certificate issue:
1. **Clear browser data** for jumble.imwald.eu
2. **Unregister service worker**:
- F12 → Application → Service Workers → Unregister
3. **Hard refresh**: Ctrl+Shift+R
4. **Or use incognito mode** to test
---
This will prevent the service worker from serving stale certificate data.

109
WEBSOCKET-DEBUG.md Normal file
View File

@@ -0,0 +1,109 @@
# WebSocket Connection Debug Guide
## 🚨 **Current Issue**
`wss://orly-relay.imwald.eu/` returns `NS_ERROR_WEBSOCKET_CONNECTION_REFUSED`
## 🔍 **Debug Steps**
### **Step 1: Verify Relay is Running**
```bash
# On your server
curl -I http://127.0.0.1:7777
# Should return: HTTP/1.1 426 Upgrade Required
docker ps | grep stella
# Should show running container
```
### **Step 2: Test Apache Modules**
```bash
# Check if WebSocket modules are enabled
apache2ctl -M | grep -E "(proxy|rewrite)"
# If missing, enable them:
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel
sudo a2enmod rewrite
sudo a2enmod headers
sudo systemctl restart apache2
```
### **Step 3: Check Apache Configuration**
```bash
# Check what Plesk generated
sudo cat /etc/apache2/plesk.conf.d/vhosts/orly-relay.imwald.eu.conf
# Look for proxy and rewrite rules
grep -E "(Proxy|Rewrite)" /etc/apache2/plesk.conf.d/vhosts/orly-relay.imwald.eu.conf
```
### **Step 4: Test Direct WebSocket Connection**
```bash
# Test if the issue is Apache or the relay itself
echo '["REQ","test",{}]' | websocat ws://127.0.0.1:7777/
# If that works, the issue is Apache proxy
# If that fails, the issue is the relay
```
### **Step 5: Check Apache Error Logs**
```bash
# Watch Apache errors in real-time
sudo tail -f /var/log/apache2/error.log
# Then try connecting to wss://orly-relay.imwald.eu/ and see what errors appear
```
## 🔧 **Specific Plesk Fix**
Based on your current status, try this **exact configuration** in Plesk:
### **Go to Apache & nginx Settings for orly-relay.imwald.eu:**
**Clear both HTTP and HTTPS sections, then add to HTTPS:**
```apache
# Enable proxy
ProxyRequests Off
ProxyPreserveHost On
# WebSocket handling - the key part
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule /(.*) ws://127.0.0.1:7777/$1 [P,L]
# Fallback for regular HTTP
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) http://127.0.0.1:7777/$1 [P,L]
# Headers
ProxyAddHeaders On
```
### **Alternative Simpler Version:**
If the above doesn't work, try just:
```apache
ProxyPass / http://127.0.0.1:7777/
ProxyPassReverse / http://127.0.0.1:7777/
ProxyPass /ws ws://127.0.0.1:7777/
ProxyPassReverse /ws ws://127.0.0.1:7777/
```
## 🧪 **Testing Commands**
```bash
# Test the WebSocket after each change
echo '["REQ","test",{}]' | websocat wss://orly-relay.imwald.eu/
# Check what's actually being served
curl -v https://orly-relay.imwald.eu/ 2>&1 | grep -E "(HTTP|upgrade|connection)"
```
## 🎯 **Expected Fix**
The issue is likely that Apache isn't properly handling the WebSocket upgrade request. The `proxy_wstunnel` module and correct rewrite rules should fix this.
Try the **simpler ProxyPass version first** - it's often more reliable in Plesk environments.

View File

@@ -45,6 +45,7 @@ type C struct {
NWCUri string `env:"ORLY_NWC_URI" usage:"NWC (Nostr Wallet Connect) connection string for Lightning payments"`
SubscriptionEnabled bool `env:"ORLY_SUBSCRIPTION_ENABLED" default:"false" usage:"enable subscription-based access control requiring payment for non-directory events"`
MonthlyPriceSats int64 `env:"ORLY_MONTHLY_PRICE_SATS" default:"6000" usage:"price in satoshis for one month subscription (default ~$2 USD)"`
RelayURL string `env:"ORLY_RELAY_URL" usage:"base URL for the relay dashboard (e.g., https://relay.example.com)"`
// Web UI and dev mode settings
WebDisableEmbedded bool `env:"ORLY_WEB_DISABLE" default:"false" usage:"disable serving the embedded web UI; useful for hot-reload during development"`

View File

@@ -72,6 +72,10 @@ func (l *Listener) handleFirstTimeUser(pubkey []byte) {
// Get payment processor to create welcome note
if l.Server.paymentProcessor != nil {
// Set the dashboard URL based on the current HTTP request
dashboardURL := l.Server.DashboardURL(l.req)
l.Server.paymentProcessor.SetDashboardURL(dashboardURL)
if err := l.Server.paymentProcessor.CreateWelcomeNote(pubkey); err != nil {
log.E.F("failed to create welcome note for first-time user: %v", err)
}

View File

@@ -4,9 +4,12 @@ import (
"encoding/json"
"net/http"
"sort"
"strings"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/crypto/p256k"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/protocol/relayinfo"
"next.orly.dev/pkg/version"
)
@@ -32,7 +35,7 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
supportedNIPs := relayinfo.GetList(
relayinfo.BasicProtocol,
relayinfo.Authentication,
// relayinfo.EncryptedDirectMessage,
relayinfo.EncryptedDirectMessage,
relayinfo.EventDeletion,
relayinfo.RelayInformationDocument,
relayinfo.GenericTagQueries,
@@ -40,7 +43,7 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
relayinfo.EventTreatment,
// relayinfo.CommandResults,
relayinfo.ParameterizedReplaceableEvents,
// relayinfo.ExpirationTimestamp,
relayinfo.ExpirationTimestamp,
relayinfo.ProtectedEvents,
relayinfo.RelayListMetadata,
)
@@ -48,7 +51,7 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
supportedNIPs = relayinfo.GetList(
relayinfo.BasicProtocol,
relayinfo.Authentication,
// relayinfo.EncryptedDirectMessage,
relayinfo.EncryptedDirectMessage,
relayinfo.EventDeletion,
relayinfo.RelayInformationDocument,
relayinfo.GenericTagQueries,
@@ -63,12 +66,26 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
}
sort.Sort(supportedNIPs)
log.T.Ln("supported NIPs", supportedNIPs)
// Construct description with dashboard URL
dashboardURL := s.DashboardURL(r)
description := version.Description + " dashboard: " + dashboardURL
// Get relay identity pubkey as hex
var relayPubkey string
if skb, err := s.D.GetRelayIdentitySecret(); err == nil && len(skb) == 32 {
sign := new(p256k.Signer)
if err := sign.InitSec(skb); err == nil {
relayPubkey = hex.Enc(sign.Pub())
}
}
info = &relayinfo.T{
Name: s.Config.AppName,
Description: version.Description,
Description: description,
PubKey: relayPubkey,
Nips: supportedNIPs,
Software: version.URL,
Version: version.V,
Version: strings.TrimPrefix(version.V, "v"),
Limitation: relayinfo.Limits{
AuthRequired: s.Config.ACLMode != "none",
RestrictedWrites: s.Config.ACLMode != "none",

View File

@@ -36,9 +36,6 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
if _, err = env.Unmarshal(msg); chk.E(err) {
return normalize.Error.Errorf(err.Error())
}
// if len(rem) > 0 {
// log.I.F("REQ extra bytes: '%s'", rem)
// }
// send a challenge to the client to auth if an ACL is active
if acl.Registry.Active.Load() != "none" {
if err = authenvelope.NewChallengeWith(l.challenge.Load()).
@@ -59,58 +56,15 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
return
default:
// user has read access or better, continue
// log.D.F("user has %s access", accessLevel)
}
var events event.S
for _, f := range *env.Filters {
// idsLen := 0
// kindsLen := 0
// authorsLen := 0
// tagsLen := 0
// if f != nil {
// if f.Ids != nil {
// idsLen = f.Ids.Len()
// }
// if f.Kinds != nil {
// kindsLen = f.Kinds.Len()
// }
// if f.Authors != nil {
// authorsLen = f.Authors.Len()
// }
// if f.Tags != nil {
// tagsLen = f.Tags.Len()
// }
// }
// log.T.F(
// "REQ %s: filter summary ids=%d kinds=%d authors=%d tags=%d",
// env.Subscription, idsLen, kindsLen, authorsLen, tagsLen,
// )
if f != nil && f.Authors != nil && f.Authors.Len() > 0 {
var authors []string
for _, a := range f.Authors.T {
authors = append(authors, hex.Enc(a))
}
// log.T.F("REQ %s: authors=%v", env.Subscription, authors)
}
// if f != nil && f.Kinds != nil && f.Kinds.Len() > 0 {
// log.T.F("REQ %s: kinds=%v", env.Subscription, f.Kinds.ToUint16())
// }
// if f != nil && f.Ids != nil && f.Ids.Len() > 0 {
// var ids []string
// for _, id := range f.Ids.T {
// ids = append(ids, hex.Enc(id))
// }
// // var lim any
// // if pointers.Present(f.Limit) {
// // lim = *f.Limit
// // } else {
// // lim = nil
// // }
// // log.T.F(
// // "REQ %s: ids filter count=%d ids=%v limit=%v", env.Subscription,
// // f.Ids.Len(), ids, lim,
// // )
// }
if f != nil && pointers.Present(f.Limit) {
if *f.Limit == 0 {
continue
@@ -121,15 +75,10 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
context.Background(), 30*time.Second,
)
defer cancel()
// log.T.F(
// "HandleReq: About to QueryEvents for %s, main context done: %v",
// l.remote, l.ctx.Err() != nil,
// )
if events, err = l.QueryEvents(queryCtx, f); chk.E(err) {
if errors.Is(err, badger.ErrDBClosed) {
return
}
// log.T.F("HandleReq: QueryEvents error for %s: %v", l.remote, err)
err = nil
}
defer func() {
@@ -137,10 +86,6 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
ev.Free()
}
}()
// log.T.F(
// "HandleReq: QueryEvents completed for %s, found %d events",
// l.remote, len(events),
// )
}
var tmp event.S
privCheck:
@@ -152,17 +97,19 @@ privCheck:
if pk == nil {
continue // no auth, can't access private events
}
// Convert authenticated pubkey to npub for comparison
authedNpub, err := bech32encoding.BinToNpub(pk)
if err != nil {
continue // couldn't convert pubkey, skip
}
// Check if authenticated npub is in any private tag
authorized := false
for _, privateTag := range privateTags {
authorizedNpubs := strings.Split(string(privateTag.Value()), ",")
authorizedNpubs := strings.Split(
string(privateTag.Value()), ",",
)
for _, npub := range authorizedNpubs {
if strings.TrimSpace(npub) == string(authedNpub) {
authorized = true
@@ -173,15 +120,15 @@ privCheck:
break
}
}
if !authorized {
continue // not authorized to see this private event
}
tmp = append(tmp, ev)
continue
}
if kind.IsPrivileged(ev.Kind) &&
accessLevel != "admin" { // admins can see all events
// log.T.C(

View File

@@ -153,7 +153,7 @@ whitelist:
continue
}
// log.T.F("received message from %s: %s", remote, string(msg))
go listener.HandleMessage(msg, remote)
listener.HandleMessage(msg, remote)
}
}

View File

@@ -27,12 +27,13 @@ import (
// PaymentProcessor handles NWC payment notifications and updates subscriptions
type PaymentProcessor struct {
nwcClient *nwc.Client
db *database.D
config *config.C
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
nwcClient *nwc.Client
db *database.D
config *config.C
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
dashboardURL string
}
// NewPaymentProcessor creates a new payment processor
@@ -301,8 +302,10 @@ Your paid subscription to this relay will expire in 7 days on %s.
Don't lose access to your private relay! Extend your subscription today.
Relay: nostr:%s`,
expiryTime.Format("2006-01-02 15:04:05 UTC"), monthlyPrice, monthlyPrice, string(relayNpubForContent))
Relay: nostr:%s
Log in to the relay dashboard to access your configuration at: %s`,
expiryTime.Format("2006-01-02 15:04:05 UTC"), monthlyPrice, monthlyPrice, string(relayNpubForContent), pp.getDashboardURL())
// Build the event
ev := event.New()
@@ -402,8 +405,10 @@ Simply zap this note with your payment amount:
Thank you for considering supporting decentralized communication!
Relay: nostr:%s`,
trialEnd.Format("2006-01-02 15:04:05 UTC"), monthlyPrice, dailyRate, monthlyPrice, string(relayNpubForContent))
Relay: nostr:%s
Log in to the relay dashboard to access your configuration at: %s`,
trialEnd.Format("2006-01-02 15:04:05 UTC"), monthlyPrice, dailyRate, monthlyPrice, string(relayNpubForContent), pp.getDashboardURL())
// Build the event
ev := event.New()
@@ -605,9 +610,9 @@ func (pp *PaymentProcessor) createPaymentNote(payerPubkey []byte, satsReceived i
return fmt.Errorf("failed to encode relay npub: %w", err)
}
// Create the note content with nostr:npub link
content := fmt.Sprintf("Payment received: %d sats for %d days. Subscription expires: %s\n\nRelay: nostr:%s",
satsReceived, days, expiryTime.Format("2006-01-02 15:04:05 UTC"), string(relayNpubForContent))
// Create the note content with nostr:npub link and dashboard link
content := fmt.Sprintf("Payment received: %d sats for %d days. Subscription expires: %s\n\nRelay: nostr:%s\n\nLog in to the relay dashboard to access your configuration at: %s",
satsReceived, days, expiryTime.Format("2006-01-02 15:04:05 UTC"), string(relayNpubForContent), pp.getDashboardURL())
// Build the event
ev := event.New()
@@ -699,7 +704,9 @@ To extend your subscription after the trial ends, simply zap this note with the
Relay: nostr:%s
Enjoy your time on the relay!`, monthlyPrice, monthlyPrice, string(relayNpubForContent))
Log in to the relay dashboard to access your configuration at: %s
Enjoy your time on the relay!`, monthlyPrice, monthlyPrice, string(relayNpubForContent), pp.getDashboardURL())
// Build the event
ev := event.New()
@@ -750,6 +757,25 @@ Enjoy your time on the relay!`, monthlyPrice, monthlyPrice, string(relayNpubForC
return nil
}
// SetDashboardURL sets the dynamic dashboard URL based on HTTP request
func (pp *PaymentProcessor) SetDashboardURL(url string) {
pp.dashboardURL = url
}
// getDashboardURL returns the dashboard URL for the relay
func (pp *PaymentProcessor) getDashboardURL() string {
// Use dynamic URL if available
if pp.dashboardURL != "" {
return pp.dashboardURL
}
// Fallback to static config
if pp.config.RelayURL != "" {
return pp.config.RelayURL
}
// Default fallback if no URL is configured
return "https://your-relay.example.com"
}
// extractNpubFromDescription extracts an npub from the payment description
func (pp *PaymentProcessor) extractNpubFromDescription(description string) string {
// check if the entire description is just an npub
@@ -794,6 +820,58 @@ func (pp *PaymentProcessor) npubToPubkey(npubStr string) ([]byte, error) {
return pubkey, nil
}
// UpdateRelayProfile creates or updates the relay's kind 0 profile with subscription information
func (pp *PaymentProcessor) UpdateRelayProfile() error {
// Get relay identity secret to sign the profile
skb, err := pp.db.GetRelayIdentitySecret()
if err != nil || len(skb) != 32 {
return fmt.Errorf("no relay identity configured")
}
// Initialize signer
sign := new(p256k.Signer)
if err := sign.InitSec(skb); err != nil {
return fmt.Errorf("failed to initialize signer: %w", err)
}
monthlyPrice := pp.config.MonthlyPriceSats
if monthlyPrice <= 0 {
monthlyPrice = 6000
}
// Calculate daily rate
dailyRate := monthlyPrice / 30
// Get relay wss:// URL - use dashboard URL but with wss:// scheme
relayURL := strings.Replace(pp.getDashboardURL(), "https://", "wss://", 1)
// Create profile content as JSON
profileContent := fmt.Sprintf(`{
"name": "Relay Bot",
"about": "This relay requires a subscription to access. Zap any of my notes to pay for access. Monthly price: %d sats (%d sats/day). Relay: %s",
"lud16": "",
"nip05": "",
"website": "%s"
}`, monthlyPrice, dailyRate, relayURL, pp.getDashboardURL())
// Build the profile event
ev := event.New()
ev.Kind = kind.ProfileMetadata.K // Kind 0 for profile metadata
ev.Pubkey = sign.Pub()
ev.CreatedAt = timestamp.Now().V
ev.Content = []byte(profileContent)
ev.Tags = tag.NewS()
// Sign and save the event
ev.Sign(sign)
if _, _, err := pp.db.SaveEvent(pp.ctx, ev); err != nil {
return fmt.Errorf("failed to save relay profile: %w", err)
}
log.I.F("updated relay profile with subscription information")
return nil
}
// decodeAnyPubkey decodes a public key from either hex string or npub format
func decodeAnyPubkey(s string) ([]byte, error) {
s = strings.TrimSpace(s)

View File

@@ -113,6 +113,15 @@ func (s *Server) ServiceURL(req *http.Request) (st string) {
return proto + "://" + host
}
// DashboardURL constructs HTTPS URL for the dashboard based on the HTTP request
func (s *Server) DashboardURL(req *http.Request) string {
host := req.Header.Get("X-Forwarded-Host")
if host == "" {
host = req.Host
}
return "https://" + host
}
// UserInterface sets up a basic Nostr NDK interface that allows users to log into the relay user interface
func (s *Server) UserInterface() {
if s.mux == nil {

Submodule cmd/benchmark/external/khatru deleted from 668c41b988

116
debug-websocket.sh Executable file
View File

@@ -0,0 +1,116 @@
#!/bin/bash
# WebSocket Debug Script for Stella's Orly Relay
echo "🔍 Debugging WebSocket Connection for orly-relay.imwald.eu"
echo "=================================================="
echo ""
echo "📋 Step 1: Check if relay container is running"
echo "----------------------------------------------"
docker ps | grep -E "(stella|relay|orly)" || echo "❌ No relay containers found"
echo ""
echo "📋 Step 2: Test local relay connection"
echo "--------------------------------------"
if curl -s -I http://127.0.0.1:7777 | grep -q "426"; then
echo "✅ Local relay responding correctly (HTTP 426)"
else
echo "❌ Local relay not responding correctly"
curl -I http://127.0.0.1:7777
fi
echo ""
echo "📋 Step 3: Check Apache modules"
echo "------------------------------"
if apache2ctl -M 2>/dev/null | grep -q "proxy_wstunnel"; then
echo "✅ proxy_wstunnel module enabled"
else
echo "❌ proxy_wstunnel module NOT enabled"
echo "Run: sudo a2enmod proxy_wstunnel"
fi
if apache2ctl -M 2>/dev/null | grep -q "rewrite"; then
echo "✅ rewrite module enabled"
else
echo "❌ rewrite module NOT enabled"
echo "Run: sudo a2enmod rewrite"
fi
echo ""
echo "📋 Step 4: Check Plesk Apache configuration"
echo "------------------------------------------"
if [ -f "/etc/apache2/plesk.conf.d/vhosts/orly-relay.imwald.eu.conf" ]; then
echo "✅ Plesk config file exists"
echo "Current proxy configuration:"
grep -E "(Proxy|Rewrite|proxy|rewrite)" /etc/apache2/plesk.conf.d/vhosts/orly-relay.imwald.eu.conf || echo "❌ No proxy/rewrite rules found"
else
echo "❌ Plesk config file not found"
fi
echo ""
echo "📋 Step 5: Test WebSocket connections"
echo "------------------------------------"
# Test with curl first (simpler)
echo "Testing HTTP upgrade request to local relay..."
if curl -s -I -H "Connection: Upgrade" -H "Upgrade: websocket" http://127.0.0.1:7777 | grep -q "426\|101"; then
echo "✅ Local relay accepts upgrade requests"
else
echo "❌ Local relay doesn't accept upgrade requests"
fi
echo "Testing HTTP upgrade request to remote relay..."
if curl -s -I -H "Connection: Upgrade" -H "Upgrade: websocket" https://orly-relay.imwald.eu | grep -q "426\|101"; then
echo "✅ Remote relay accepts upgrade requests"
else
echo "❌ Remote relay doesn't accept upgrade requests"
echo "This indicates Apache proxy issue"
fi
# Try to install websocat if not available
if ! command -v websocat >/dev/null 2>&1; then
echo ""
echo "📥 Installing websocat for proper WebSocket testing..."
if wget -q https://github.com/vi/websocat/releases/download/v1.12.0/websocat.x86_64-unknown-linux-musl -O websocat 2>/dev/null; then
chmod +x websocat
echo "✅ websocat installed"
else
echo "❌ Could not install websocat (no internet or wget issue)"
echo "Manual install: wget https://github.com/vi/websocat/releases/download/v1.12.0/websocat.x86_64-unknown-linux-musl -O websocat && chmod +x websocat"
fi
fi
# Test with websocat if available
if command -v ./websocat >/dev/null 2>&1; then
echo ""
echo "Testing actual WebSocket connection..."
echo "Local WebSocket test:"
timeout 3 bash -c 'echo "[\"REQ\",\"test\",{}]" | ./websocat ws://127.0.0.1:7777/' 2>/dev/null || echo "❌ Local WebSocket failed"
echo "Remote WebSocket test (ignoring SSL):"
timeout 3 bash -c 'echo "[\"REQ\",\"test\",{}]" | ./websocat --insecure wss://orly-relay.imwald.eu/' 2>/dev/null || echo "❌ Remote WebSocket failed"
fi
echo ""
echo "📋 Step 6: Check ports and connections"
echo "------------------------------------"
echo "Ports listening on 7777:"
netstat -tlnp 2>/dev/null | grep :7777 || ss -tlnp 2>/dev/null | grep :7777 || echo "❌ No process listening on port 7777"
echo ""
echo "📋 Step 7: Test SSL certificate"
echo "------------------------------"
echo "Certificate issuer:"
echo | openssl s_client -connect orly-relay.imwald.eu:443 -servername orly-relay.imwald.eu 2>/dev/null | openssl x509 -noout -issuer 2>/dev/null || echo "❌ SSL test failed"
echo ""
echo "🎯 RECOMMENDED NEXT STEPS:"
echo "========================="
echo "1. If proxy_wstunnel is missing: sudo a2enmod proxy_wstunnel && sudo systemctl restart apache2"
echo "2. If no proxy rules found: Add configuration in Plesk Apache & nginx Settings"
echo "3. If local WebSocket fails: Check if relay container is actually running"
echo "4. If remote WebSocket fails but local works: Apache proxy configuration issue"
echo ""
echo "🔧 Try this simple Plesk configuration:"
echo "ProxyPass / http://127.0.0.1:7777/"
echo "ProxyPassReverse / http://127.0.0.1:7777/"

93
docker-compose.yml Normal file
View File

@@ -0,0 +1,93 @@
# Docker Compose for Stella's Nostr Relay
# Owner: npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx
version: '3.8'
services:
stella-relay:
image: silberengel/orly-relay:latest
container_name: stella-nostr-relay
restart: unless-stopped
ports:
- "127.0.0.1:7777:7777"
volumes:
- relay_data:/data
- ./profiles:/profiles:ro
environment:
# Relay Configuration
- ORLY_DATA_DIR=/data
- ORLY_LISTEN=0.0.0.0
- ORLY_PORT=7777
- ORLY_LOG_LEVEL=info
- ORLY_MAX_CONNECTIONS=1000
- ORLY_OWNERS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx
- ORLY_ADMINS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx,npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z
# Performance Settings (based on v0.4.8 optimizations)
- ORLY_CONCURRENT_WORKERS=0 # 0 = auto-detect CPU cores
- ORLY_BATCH_SIZE=1000
- ORLY_CACHE_SIZE=10000
# Database Settings
- BADGER_LOG_LEVEL=ERROR
- BADGER_SYNC_WRITES=false # Better performance, slightly less durability
# Security Settings
- ORLY_REQUIRE_AUTH=false
- ORLY_MAX_EVENT_SIZE=65536
- ORLY_MAX_SUBSCRIPTIONS=20
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7777"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Resource limits
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
reservations:
memory: 256M
cpus: '0.25'
# Logging configuration
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Optional: Nginx reverse proxy for SSL/domain setup
nginx:
image: nginx:alpine
container_name: stella-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- nginx_logs:/var/log/nginx
depends_on:
- stella-relay
profiles:
- proxy # Only start with: docker-compose --profile proxy up
volumes:
relay_data:
driver: local
driver_opts:
type: none
o: bind
device: ./data
nginx_logs:
driver: local
networks:
default:
name: stella-relay-network

89
manage-relay.sh Executable file
View File

@@ -0,0 +1,89 @@
#!/bin/bash
# Stella's Orly Relay Management Script
set -e
RELAY_SERVICE="stella-relay"
RELAY_URL="ws://127.0.0.1:7777"
case "${1:-}" in
"start")
echo "🚀 Starting Stella's Orly Relay..."
sudo systemctl start $RELAY_SERVICE
echo "✅ Relay started!"
;;
"stop")
echo "⏹️ Stopping Stella's Orly Relay..."
sudo systemctl stop $RELAY_SERVICE
echo "✅ Relay stopped!"
;;
"restart")
echo "🔄 Restarting Stella's Orly Relay..."
sudo systemctl restart $RELAY_SERVICE
echo "✅ Relay restarted!"
;;
"status")
echo "📊 Stella's Orly Relay Status:"
sudo systemctl status $RELAY_SERVICE --no-pager
;;
"logs")
echo "📜 Stella's Orly Relay Logs:"
sudo journalctl -u $RELAY_SERVICE -f --no-pager
;;
"test")
echo "🧪 Testing relay connection..."
if curl -s -I http://127.0.0.1:7777 | grep -q "426 Upgrade Required"; then
echo "✅ Relay is responding correctly!"
echo "📡 WebSocket URL: $RELAY_URL"
else
echo "❌ Relay is not responding correctly"
exit 1
fi
;;
"enable")
echo "🔧 Enabling relay to start at boot..."
sudo systemctl enable $RELAY_SERVICE
echo "✅ Relay will start automatically at boot!"
;;
"disable")
echo "🔧 Disabling relay auto-start..."
sudo systemctl disable $RELAY_SERVICE
echo "✅ Relay will not start automatically at boot!"
;;
"info")
echo "📋 Stella's Orly Relay Information:"
echo " Service: $RELAY_SERVICE"
echo " WebSocket URL: $RELAY_URL"
echo " HTTP URL: http://127.0.0.1:7777"
echo " Data Directory: /home/madmin/.local/share/orly-relay"
echo " Config Directory: $(pwd)"
echo ""
echo "🔑 Admin NPubs:"
echo " Stella: npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx"
echo " Admin2: npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z"
;;
*)
echo "🌲 Stella's Orly Relay Management Script"
echo ""
echo "Usage: $0 [COMMAND]"
echo ""
echo "Commands:"
echo " start Start the relay"
echo " stop Stop the relay"
echo " restart Restart the relay"
echo " status Show relay status"
echo " logs Show relay logs (follow mode)"
echo " test Test relay connection"
echo " enable Enable auto-start at boot"
echo " disable Disable auto-start at boot"
echo " info Show relay information"
echo ""
echo "Examples:"
echo " $0 start # Start the relay"
echo " $0 status # Check if it's running"
echo " $0 test # Test WebSocket connection"
echo " $0 logs # Watch real-time logs"
echo ""
echo "🌲 Crafted in the digital forest by Stella ✨"
;;
esac

View File

@@ -147,6 +147,9 @@ func (s *S) Unmarshal(b []byte) (r []byte, err error) {
// GetFirst returns the first tag.T that has the same Key as t.
func (s *S) GetFirst(t []byte) (first *T) {
if s == nil || len(*s) < 1 {
return
}
for _, tt := range *s {
if tt.Len() == 0 {
continue
@@ -159,7 +162,13 @@ func (s *S) GetFirst(t []byte) (first *T) {
}
func (s *S) GetAll(t []byte) (all []*T) {
if s == nil || len(*s) < 1 {
return
}
for _, tt := range *s {
if len(tt.T) < 1 {
continue
}
if utils.FastEqual(tt.T[0], t) {
all = append(all, tt)
}

View File

@@ -1 +1 @@
v0.8.1
v0.8.5

View File

@@ -20,4 +20,332 @@ ORLY is a nostr relay written from the ground up to be performant, low latency,
ORLY uses a fast embedded link:https://github.com/hypermodeinc/badger[badger] database with a database designed for high performance querying and event storage.
On linux platforms, it uses https://github.com/bitcoin/secp256k1[libsecp256k1]-enabled signature and signature verification (see link:pkg/p256k/README.md[here]).
On linux platforms, it uses https://github.com/bitcoin/secp256k1[libsecp256k1]-enabled signature and signature verification (see link:pkg/crypto/p256k/README.md[here]).
== building
ORLY is a standard Go application that can be built using the Go toolchain.
=== prerequisites
- Go 1.25.0 or later
- Git
- For web UI: link:https://bun.sh/[Bun] JavaScript runtime
=== basic build
To build the relay binary only:
[source,bash]
----
git clone <repository-url>
cd next.orly.dev
go build -o orly
----
=== building with web UI
To build with the embedded web interface:
[source,bash]
----
# Build the React web application
cd app/web
bun install
bun run build
# Build the Go binary from project root
cd ../../
go build -o orly
----
You can automate this process with a build script:
[source,bash]
----
#!/bin/bash
# build.sh
echo "Building React app..."
cd app/web
bun install
bun run build
echo "Building Go binary..."
cd ../../
go build -o orly
echo "Build complete!"
----
Make it executable with `chmod +x build.sh` and run with `./build.sh`.
== secp256k1 dependency
ORLY uses the optimized `libsecp256k1` C library from Bitcoin Core for schnorr signatures, providing 4x faster signing and ECDH operations compared to pure Go implementations.
=== installation
For Ubuntu/Debian, you can use the provided installation script:
[source,bash]
----
./scripts/ubuntu_install_libsecp256k1.sh
----
Or install manually:
[source,bash]
----
# Install build dependencies
sudo apt -y install build-essential autoconf libtool
# Initialize and build secp256k1
cd pkg/crypto/p256k/secp256k1
git submodule init
git submodule update
./autogen.sh
./configure --enable-module-schnorrsig --enable-module-ecdh --prefix=/usr
make
sudo make install
----
=== fallback mode
If you need to build without the C library dependency, disable CGO:
[source,bash]
----
export CGO_ENABLED=0
go build -o orly
----
This uses the pure Go `btcec` fallback library, which is slower but doesn't require system dependencies.
== stress testing
The stress tester is a tool for performance testing relay implementations under various load conditions.
=== usage
[source,bash]
----
cd cmd/stresstest
go run . [options]
----
Or use the compiled binary:
[source,bash]
----
./cmd/stresstest/stresstest [options]
----
=== options
* `--address` - Relay address (default: localhost)
* `--port` - Relay port (default: 3334)
* `--workers` - Number of concurrent publisher workers (default: 8)
* `--duration` - How long to run the stress test (default: 60s)
* `--publish-timeout` - Timeout waiting for OK per publish (default: 15s)
* `--query-workers` - Number of concurrent query workers (default: 4)
* `--query-timeout` - Subscription timeout for queries (default: 3s)
* `--query-min-interval` - Minimum interval between queries per worker (default: 50ms)
* `--query-max-interval` - Maximum interval between queries per worker (default: 300ms)
* `--skip-cache` - Skip uploading example events before running
=== example
[source,bash]
----
# Run stress test against local relay for 2 minutes with 16 workers
go run cmd/stresstest/main.go --address localhost --port 3334 --workers 16 --duration 120s
# Test a remote relay with higher query load
go run cmd/stresstest/main.go --address relay.example.com --port 443 --query-workers 8 --duration 300s
----
The stress tester will show real-time statistics including events sent/received per second, query counts, and results.
== benchmarks
The benchmark suite provides comprehensive performance testing and comparison across multiple relay implementations.
=== quick start
1. **Setup external relays:**
+
[source,bash]
----
cd cmd/benchmark
./setup-external-relays.sh
----
2. **Run all benchmarks:**
+
[source,bash]
----
docker compose up --build
----
3. **View results:**
+
[source,bash]
----
# View aggregate report
cat reports/run_YYYYMMDD_HHMMSS/aggregate_report.txt
# List individual relay results
ls reports/run_YYYYMMDD_HHMMSS/
----
=== benchmark types
The suite includes three main benchmark patterns:
==== peak throughput test
Tests maximum event ingestion rate with concurrent workers pushing events as fast as possible. Measures events/second, latency distribution, and success rate.
==== burst pattern test
Simulates real-world traffic with alternating high-activity bursts and quiet periods to test relay behavior under varying loads.
==== mixed read/write test
Concurrent read and write operations to test query performance while events are being ingested. Measures combined throughput and latency.
=== tested relays
The benchmark suite compares:
* **next.orly.dev** (this repository) - BadgerDB-based relay
* **Khatru** - SQLite and Badger variants
* **Relayer** - Basic example implementation
* **Strfry** - C++ LMDB-based relay
* **nostr-rs-relay** - Rust-based relay with SQLite
=== metrics reported
* **Throughput**: Events processed per second
* **Latency**: Average, P95, and P99 response times
* **Success Rate**: Percentage of successful operations
* **Memory Usage**: Peak memory consumption during tests
* **Error Analysis**: Detailed error reporting and categorization
Results are timestamped and stored in the `reports/` directory for tracking performance improvements over time.
== follows ACL
The follows ACL (Access Control List) system provides a flexible way to control relay access based on social relationships in the Nostr network. It grants different access levels to users based on whether they are followed by designated admin users.
=== how it works
The follows ACL system operates by:
1. **Admin Configuration**: Designated admin users are specified in the relay configuration
2. **Follow List Discovery**: The system fetches follow lists (kind 3 events) from admin users
3. **Access Level Assignment**:
- **Admin access**: Users listed as admins get full administrative privileges
- **Write access**: Users followed by any admin can publish events to the relay
- **Read access**: All other users can only read events from the relay
=== configuration
Enable the follows ACL system by setting the ACL mode:
[source,bash]
----
export ORLY_ACL_MODE=follows
export ORLY_ADMINS=npub1abc...,npub1xyz...
----
Or in your environment configuration:
[source,env]
----
ORLY_ACL_MODE=follows
ORLY_ADMINS=npub1abc123...,npub1xyz456...
----
=== usage example
[source,bash]
----
# Set up a relay with follows ACL
export ORLY_ACL_MODE=follows
export ORLY_ADMINS=npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku
# Start the relay
./orly
----
The relay will automatically:
- Load the follow lists of the specified admin users
- Grant write access to anyone followed by these admins
- Provide read-only access to everyone else
- Update follow lists dynamically as admins modify their follows
== relay sync spider
The relay sync spider is an intelligent synchronization system that discovers and syncs events from other Nostr relays based on social relationships. It works in conjunction with the follows ACL to create a distributed network of synchronized content.
=== how it works
The spider operates in two phases:
1. **Relay Discovery**:
- Finds relay lists (kind 10002 events) from followed users
- Builds a list of relays used by people in your social network
- Prioritizes relays mentioned by admin users
2. **Event Synchronization**:
- Queries discovered relays for events from followed users
- Performs one-time historical sync (default: 1 month back)
- Runs periodic syncs to stay current with new events
- Validates and stores events locally
=== configuration
Enable the spider by setting the spider mode to "follow":
[source,bash]
----
export ORLY_SPIDER_MODE=follow
export ORLY_SPIDER_FREQUENCY=1h
----
Configuration options:
* `ORLY_SPIDER_MODE` - Spider mode: "none" (disabled) or "follow" (enabled)
* `ORLY_SPIDER_FREQUENCY` - How often to sync (default: 1h)
=== usage example
[source,bash]
----
# Enable both follows ACL and spider sync
export ORLY_ACL_MODE=follows
export ORLY_SPIDER_MODE=follow
export ORLY_SPIDER_FREQUENCY=30m
export ORLY_ADMINS=npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku
# Start the relay
./orly
----
The spider will:
- Perform a one-time sync of the last month's events
- Discover relays from followed users' relay lists
- Sync events from those relays every 30 minutes
- Only sync events from users in the follow network
=== benefits
* **Decentralized Content**: Automatically aggregates content from your social network
* **Reduced Relay Dependency**: Less reliance on single large relays
* **Improved User Experience**: Users see content from their social circle even when offline from other relays
* **Network Resilience**: Content remains accessible even if origin relays go offline
=== technical notes
* The spider only runs when `ORLY_ACL_MODE=follows` to ensure proper authorization
* One-time sync is marked to prevent repeated historical syncs on restart
* Event validation ensures only properly signed events are stored
* Sync windows are configurable to balance freshness with resource usage

39
stella-relay.service Normal file
View File

@@ -0,0 +1,39 @@
[Unit]
Description=Stella's Orly Nostr Relay
Documentation=https://github.com/Silberengel/next.orly.dev
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=madmin
Group=madmin
WorkingDirectory=/home/madmin/Projects/GitCitadel/next.orly.dev
ExecStart=docker compose up stella-relay
ExecStop=docker compose down
Restart=always
RestartSec=10
TimeoutStartSec=60
TimeoutStopSec=30
# Environment variables
Environment=ORLY_DATA_DIR=/home/madmin/.local/share/orly-relay
Environment=ORLY_LISTEN=127.0.0.1
Environment=ORLY_PORT=7777
Environment=ORLY_LOG_LEVEL=info
Environment=ORLY_OWNERS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx
Environment=ORLY_ADMINS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx,npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z
# Security settings
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/madmin/.local/share/orly-relay
ReadWritePaths=/home/madmin/Projects/GitCitadel/next.orly.dev/data
# Resource limits
LimitNOFILE=65536
LimitNPROC=4096
[Install]
WantedBy=multi-user.target