Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e1da199858
|
|||
|
45b4f82995
|
|||
|
e58eb1d3e3
|
|||
|
72d6ddff15
|
|||
|
a50ef55d8e
|
|||
| c2d5d2a165 | |||
|
05b13399e3
|
|||
|
0dea0ca791
|
|||
|
ff017b45d2
|
|||
|
50179e44ed
|
|||
|
34a3b1ba69
|
|||
|
|
42273ab2fa |
@@ -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
364
APACHE-PROXY-GUIDE.md
Normal 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
188
DOCKER.md
Normal 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
78
Dockerfile
Normal 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
101
SERVICE-WORKER-FIX.md
Normal 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
109
WEBSOCKET-DEBUG.md
Normal 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.
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
1
cmd/benchmark/external/khatru
vendored
1
cmd/benchmark/external/khatru
vendored
Submodule cmd/benchmark/external/khatru deleted from 668c41b988
116
debug-websocket.sh
Executable file
116
debug-websocket.sh
Executable 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
93
docker-compose.yml
Normal 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
89
manage-relay.sh
Executable 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.8.1
|
||||
v0.8.5
|
||||
330
readme.adoc
330
readme.adoc
@@ -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
39
stella-relay.service
Normal 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
|
||||
Reference in New Issue
Block a user