Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3afd6131d5
|
|||
|
386878fec8
|
|||
| 474e16c315 | |||
|
|
47e94c5ff6 | ||
|
|
c62fdc96d5 | ||
|
|
4c66eda10e | ||
|
|
9fdef77e02 | ||
|
e8a69077b3
|
|||
|
128bc60726
|
|||
|
6c6f9e8874
|
@@ -13,6 +13,8 @@ cmd/benchmark/reports/
|
||||
|
||||
# Go build cache and binaries
|
||||
**/bin/
|
||||
**/dist/
|
||||
**/build/
|
||||
**/*.out
|
||||
|
||||
# Allow web dist directory (needed for embedding)
|
||||
!app/web/dist/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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**
|
||||
**Updated with real-world troubleshooting solutions and latest Orly relay improvements**
|
||||
|
||||
## 🎯 **What This Solves**
|
||||
- WebSocket connection failures (`NS_ERROR_WEBSOCKET_CONNECTION_REFUSED`)
|
||||
@@ -9,24 +9,33 @@
|
||||
- Docker container proxy configuration
|
||||
- SSL certificate integration
|
||||
- Plesk configuration conflicts and virtual host precedence issues
|
||||
- **NEW**: WebSocket scheme validation errors (`expected 'ws' got 'wss'`)
|
||||
- **NEW**: Proxy-friendly relay configuration with enhanced CORS headers
|
||||
- **NEW**: Improved error handling for malformed client data
|
||||
|
||||
## 🐳 **Step 1: Deploy Your Docker Application**
|
||||
|
||||
### **For Stella's Orly Relay:**
|
||||
### **For Stella's Orly Relay (Latest Version with Proxy Improvements):**
|
||||
```bash
|
||||
# Pull and run the relay
|
||||
# Pull and run the relay with enhanced proxy support
|
||||
docker run -d \
|
||||
--name stella-relay \
|
||||
--name orly-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
|
||||
-e ORLY_ADMINS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx,npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z,npub1m4ny6hjqzepn4rxknuq94c2gpqzr29ufkkw7ttcxyak7v43n6vvsajc2jl \
|
||||
-e ORLY_BOOTSTRAP_RELAYS=wss://profiles.nostr1.com,wss://purplepag.es,wss://relay.nostr.band,wss://relay.damus.io \
|
||||
-e ORLY_RELAY_URL=wss://orly-relay.imwald.eu \
|
||||
-e ORLY_ACL_MODE=follows \
|
||||
-e ORLY_SPIDER_MODE=follows \
|
||||
-e ORLY_SPIDER_FREQUENCY=1h \
|
||||
-e ORLY_SUBSCRIPTION_ENABLED=false \
|
||||
silberengel/next-orly:latest
|
||||
|
||||
# Test the relay
|
||||
curl -I http://127.0.0.1:7777
|
||||
# Should return: HTTP/1.1 426 Upgrade Required
|
||||
# Should return: HTTP/1.1 200 OK with enhanced CORS headers
|
||||
```
|
||||
|
||||
### **For Web Apps (like Jumble):**
|
||||
@@ -253,9 +262,40 @@ sudo a2enmod proxy
|
||||
sudo a2enmod proxy_http
|
||||
sudo a2enmod proxy_wstunnel
|
||||
sudo a2enmod rewrite
|
||||
sudo a2enmod headers
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
## 🆕 **Step 4: Latest Orly Relay Improvements**
|
||||
|
||||
### **Enhanced Proxy Support**
|
||||
The latest Orly relay includes several proxy improvements:
|
||||
|
||||
1. **Flexible WebSocket Scheme Handling**: Accepts both `ws://` and `wss://` schemes for authentication
|
||||
2. **Enhanced CORS Headers**: Better compatibility with web applications
|
||||
3. **Improved Error Handling**: More robust handling of malformed client data
|
||||
4. **Proxy-Aware Logging**: Better debugging information for proxy setups
|
||||
|
||||
### **Key Environment Variables**
|
||||
```bash
|
||||
# Essential for proxy setups
|
||||
ORLY_RELAY_URL=wss://your-domain.com # Must match your public URL
|
||||
ORLY_ACL_MODE=follows # Enable follows-based access control
|
||||
ORLY_SPIDER_MODE=follows # Enable content syncing from other relays
|
||||
ORLY_SUBSCRIPTION_ENABLED=false # Disable payment requirements
|
||||
```
|
||||
|
||||
### **Testing the Enhanced Relay**
|
||||
```bash
|
||||
# Test local connectivity
|
||||
curl -I http://127.0.0.1:7777
|
||||
|
||||
# Expected response includes enhanced CORS headers:
|
||||
# Access-Control-Allow-Credentials: true
|
||||
# Access-Control-Max-Age: 86400
|
||||
# Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
|
||||
```
|
||||
|
||||
## ⚡ **Step 4: Alternative - Nginx in Plesk**
|
||||
|
||||
If Apache keeps giving issues, switch to Nginx in Plesk:
|
||||
@@ -327,13 +367,67 @@ After making changes:
|
||||
```bash
|
||||
# Essential debugging
|
||||
docker ps | grep relay # Container running?
|
||||
curl -I http://127.0.0.1:7777 # Local relay (should return 426)
|
||||
curl -I http://127.0.0.1:7777 # Local relay (should return 200 with CORS headers)
|
||||
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
|
||||
|
||||
# Check relay logs for proxy information
|
||||
docker logs relay-name | grep -i "proxy info"
|
||||
docker logs relay-name | grep -i "websocket connection"
|
||||
```
|
||||
|
||||
## 🚨 **Latest Troubleshooting Solutions**
|
||||
|
||||
### **WebSocket Scheme Validation Errors**
|
||||
**Problem**: `"HTTP Scheme incorrect: expected 'ws' got 'wss'"`
|
||||
|
||||
**Solution**: Use the latest Orly relay image with enhanced proxy support:
|
||||
```bash
|
||||
# Pull the latest image with proxy improvements
|
||||
docker pull silberengel/next-orly:latest
|
||||
|
||||
# Restart with the latest image
|
||||
docker stop orly-relay && docker rm orly-relay
|
||||
# Then run with the configuration above
|
||||
```
|
||||
|
||||
### **Malformed Client Data Errors**
|
||||
**Problem**: `"invalid hex array size, got 2 expect 64"`
|
||||
|
||||
**Solution**: These are client-side issues, not server problems. The latest relay handles them gracefully:
|
||||
- The relay now sends helpful error messages to clients
|
||||
- Malformed requests are logged but don't crash the relay
|
||||
- Normal operations continue despite client errors
|
||||
|
||||
### **Follows ACL Not Working**
|
||||
**Problem**: Only owners can write, admins can't write
|
||||
|
||||
**Solution**: Ensure proper configuration:
|
||||
```bash
|
||||
# Check ACL configuration
|
||||
docker exec orly-relay env | grep ACL
|
||||
|
||||
# Should show: ORLY_ACL_MODE=follows
|
||||
# If not, restart with explicit configuration
|
||||
```
|
||||
|
||||
### **Spider Not Syncing Content**
|
||||
**Problem**: Spider enabled but not pulling events
|
||||
|
||||
**Solution**: Check for relay lists and follow events:
|
||||
```bash
|
||||
# Check spider status
|
||||
docker logs orly-relay | grep -i spider
|
||||
|
||||
# Look for relay discovery
|
||||
docker logs orly-relay | grep -i "relay URLs"
|
||||
|
||||
# Check for follow events
|
||||
docker logs orly-relay | grep -i "kind.*3"
|
||||
```
|
||||
|
||||
### **Working Solution (Proven):**
|
||||
@@ -362,3 +456,28 @@ echo '["REQ","test",{}]' | websocat wss://domain.com/ws/ # /ws/ path
|
||||
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`
|
||||
5. **NEW**: Use the latest Orly relay image for better proxy compatibility
|
||||
6. **NEW**: Enhanced CORS headers improve web app compatibility
|
||||
7. **NEW**: Flexible WebSocket scheme handling eliminates authentication errors
|
||||
8. **NEW**: Improved error handling makes the relay more robust
|
||||
|
||||
## 🎉 **Summary of Latest Improvements**
|
||||
|
||||
### **Enhanced Proxy Support**
|
||||
- ✅ Flexible WebSocket scheme validation (accepts both `ws://` and `wss://`)
|
||||
- ✅ Enhanced CORS headers for better web app compatibility
|
||||
- ✅ Improved error handling for malformed client data
|
||||
- ✅ Proxy-aware logging for better debugging
|
||||
|
||||
### **Spider and ACL Features**
|
||||
- ✅ Follows-based access control (`ORLY_ACL_MODE=follows`)
|
||||
- ✅ Content syncing from other relays (`ORLY_SPIDER_MODE=follows`)
|
||||
- ✅ No payment requirements (`ORLY_SUBSCRIPTION_ENABLED=false`)
|
||||
|
||||
### **Production Ready**
|
||||
- ✅ Robust error handling
|
||||
- ✅ Enhanced logging and debugging
|
||||
- ✅ Better client compatibility
|
||||
- ✅ Improved proxy support
|
||||
|
||||
**The latest Orly relay is now fully optimized for proxy environments and provides a much better user experience!**
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f stella-relay
|
||||
docker-compose logs -f orly-relay
|
||||
|
||||
# Stop the relay
|
||||
docker-compose down
|
||||
@@ -136,7 +136,7 @@ go run ./cmd/stresstest -relay ws://localhost:7777
|
||||
```bash
|
||||
# Container debugging
|
||||
docker ps | grep relay
|
||||
docker logs stella-relay
|
||||
docker logs orly-relay
|
||||
curl -I http://127.0.0.1:7777 # Should return HTTP 426
|
||||
|
||||
# WebSocket testing
|
||||
@@ -153,7 +153,7 @@ grep ProxyPass /etc/apache2/plesk.conf.d/vhosts/domain.conf
|
||||
|
||||
```bash
|
||||
# View relay logs
|
||||
docker-compose logs -f stella-relay
|
||||
docker-compose logs -f orly-relay
|
||||
|
||||
# View nginx logs (if using proxy)
|
||||
docker-compose logs -f nginx
|
||||
|
||||
@@ -62,7 +62,7 @@ ENV ORLY_PORT=7777
|
||||
ENV ORLY_LOG_LEVEL=info
|
||||
ENV ORLY_MAX_CONNECTIONS=1000
|
||||
ENV ORLY_OWNERS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx
|
||||
ENV ORLY_ADMINS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx,npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z
|
||||
ENV ORLY_ADMINS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx,npub1m4ny6hjqzepn4rxknuq94c2gpqzr29ufkkw7ttcxyak7v43n6vvsajc2jl,npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z
|
||||
|
||||
# Health check to ensure relay is responding
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
|
||||
@@ -40,8 +40,9 @@ type C struct {
|
||||
Admins []string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
|
||||
Owners []string `env:"ORLY_OWNERS" usage:"comma-separated list of owner npubs, who have full control of the relay for wipe and restart and other functions"`
|
||||
ACLMode string `env:"ORLY_ACL_MODE" usage:"ACL mode: follows,none" default:"none"`
|
||||
SpiderMode string `env:"ORLY_SPIDER_MODE" usage:"spider mode: none,follow" default:"none"`
|
||||
SpiderMode string `env:"ORLY_SPIDER_MODE" usage:"spider mode: none,follows" default:"none"`
|
||||
SpiderFrequency time.Duration `env:"ORLY_SPIDER_FREQUENCY" usage:"spider frequency in seconds" default:"1h"`
|
||||
BootstrapRelays []string `env:"ORLY_BOOTSTRAP_RELAYS" usage:"comma-separated list of bootstrap relay URLs for initial sync"`
|
||||
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)"`
|
||||
@@ -225,15 +226,14 @@ func EnvKV(cfg any) (m KVSlice) {
|
||||
k := t.Field(i).Tag.Get("env")
|
||||
v := reflect.ValueOf(cfg).Field(i).Interface()
|
||||
var val string
|
||||
switch v.(type) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
val = v.(string)
|
||||
val = v
|
||||
case int, bool, time.Duration:
|
||||
val = fmt.Sprint(v)
|
||||
case []string:
|
||||
arr := v.([]string)
|
||||
if len(arr) > 0 {
|
||||
val = strings.Join(arr, ",")
|
||||
if len(v) > 0 {
|
||||
val = strings.Join(v, ",")
|
||||
}
|
||||
}
|
||||
// this can happen with embedded structs
|
||||
@@ -305,5 +305,4 @@ func PrintHelp(cfg *C, printer io.Writer) {
|
||||
fmt.Fprintf(printer, "\ncurrent configuration:\n\n")
|
||||
PrintEnv(cfg, printer)
|
||||
fmt.Fprintln(printer)
|
||||
return
|
||||
}
|
||||
|
||||
78
app/handle-count.go
Normal file
78
app/handle-count.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/log"
|
||||
"next.orly.dev/pkg/acl"
|
||||
"next.orly.dev/pkg/encoders/envelopes/authenvelope"
|
||||
"next.orly.dev/pkg/encoders/envelopes/countenvelope"
|
||||
"next.orly.dev/pkg/utils/normalize"
|
||||
)
|
||||
|
||||
// HandleCount processes a COUNT envelope by parsing the request, verifying
|
||||
// permissions, invoking the database CountEvents for each provided filter, and
|
||||
// responding with a COUNT response containing the aggregate count.
|
||||
func (l *Listener) HandleCount(msg []byte) (err error) {
|
||||
log.D.F("HandleCount: START processing from %s", l.remote)
|
||||
|
||||
// Parse the COUNT request
|
||||
env := countenvelope.New()
|
||||
if _, err = env.Unmarshal(msg); chk.E(err) {
|
||||
return normalize.Error.Errorf(err.Error())
|
||||
}
|
||||
log.D.C(func() string { return fmt.Sprintf("COUNT sub=%s filters=%d", env.Subscription, len(env.Filters)) })
|
||||
|
||||
// If ACL is active, send a challenge (same as REQ path)
|
||||
if acl.Registry.Active.Load() != "none" {
|
||||
if err = authenvelope.NewChallengeWith(l.challenge.Load()).Write(l); chk.E(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check read permissions
|
||||
accessLevel := acl.Registry.GetAccessLevel(l.authedPubkey.Load(), l.remote)
|
||||
switch accessLevel {
|
||||
case "none":
|
||||
return errors.New("auth required: user not authed or has no read access")
|
||||
default:
|
||||
// allowed to read
|
||||
}
|
||||
|
||||
// Use a bounded context for counting
|
||||
ctx, cancel := context.WithTimeout(l.ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Aggregate count across all provided filters
|
||||
var total int
|
||||
var approx bool // database returns false per implementation
|
||||
for _, f := range env.Filters {
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
var cnt int
|
||||
var a bool
|
||||
cnt, a, err = l.D.CountEvents(ctx, f)
|
||||
if chk.E(err) {
|
||||
return
|
||||
}
|
||||
total += cnt
|
||||
approx = approx || a
|
||||
}
|
||||
|
||||
// Build and send COUNT response
|
||||
var res *countenvelope.Response
|
||||
if res, err = countenvelope.NewResponseFrom(env.Subscription, total, approx); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if err = res.Write(l); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
log.D.F("HandleCount: COMPLETED processing from %s count=%d approx=%v", l.remote, total, approx)
|
||||
return nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"next.orly.dev/pkg/encoders/envelopes"
|
||||
"next.orly.dev/pkg/encoders/envelopes/authenvelope"
|
||||
"next.orly.dev/pkg/encoders/envelopes/closeenvelope"
|
||||
"next.orly.dev/pkg/encoders/envelopes/countenvelope"
|
||||
"next.orly.dev/pkg/encoders/envelopes/eventenvelope"
|
||||
"next.orly.dev/pkg/encoders/envelopes/noticeenvelope"
|
||||
"next.orly.dev/pkg/encoders/envelopes/reqenvelope"
|
||||
@@ -55,6 +56,9 @@ func (l *Listener) HandleMessage(msg []byte, remote string) {
|
||||
case authenvelope.L:
|
||||
log.D.F("%s processing AUTH envelope", remote)
|
||||
err = l.HandleAuth(rem)
|
||||
case countenvelope.L:
|
||||
log.D.F("%s processing COUNT envelope", remote)
|
||||
err = l.HandleCount(rem)
|
||||
default:
|
||||
err = fmt.Errorf("unknown envelope type %s", t)
|
||||
log.E.F("%s unknown envelope type: %s (payload: %q)", remote, t, string(rem))
|
||||
|
||||
@@ -40,6 +40,7 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
|
||||
relayinfo.RelayInformationDocument,
|
||||
relayinfo.GenericTagQueries,
|
||||
// relayinfo.NostrMarketplace,
|
||||
relayinfo.CountingResults,
|
||||
relayinfo.EventTreatment,
|
||||
relayinfo.CommandResults,
|
||||
relayinfo.ParameterizedReplaceableEvents,
|
||||
@@ -57,6 +58,7 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
|
||||
relayinfo.RelayInformationDocument,
|
||||
relayinfo.GenericTagQueries,
|
||||
// relayinfo.NostrMarketplace,
|
||||
relayinfo.CountingResults,
|
||||
relayinfo.EventTreatment,
|
||||
relayinfo.CommandResults,
|
||||
relayinfo.ParameterizedReplaceableEvents,
|
||||
@@ -67,7 +69,7 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
}
|
||||
sort.Sort(supportedNIPs)
|
||||
log.T.Ln("supported NIPs", supportedNIPs)
|
||||
log.I.Ln("supported NIPs", supportedNIPs)
|
||||
// Construct description with dashboard URL
|
||||
dashboardURL := s.DashboardURL(r)
|
||||
description := version.Description + " dashboard: " + dashboardURL
|
||||
|
||||
@@ -64,7 +64,7 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
|
||||
l.ctx, 30*time.Second,
|
||||
)
|
||||
defer queryCancel()
|
||||
|
||||
|
||||
// Collect all events from all filters
|
||||
var allEvents event.S
|
||||
for _, f := range *env.Filters {
|
||||
|
||||
@@ -38,7 +38,9 @@ const (
|
||||
|
||||
func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
remote := GetRemoteFromReq(r)
|
||||
log.T.F("handling websocket connection from %s", remote)
|
||||
|
||||
// Log comprehensive proxy information for debugging
|
||||
LogProxyInfo(r, "WebSocket connection from "+remote)
|
||||
if len(s.Config.IPWhitelist) > 0 {
|
||||
for _, ip := range s.Config.IPWhitelist {
|
||||
log.T.F("checking IP whitelist: %s", ip)
|
||||
@@ -55,9 +57,14 @@ whitelist:
|
||||
defer cancel()
|
||||
var err error
|
||||
var conn *websocket.Conn
|
||||
if conn, err = websocket.Accept(
|
||||
w, r, &websocket.AcceptOptions{OriginPatterns: []string{"*"}},
|
||||
); chk.E(err) {
|
||||
// Configure WebSocket accept options for proxy compatibility
|
||||
acceptOptions := &websocket.AcceptOptions{
|
||||
OriginPatterns: []string{"*"}, // Allow all origins for proxy compatibility
|
||||
// Don't check origin when behind a proxy - let the proxy handle it
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
if conn, err = websocket.Accept(w, r, acceptOptions); chk.E(err) {
|
||||
log.E.F("websocket accept failed from %s: %v", remote, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package app
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"lol.mleku.dev/log"
|
||||
)
|
||||
|
||||
// GetRemoteFromReq retrieves the originating IP address of the client from
|
||||
@@ -67,3 +69,28 @@ func GetRemoteFromReq(r *http.Request) (rr string) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LogProxyInfo logs comprehensive proxy information for debugging
|
||||
func LogProxyInfo(r *http.Request, prefix string) {
|
||||
proxyHeaders := map[string]string{
|
||||
"X-Forwarded-For": r.Header.Get("X-Forwarded-For"),
|
||||
"X-Real-IP": r.Header.Get("X-Real-IP"),
|
||||
"X-Forwarded-Proto": r.Header.Get("X-Forwarded-Proto"),
|
||||
"X-Forwarded-Host": r.Header.Get("X-Forwarded-Host"),
|
||||
"X-Forwarded-Port": r.Header.Get("X-Forwarded-Port"),
|
||||
"Forwarded": r.Header.Get("Forwarded"),
|
||||
"Host": r.Header.Get("Host"),
|
||||
"User-Agent": r.Header.Get("User-Agent"),
|
||||
}
|
||||
|
||||
var info []string
|
||||
for header, value := range proxyHeaders {
|
||||
if value != "" {
|
||||
info = append(info, header+":"+value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(info) > 0 {
|
||||
log.T.F("%s proxy info: %s", prefix, strings.Join(info, " "))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,17 +40,24 @@ type Server struct {
|
||||
// Challenge storage for HTTP UI authentication
|
||||
challengeMutex sync.RWMutex
|
||||
challenges map[string][]byte
|
||||
|
||||
|
||||
paymentProcessor *PaymentProcessor
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Set CORS headers for all responses
|
||||
// Set comprehensive CORS headers for proxy compatibility
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set(
|
||||
"Access-Control-Allow-Headers", "Content-Type, Authorization",
|
||||
)
|
||||
w.Header().Set("Access-Control-Allow-Headers",
|
||||
"Origin, X-Requested-With, Content-Type, Accept, Authorization, "+
|
||||
"X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, X-Real-IP, "+
|
||||
"Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, "+
|
||||
"Sec-WebSocket-Protocol, Sec-WebSocket-Extensions")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
|
||||
// Add proxy-friendly headers
|
||||
w.Header().Set("Vary", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers")
|
||||
|
||||
// Handle preflight OPTIONS requests
|
||||
if r.Method == "OPTIONS" {
|
||||
@@ -58,6 +65,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Log proxy information for debugging (only for WebSocket requests to avoid spam)
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
LogProxyInfo(r, "HTTP request")
|
||||
}
|
||||
|
||||
// If this is a websocket request, only intercept the relay root path.
|
||||
// This allows other websocket paths (e.g., Vite HMR) to be handled by the dev proxy when enabled.
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
@@ -83,13 +95,30 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) ServiceURL(req *http.Request) (st string) {
|
||||
// Get host from various proxy headers
|
||||
host := req.Header.Get("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = req.Header.Get("Host")
|
||||
}
|
||||
if host == "" {
|
||||
host = req.Host
|
||||
}
|
||||
|
||||
// Get protocol from various proxy headers
|
||||
proto := req.Header.Get("X-Forwarded-Proto")
|
||||
if proto == "" {
|
||||
if host == "localhost" {
|
||||
proto = req.Header.Get("X-Forwarded-Scheme")
|
||||
}
|
||||
if proto == "" {
|
||||
// Check if we're behind a proxy by looking for common proxy headers
|
||||
hasProxyHeaders := req.Header.Get("X-Forwarded-For") != "" ||
|
||||
req.Header.Get("X-Real-IP") != "" ||
|
||||
req.Header.Get("Forwarded") != ""
|
||||
|
||||
if hasProxyHeaders {
|
||||
// If we have proxy headers, assume HTTPS/WSS
|
||||
proto = "wss"
|
||||
} else if host == "localhost" {
|
||||
proto = "ws"
|
||||
} else if strings.Contains(host, ":") {
|
||||
// has a port number
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-json-pretty": "^2.2.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest",
|
||||
@@ -25,10 +26,18 @@
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
||||
|
||||
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"react-json-pretty": ["react-json-pretty@2.2.0", "", { "dependencies": { "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=15.0", "react-dom": ">=15.0" } }, "sha512-3UMzlAXkJ4R8S4vmkRKtvJHTewG4/rn1Q18n0zqdu/ipZbUPLVZD+QwC7uVcD/IAY3s8iNVHlgR2dMzIUS0n1A=="],
|
||||
|
||||
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
||||
|
||||
160
app/web/dist/index-4xsq3yxw.js
vendored
160
app/web/dist/index-4xsq3yxw.js
vendored
File diff suppressed because one or more lines are too long
161
app/web/dist/index-kk1m7jg4.js
vendored
Normal file
161
app/web/dist/index-kk1m7jg4.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
app/web/dist/index.html
vendored
2
app/web/dist/index.html
vendored
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nostr Relay</title>
|
||||
|
||||
<link rel="stylesheet" crossorigin href="./index-q4cwd1fy.css"><script type="module" crossorigin src="./index-4xsq3yxw.js"></script></head>
|
||||
<link rel="stylesheet" crossorigin href="./index-q4cwd1fy.css"><script type="module" crossorigin src="./index-kk1m7jg4.js"></script></head>
|
||||
<body>
|
||||
<script>
|
||||
// Apply system theme preference immediately to avoid flash of wrong theme
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-json-pretty": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
function PrettyJSONView({ jsonString, maxHeightClass = 'max-h-64' }) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(jsonString);
|
||||
} catch (_) {
|
||||
data = jsonString;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`text-xs p-2 rounded overflow-auto ${maxHeightClass} break-all break-words whitespace-pre-wrap bg-gray-950 text-white`}
|
||||
style={{ overflowWrap: 'anywhere', wordBreak: 'break-word' }}
|
||||
>
|
||||
<JSONPretty data={data} space={2} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
@@ -1195,11 +1213,22 @@ function App() {
|
||||
|
||||
function copyEventJSON(eventJSON) {
|
||||
try {
|
||||
navigator.clipboard.writeText(eventJSON);
|
||||
// Ensure minified JSON is copied regardless of input format
|
||||
let toCopy = eventJSON;
|
||||
try {
|
||||
toCopy = JSON.stringify(JSON.parse(eventJSON));
|
||||
} catch (_) {
|
||||
// if not valid JSON string, fall back to original
|
||||
}
|
||||
navigator.clipboard.writeText(toCopy);
|
||||
} catch (error) {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = eventJSON;
|
||||
let toCopy = eventJSON;
|
||||
try {
|
||||
toCopy = JSON.stringify(JSON.parse(eventJSON));
|
||||
} catch (_) {}
|
||||
textArea.value = toCopy;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
@@ -1910,7 +1939,7 @@ function App() {
|
||||
Copy JSON
|
||||
</button>
|
||||
</div>
|
||||
<pre className={`text-xs overflow-auto max-h-64 ${getThemeClasses('bg-white text-black', 'bg-gray-950 text-gray-200')} p-2 rounded`}>{event.raw_json}</pre>
|
||||
<PrettyJSONView jsonString={event.raw_json} maxHeightClass="max-h-64" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2048,9 +2077,7 @@ function App() {
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre className={`text-xs p-2 rounded overflow-auto max-h-40 break-all whitespace-pre-wrap ${getPanelBgClass()} ${getTextClass()}`}>
|
||||
{JSON.stringify(JSON.parse(event.raw_json), null, 2)}
|
||||
</pre>
|
||||
<PrettyJSONView jsonString={event.raw_json} maxHeightClass="max-h-40" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2197,9 +2224,7 @@ function App() {
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre className={`text-xs p-2 rounded overflow-auto max-h-40 break-all whitespace-pre-wrap ${getPanelBgClass()} ${getTextClass()}`}>
|
||||
{JSON.stringify(JSON.parse(event.raw_json), null, 2)}
|
||||
</pre>
|
||||
<PrettyJSONView jsonString={event.raw_json} maxHeightClass="max-h-40" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# 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
|
||||
orly-relay:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: silberengel/next-orly:latest
|
||||
container_name: orly-relay
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:7777:7777"
|
||||
@@ -19,21 +20,23 @@ services:
|
||||
- ORLY_LISTEN=0.0.0.0
|
||||
- ORLY_PORT=7777
|
||||
- ORLY_LOG_LEVEL=info
|
||||
- ORLY_MAX_CONNECTIONS=1000
|
||||
- ORLY_DB_LOG_LEVEL=error
|
||||
- ORLY_OWNERS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx
|
||||
- ORLY_ADMINS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx,npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z
|
||||
- ORLY_ADMINS=npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx,npub1m4ny6hjqzepn4rxknuq94c2gpqzr29ufkkw7ttcxyak7v43n6vvsajc2jl,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
|
||||
# ACL and Spider Configuration
|
||||
- ORLY_ACL_MODE=follows
|
||||
- ORLY_SPIDER_MODE=follows
|
||||
|
||||
# Database Settings
|
||||
- BADGER_LOG_LEVEL=ERROR
|
||||
- BADGER_SYNC_WRITES=false # Better performance, slightly less durability
|
||||
# Bootstrap relay URLs for initial sync
|
||||
- ORLY_BOOTSTRAP_RELAYS=wss://profiles.nostr1.com,wss://purplepag.es,wss://relay.nostr.band,wss://relay.damus.io
|
||||
|
||||
# Security Settings
|
||||
- ORLY_REQUIRE_AUTH=false
|
||||
# Subscription Settings (optional)
|
||||
- ORLY_SUBSCRIPTION_ENABLED=false
|
||||
- ORLY_MONTHLY_PRICE_SATS=0
|
||||
|
||||
# Performance Settings
|
||||
- ORLY_MAX_CONNECTIONS=1000
|
||||
- ORLY_MAX_EVENT_SIZE=65536
|
||||
- ORLY_MAX_SUBSCRIPTIONS=20
|
||||
|
||||
@@ -74,7 +77,7 @@ services:
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
- nginx_logs:/var/log/nginx
|
||||
depends_on:
|
||||
- stella-relay
|
||||
- orly-relay
|
||||
profiles:
|
||||
- proxy # Only start with: docker-compose --profile proxy up
|
||||
|
||||
@@ -90,4 +93,4 @@ volumes:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: stella-relay-network
|
||||
name: orly-relay-network
|
||||
|
||||
117
manage-relay.sh
117
manage-relay.sh
@@ -1,42 +1,57 @@
|
||||
#!/bin/bash
|
||||
# Stella's Orly Relay Management Script
|
||||
# Uses docker-compose.yml directly for configuration
|
||||
|
||||
set -e
|
||||
|
||||
RELAY_SERVICE="stella-relay"
|
||||
# Get script directory and project root
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$SCRIPT_DIR"
|
||||
|
||||
# Configuration from docker-compose.yml
|
||||
RELAY_SERVICE="orly-relay"
|
||||
CONTAINER_NAME="orly-nostr-relay"
|
||||
RELAY_URL="ws://127.0.0.1:7777"
|
||||
HTTP_URL="http://127.0.0.1:7777"
|
||||
RELAY_DATA_DIR="/home/madmin/.local/share/orly-relay"
|
||||
|
||||
# Change to project directory for docker-compose commands
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
case "${1:-}" in
|
||||
"start")
|
||||
echo "🚀 Starting Stella's Orly Relay..."
|
||||
sudo systemctl start $RELAY_SERVICE
|
||||
docker compose up -d orly-relay
|
||||
echo "✅ Relay started!"
|
||||
;;
|
||||
"stop")
|
||||
echo "⏹️ Stopping Stella's Orly Relay..."
|
||||
sudo systemctl stop $RELAY_SERVICE
|
||||
docker compose down
|
||||
echo "✅ Relay stopped!"
|
||||
;;
|
||||
"restart")
|
||||
echo "🔄 Restarting Stella's Orly Relay..."
|
||||
sudo systemctl restart $RELAY_SERVICE
|
||||
docker compose restart orly-relay
|
||||
echo "✅ Relay restarted!"
|
||||
;;
|
||||
"status")
|
||||
echo "📊 Stella's Orly Relay Status:"
|
||||
sudo systemctl status $RELAY_SERVICE --no-pager
|
||||
docker compose ps orly-relay
|
||||
;;
|
||||
"logs")
|
||||
echo "📜 Stella's Orly Relay Logs:"
|
||||
sudo journalctl -u $RELAY_SERVICE -f --no-pager
|
||||
docker compose logs -f orly-relay
|
||||
;;
|
||||
"test")
|
||||
echo "🧪 Testing relay connection..."
|
||||
if curl -s -I http://127.0.0.1:7777 | grep -q "426 Upgrade Required"; then
|
||||
if curl -s -I "$HTTP_URL" | grep -q "426 Upgrade Required"; then
|
||||
echo "✅ Relay is responding correctly!"
|
||||
echo "📡 WebSocket URL: $RELAY_URL"
|
||||
echo "🌐 HTTP URL: $HTTP_URL"
|
||||
else
|
||||
echo "❌ Relay is not responding correctly"
|
||||
echo " Expected: 426 Upgrade Required"
|
||||
echo " URL: $HTTP_URL"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
@@ -53,14 +68,53 @@ case "${1:-}" in
|
||||
"info")
|
||||
echo "📋 Stella's Orly Relay Information:"
|
||||
echo " Service: $RELAY_SERVICE"
|
||||
echo " Container: $CONTAINER_NAME"
|
||||
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 " HTTP URL: $HTTP_URL"
|
||||
echo " Data Directory: $RELAY_DATA_DIR"
|
||||
echo " Config Directory: $PROJECT_DIR"
|
||||
echo ""
|
||||
echo "🔑 Admin NPubs:"
|
||||
echo " Stella: npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx"
|
||||
echo " Admin2: npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z"
|
||||
echo "🐳 Docker Information:"
|
||||
echo " Compose File: $PROJECT_DIR/docker-compose.yml"
|
||||
echo " Container Status:"
|
||||
docker compose ps orly-relay 2>/dev/null || echo " Not running"
|
||||
echo ""
|
||||
echo "💡 Configuration:"
|
||||
echo " All settings are defined in docker-compose.yml"
|
||||
echo " Use 'docker compose config' to see parsed configuration"
|
||||
;;
|
||||
"docker-logs")
|
||||
echo "🐳 Docker Container Logs:"
|
||||
docker compose logs -f orly-relay 2>/dev/null || echo "❌ Container not found or not running"
|
||||
;;
|
||||
"docker-status")
|
||||
echo "🐳 Docker Container Status:"
|
||||
docker compose ps orly-relay
|
||||
;;
|
||||
"docker-restart")
|
||||
echo "🔄 Restarting Docker Container..."
|
||||
docker compose restart orly-relay
|
||||
echo "✅ Container restarted!"
|
||||
;;
|
||||
"docker-update")
|
||||
echo "🔄 Updating and restarting Docker Container..."
|
||||
docker compose pull orly-relay
|
||||
docker compose up -d orly-relay
|
||||
echo "✅ Container updated and restarted!"
|
||||
;;
|
||||
"docker-build")
|
||||
echo "🔨 Building Docker Container..."
|
||||
docker compose build orly-relay
|
||||
echo "✅ Container built!"
|
||||
;;
|
||||
"docker-down")
|
||||
echo "⏹️ Stopping Docker Container..."
|
||||
docker compose down
|
||||
echo "✅ Container stopped!"
|
||||
;;
|
||||
"docker-config")
|
||||
echo "📋 Docker Compose Configuration:"
|
||||
docker compose config
|
||||
;;
|
||||
*)
|
||||
echo "🌲 Stella's Orly Relay Management Script"
|
||||
@@ -68,21 +122,32 @@ case "${1:-}" in
|
||||
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 " 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 "Docker Commands:"
|
||||
echo " docker-logs Show Docker container logs"
|
||||
echo " docker-status Show Docker container status"
|
||||
echo " docker-restart Restart Docker container only"
|
||||
echo " docker-update Update and restart container"
|
||||
echo " docker-build Build Docker container"
|
||||
echo " docker-down Stop Docker container"
|
||||
echo " docker-config Show Docker Compose configuration"
|
||||
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 " $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 " $0 docker-logs # Watch Docker container logs"
|
||||
echo " $0 docker-update # Update and restart container"
|
||||
echo ""
|
||||
echo "🌲 Crafted in the digital forest by Stella ✨"
|
||||
;;
|
||||
|
||||
@@ -3,6 +3,8 @@ package acl
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -22,9 +24,9 @@ import (
|
||||
"next.orly.dev/pkg/encoders/envelopes/reqenvelope"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/filter"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/encoders/kind"
|
||||
"next.orly.dev/pkg/encoders/tag"
|
||||
"next.orly.dev/pkg/encoders/timestamp"
|
||||
"next.orly.dev/pkg/protocol/publish"
|
||||
"next.orly.dev/pkg/utils"
|
||||
"next.orly.dev/pkg/utils/normalize"
|
||||
@@ -108,7 +110,7 @@ func (f *Follows) Configure(cfg ...any) (err error) {
|
||||
for _, v := range ev.Tags.GetAll([]byte("p")) {
|
||||
// log.I.F("adding follow: %s", v.Value())
|
||||
var a []byte
|
||||
if b, e := hex.Dec(string(v.Value())); chk.E(e) {
|
||||
if b, e := hex.DecodeString(string(v.Value())); chk.E(e) {
|
||||
continue
|
||||
} else {
|
||||
a = b
|
||||
@@ -158,6 +160,8 @@ func (f *Follows) adminRelays() (urls []string) {
|
||||
copy(admins, f.admins)
|
||||
f.followsMx.RUnlock()
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
// First, try to get relay URLs from admin kind 10002 events
|
||||
for _, adm := range admins {
|
||||
fl := &filter.F{
|
||||
Authors: tag.NewFromAny(adm),
|
||||
@@ -194,6 +198,29 @@ func (f *Follows) adminRelays() (urls []string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no admin relays found, use bootstrap relays as fallback
|
||||
if len(urls) == 0 {
|
||||
log.I.F("no admin relays found in DB, checking bootstrap relays")
|
||||
if len(f.cfg.BootstrapRelays) > 0 {
|
||||
log.I.F("using bootstrap relays: %v", f.cfg.BootstrapRelays)
|
||||
for _, relay := range f.cfg.BootstrapRelays {
|
||||
n := string(normalize.URL(relay))
|
||||
if n == "" {
|
||||
log.W.F("invalid bootstrap relay URL: %s", relay)
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[n]; ok {
|
||||
continue
|
||||
}
|
||||
seen[n] = struct{}{}
|
||||
urls = append(urls, n)
|
||||
}
|
||||
} else {
|
||||
log.W.F("no bootstrap relays configured")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -211,7 +238,7 @@ func (f *Follows) startSubscriptions(ctx context.Context) {
|
||||
urls := f.adminRelays()
|
||||
log.I.S(urls)
|
||||
if len(urls) == 0 {
|
||||
log.W.F("follows syncer: no admin relays found in DB (kind 10002)")
|
||||
log.W.F("follows syncer: no admin relays found in DB (kind 10002) and no bootstrap relays configured")
|
||||
return
|
||||
}
|
||||
log.T.F(
|
||||
@@ -228,18 +255,45 @@ func (f *Follows) startSubscriptions(ctx context.Context) {
|
||||
return
|
||||
default:
|
||||
}
|
||||
c, _, err := websocket.Dial(ctx, u, nil)
|
||||
// Create a timeout context for the connection
|
||||
connCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
|
||||
// Create proper headers for the WebSocket connection
|
||||
headers := http.Header{}
|
||||
headers.Set("User-Agent", "ORLY-Relay/0.9.2")
|
||||
headers.Set("Origin", "https://orly.dev")
|
||||
|
||||
// Use proper WebSocket dial options
|
||||
dialOptions := &websocket.DialOptions{
|
||||
HTTPHeader: headers,
|
||||
}
|
||||
|
||||
c, _, err := websocket.Dial(connCtx, u, dialOptions)
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.W.F("follows syncer: dial %s failed: %v", u, err)
|
||||
if strings.Contains(
|
||||
err.Error(), "response status code 101 but got 403",
|
||||
) {
|
||||
// 403 means the relay is not accepting connections from
|
||||
// us. Forbidden is the meaning, usually used to
|
||||
// indicate either the IP or user is blocked. so stop
|
||||
// trying this one.
|
||||
return
|
||||
|
||||
// Handle different types of errors
|
||||
if strings.Contains(err.Error(), "response status code 101 but got 403") {
|
||||
// 403 means the relay is not accepting connections from us
|
||||
// Forbidden is the meaning, usually used to indicate either the IP or user is blocked
|
||||
// But we should still retry after a longer delay
|
||||
log.W.F("follows syncer: relay %s returned 403, will retry after longer delay", u)
|
||||
timer := time.NewTimer(5 * time.Minute) // Wait 5 minutes before retrying 403 errors
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
continue
|
||||
} else if strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "connection refused") {
|
||||
// Network issues, retry with normal backoff
|
||||
log.W.F("follows syncer: network issue with %s, retrying in %v", u, backoff)
|
||||
} else {
|
||||
// Other errors, retry with normal backoff
|
||||
log.W.F("follows syncer: connection error with %s, retrying in %v", u, backoff)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(backoff)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -252,21 +306,37 @@ func (f *Follows) startSubscriptions(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
backoff = time.Second
|
||||
// send REQ
|
||||
log.I.F("follows syncer: successfully connected to %s", u)
|
||||
|
||||
// send REQ for kind 3 (follow lists), kind 10002 (relay lists), and all events from follows
|
||||
ff := &filter.S{}
|
||||
f1 := &filter.F{
|
||||
Authors: tag.NewFromBytesSlice(authors...),
|
||||
Limit: values.ToUintPointer(0),
|
||||
Kinds: kind.NewS(kind.New(kind.FollowList.K)),
|
||||
Limit: values.ToUintPointer(100),
|
||||
}
|
||||
*ff = append(*ff, f1)
|
||||
f2 := &filter.F{
|
||||
Authors: tag.NewFromBytesSlice(authors...),
|
||||
Kinds: kind.NewS(kind.New(kind.RelayListMetadata.K)),
|
||||
Limit: values.ToUintPointer(100),
|
||||
}
|
||||
// Add filter for all events from follows (last 30 days)
|
||||
oneMonthAgo := timestamp.FromUnix(time.Now().Add(-30 * 24 * time.Hour).Unix())
|
||||
f3 := &filter.F{
|
||||
Authors: tag.NewFromBytesSlice(authors...),
|
||||
Since: oneMonthAgo,
|
||||
Limit: values.ToUintPointer(1000),
|
||||
}
|
||||
*ff = append(*ff, f1, f2, f3)
|
||||
req := reqenvelope.NewFrom([]byte("follows-sync"), ff)
|
||||
if err = c.Write(
|
||||
ctx, websocket.MessageText, req.Marshal(nil),
|
||||
); chk.E(err) {
|
||||
log.W.F("follows syncer: failed to send REQ to %s: %v", u, err)
|
||||
_ = c.Close(websocket.StatusInternalError, "write failed")
|
||||
continue
|
||||
}
|
||||
log.T.F("sent REQ to %s for follows subscription", u)
|
||||
log.I.F("follows syncer: sent REQ to %s for kind 3, 10002, and all events (last 30 days) from followed users", u)
|
||||
// read loop
|
||||
for {
|
||||
select {
|
||||
@@ -294,6 +364,23 @@ func (f *Follows) startSubscriptions(ctx context.Context) {
|
||||
if ok, err := res.Event.Verify(); chk.T(err) || !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process events based on kind
|
||||
switch res.Event.Kind {
|
||||
case kind.FollowList.K:
|
||||
log.I.F("follows syncer: received kind 3 (follow list) event from %s on relay %s",
|
||||
hex.EncodeToString(res.Event.Pubkey), u)
|
||||
// Extract followed pubkeys from 'p' tags in kind 3 events
|
||||
f.extractFollowedPubkeys(res.Event)
|
||||
case kind.RelayListMetadata.K:
|
||||
log.I.F("follows syncer: received kind 10002 (relay list) event from %s on relay %s",
|
||||
hex.EncodeToString(res.Event.Pubkey), u)
|
||||
default:
|
||||
// Log all other events from followed users
|
||||
log.I.F("follows syncer: received kind %d event from %s on relay %s",
|
||||
res.Event.Kind, hex.EncodeToString(res.Event.Pubkey), u)
|
||||
}
|
||||
|
||||
if _, _, err = f.D.SaveEvent(
|
||||
ctx, res.Event,
|
||||
); err != nil {
|
||||
@@ -365,12 +452,26 @@ func (f *Follows) Syncer() {
|
||||
func (f *Follows) GetFollowedPubkeys() [][]byte {
|
||||
f.followsMx.RLock()
|
||||
defer f.followsMx.RUnlock()
|
||||
|
||||
|
||||
followedPubkeys := make([][]byte, len(f.follows))
|
||||
copy(followedPubkeys, f.follows)
|
||||
return followedPubkeys
|
||||
}
|
||||
|
||||
// extractFollowedPubkeys extracts followed pubkeys from 'p' tags in kind 3 events
|
||||
func (f *Follows) extractFollowedPubkeys(event *event.E) {
|
||||
if event.Kind != kind.FollowList.K {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract all 'p' tags (followed pubkeys) from the kind 3 event
|
||||
for _, tag := range event.Tags.GetAll([]byte("p")) {
|
||||
if len(tag.Value()) == 32 { // Valid pubkey length
|
||||
f.AddFollow(tag.Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddFollow appends a pubkey to the in-memory follows list if not already present
|
||||
// and signals the syncer to refresh subscriptions.
|
||||
func (f *Follows) AddFollow(pub []byte) {
|
||||
@@ -387,6 +488,7 @@ func (f *Follows) AddFollow(pub []byte) {
|
||||
b := make([]byte, len(pub))
|
||||
copy(b, pub)
|
||||
f.follows = append(f.follows, b)
|
||||
log.I.F("follows syncer: added new followed pubkey: %s", hex.EncodeToString(pub))
|
||||
// notify syncer if initialized
|
||||
if f.updated != nil {
|
||||
select {
|
||||
|
||||
44
pkg/database/count.go
Normal file
44
pkg/database/count.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"next.orly.dev/pkg/encoders/filter"
|
||||
)
|
||||
|
||||
// CountEvents mirrors the initial selection logic of QueryEvents but stops
|
||||
// once we have identified candidate event serials (id/pk/ts). It returns the
|
||||
// count of those serials. The `approx` flag is always false as requested.
|
||||
func (d *D) CountEvents(c context.Context, f *filter.F) (
|
||||
count int, approx bool, err error,
|
||||
) {
|
||||
approx = false
|
||||
if f == nil {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
// If explicit Ids are provided, count how many of them resolve to serials.
|
||||
if f.Ids != nil && f.Ids.Len() > 0 {
|
||||
var serials map[string]interface{}
|
||||
// Use type inference without importing extra packages by discarding the
|
||||
// concrete value type via a two-step assignment.
|
||||
if tmp, idErr := d.GetSerialsByIds(f.Ids); idErr != nil {
|
||||
return 0, false, idErr
|
||||
} else {
|
||||
// Reassign to a map with empty interface values to avoid referencing
|
||||
// the concrete Uint40 type here.
|
||||
serials = make(map[string]interface{}, len(tmp))
|
||||
for k := range tmp {
|
||||
serials[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
return len(serials), false, nil
|
||||
}
|
||||
|
||||
// Otherwise, query for candidate Id/Pubkey/Timestamp triplets and count them.
|
||||
if idPkTs, qErr := d.QueryForIds(c, f); qErr != nil {
|
||||
return 0, false, qErr
|
||||
} else {
|
||||
return len(idPkTs), false, nil
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"next.orly.dev/pkg/database/indexes/types"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/filter"
|
||||
"next.orly.dev/pkg/interfaces/store"
|
||||
)
|
||||
@@ -59,7 +60,58 @@ func (d *D) QueryForIds(c context.Context, f *filter.F) (
|
||||
idPkTs = append(idPkTs, idpk)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If search is combined with Authors/Kinds/Tags, require events to match ALL of those present fields in addition to the word match.
|
||||
if len(f.Search) > 0 && ((f.Authors != nil && f.Authors.Len() > 0) || (f.Kinds != nil && f.Kinds.Len() > 0) || (f.Tags != nil && f.Tags.Len() > 0)) {
|
||||
// Build serial list for fetching full events
|
||||
serials := make([]*types.Uint40, 0, len(idPkTs))
|
||||
for _, v := range idPkTs {
|
||||
s := new(types.Uint40)
|
||||
s.Set(v.Ser)
|
||||
serials = append(serials, s)
|
||||
}
|
||||
var evs map[uint64]*event.E
|
||||
if evs, err = d.FetchEventsBySerials(serials); chk.E(err) {
|
||||
return
|
||||
}
|
||||
filtered := make([]*store.IdPkTs, 0, len(idPkTs))
|
||||
for _, v := range idPkTs {
|
||||
ev, ok := evs[v.Ser]
|
||||
if !ok || ev == nil {
|
||||
continue
|
||||
}
|
||||
matchesAll := true
|
||||
if f.Authors != nil && f.Authors.Len() > 0 && !f.Authors.Contains(ev.Pubkey) {
|
||||
matchesAll = false
|
||||
}
|
||||
if matchesAll && f.Kinds != nil && f.Kinds.Len() > 0 && !f.Kinds.Contains(ev.Kind) {
|
||||
matchesAll = false
|
||||
}
|
||||
if matchesAll && f.Tags != nil && f.Tags.Len() > 0 {
|
||||
// Require the event to satisfy all tag filters as in MatchesIgnoringTimestampConstraints
|
||||
tagOK := true
|
||||
for _, t := range *f.Tags {
|
||||
if t.Len() < 2 {
|
||||
continue
|
||||
}
|
||||
key := t.Key()
|
||||
values := t.T[1:]
|
||||
if !ev.Tags.ContainsAny(key, values) {
|
||||
tagOK = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !tagOK {
|
||||
matchesAll = false
|
||||
}
|
||||
}
|
||||
if matchesAll {
|
||||
filtered = append(filtered, v)
|
||||
}
|
||||
}
|
||||
idPkTs = filtered
|
||||
}
|
||||
|
||||
if len(f.Search) == 0 {
|
||||
// No search query: sort by timestamp in reverse chronological order
|
||||
sort.Slice(
|
||||
|
||||
@@ -91,12 +91,22 @@ func Validate(evt *event.E, challenge []byte, relayURL string) (
|
||||
err = errorf.E("error parsing relay url: %s", err)
|
||||
return
|
||||
}
|
||||
// Allow both ws:// and wss:// schemes when behind a reverse proxy
|
||||
// This handles cases where the relay expects ws:// but receives wss:// from clients
|
||||
// connecting through HTTPS proxies
|
||||
if expected.Scheme != found.Scheme {
|
||||
err = errorf.E(
|
||||
"HTTP Scheme incorrect: expected '%s' got '%s",
|
||||
expected.Scheme, found.Scheme,
|
||||
)
|
||||
return
|
||||
// Check if this is a ws/wss scheme mismatch (acceptable behind proxy)
|
||||
if (expected.Scheme == "ws" && found.Scheme == "wss") ||
|
||||
(expected.Scheme == "wss" && found.Scheme == "ws") {
|
||||
// This is acceptable when behind a reverse proxy
|
||||
// The client will always send wss:// when connecting through HTTPS
|
||||
} else {
|
||||
err = errorf.E(
|
||||
"HTTP Scheme incorrect: expected '%s' got '%s",
|
||||
expected.Scheme, found.Scheme,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
if expected.Host != found.Host {
|
||||
err = errorf.E(
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.9.1
|
||||
v0.10.0
|
||||
@@ -303,11 +303,11 @@ The spider operates in two phases:
|
||||
|
||||
=== configuration
|
||||
|
||||
Enable the spider by setting the spider mode to "follow":
|
||||
Enable the spider by setting the spider mode to "follows":
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
export ORLY_SPIDER_MODE=follow
|
||||
export ORLY_SPIDER_MODE=follows
|
||||
export ORLY_SPIDER_FREQUENCY=1h
|
||||
----
|
||||
|
||||
@@ -322,7 +322,7 @@ Configuration options:
|
||||
----
|
||||
# Enable both follows ACL and spider sync
|
||||
export ORLY_ACL_MODE=follows
|
||||
export ORLY_SPIDER_MODE=follow
|
||||
export ORLY_SPIDER_MODE=follows
|
||||
export ORLY_SPIDER_FREQUENCY=30m
|
||||
export ORLY_ADMINS=npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku
|
||||
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
[Unit]
|
||||
Description=Stella's Orly Nostr Relay
|
||||
Description=Stella's Orly Nostr Relay (Docker Compose)
|
||||
Documentation=https://github.com/Silberengel/next.orly.dev
|
||||
After=network-online.target
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
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
|
||||
# Start the relay using docker compose
|
||||
ExecStart=/usr/bin/docker compose up -d orly-relay
|
||||
|
||||
# Stop the relay
|
||||
ExecStop=/usr/bin/docker compose down
|
||||
|
||||
# Reload configuration (restart containers)
|
||||
ExecReload=/usr/bin/docker compose restart orly-relay
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=true
|
||||
@@ -35,5 +32,11 @@ ReadWritePaths=/home/madmin/Projects/GitCitadel/next.orly.dev/data
|
||||
LimitNOFILE=65536
|
||||
LimitNPROC=4096
|
||||
|
||||
# Restart policy
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
TimeoutStartSec=60
|
||||
TimeoutStopSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Reference in New Issue
Block a user