Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f5d13a6807
|
|||
|
a735bd3d5e
|
|||
|
0a32cc3125
|
|||
|
7906bb2295
|
|||
|
50a8b39ea3
|
|||
|
45cfd04214
|
|||
|
ced06a9175
|
11
.github/workflows/go.yml
vendored
11
.github/workflows/go.yml
vendored
@@ -81,15 +81,8 @@ jobs:
|
||||
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-darwin-arm64 .
|
||||
GOEXPERIMENT=greenteagc,jsonv2 GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-windows-amd64.exe .
|
||||
|
||||
# Build cmd executables
|
||||
for cmd in lerproxy nauth nurl vainstr walletcli; do
|
||||
echo "Building $cmd"
|
||||
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o release-binaries/${cmd}-${VERSION}-linux-amd64 ./cmd/${cmd}
|
||||
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-linux-arm64 ./cmd/${cmd}
|
||||
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-darwin-amd64 ./cmd/${cmd}
|
||||
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-darwin-arm64 ./cmd/${cmd}
|
||||
GOEXPERIMENT=greenteagc,jsonv2 GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-windows-amd64.exe ./cmd/${cmd}
|
||||
done
|
||||
# Note: Only building orly binary as requested
|
||||
# Other cmd utilities (aggregator, benchmark, convert, policytest, stresstest) are development tools
|
||||
|
||||
# Create checksums
|
||||
cd release-binaries
|
||||
|
||||
@@ -37,7 +37,6 @@ type C struct {
|
||||
Pprof string `env:"ORLY_PPROF" usage:"enable pprof in modes: cpu,memory,allocation,heap,block,goroutine,threadcreate,mutex"`
|
||||
PprofPath string `env:"ORLY_PPROF_PATH" usage:"optional directory to write pprof profiles into (inside container); default is temporary dir"`
|
||||
PprofHTTP bool `env:"ORLY_PPROF_HTTP" default:"false" usage:"if true, expose net/http/pprof on port 6060"`
|
||||
OpenPprofWeb bool `env:"ORLY_OPEN_PPROF_WEB" default:"false" usage:"if true, automatically open the pprof web viewer when profiling is enabled"`
|
||||
IPWhitelist []string `env:"ORLY_IP_WHITELIST" usage:"comma-separated list of IP addresses to allow access from, matches on prefixes to allow private subnets, eg 10.0.0 = 10.0.0.0/8"`
|
||||
IPBlacklist []string `env:"ORLY_IP_BLACKLIST" usage:"comma-separated list of IP addresses to block; matches on prefixes to allow subnets, e.g. 192.168 = 192.168.0.0/16"`
|
||||
Admins []string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
|
||||
|
||||
@@ -75,9 +75,9 @@ func (l *Listener) HandleMessage(msg []byte, remote string) {
|
||||
// Validate message for invalid characters before processing
|
||||
if err := validateJSONMessage(msg); err != nil {
|
||||
log.E.F("%s message validation FAILED (len=%d): %v", remote, len(msg), err)
|
||||
log.T.F("%s invalid message content: %q", remote, msgPreview)
|
||||
// Send error notice to client
|
||||
if noticeErr := noticeenvelope.NewFrom("invalid message format: " + err.Error()).Write(l); noticeErr != nil {
|
||||
// Don't log the actual message content as it contains binary data
|
||||
// Send generic error notice to client
|
||||
if noticeErr := noticeenvelope.NewFrom("invalid message format: contains invalid characters").Write(l); noticeErr != nil {
|
||||
log.E.F("%s failed to send validation error notice: %v", remote, noticeErr)
|
||||
}
|
||||
return
|
||||
@@ -94,10 +94,10 @@ func (l *Listener) HandleMessage(msg []byte, remote string) {
|
||||
"%s envelope identification FAILED (len=%d): %v", remote, len(msg),
|
||||
err,
|
||||
)
|
||||
log.T.F("%s malformed message content: %q", remote, msgPreview)
|
||||
// Don't log message preview as it may contain binary data
|
||||
chk.E(err)
|
||||
// Send error notice to client
|
||||
if noticeErr := noticeenvelope.NewFrom("malformed message: " + err.Error()).Write(l); noticeErr != nil {
|
||||
if noticeErr := noticeenvelope.NewFrom("malformed message").Write(l); noticeErr != nil {
|
||||
log.E.F(
|
||||
"%s failed to send malformed message notice: %v", remote,
|
||||
noticeErr,
|
||||
@@ -132,18 +132,18 @@ func (l *Listener) HandleMessage(msg []byte, remote string) {
|
||||
default:
|
||||
err = fmt.Errorf("unknown envelope type %s", t)
|
||||
log.E.F(
|
||||
"%s unknown envelope type: %s (payload: %q)", remote, t,
|
||||
string(rem),
|
||||
"%s unknown envelope type: %s (payload_len: %d)", remote, t,
|
||||
len(rem),
|
||||
)
|
||||
}
|
||||
|
||||
// Handle any processing errors
|
||||
if err != nil {
|
||||
log.E.F("%s message processing FAILED (type=%s): %v", remote, t, err)
|
||||
log.T.F("%s error context - original message: %q", remote, msgPreview)
|
||||
// Don't log message preview as it may contain binary data
|
||||
|
||||
// Send error notice to client
|
||||
noticeMsg := fmt.Sprintf("%s: %s", t, err.Error())
|
||||
// Send error notice to client (use generic message to avoid control chars in errors)
|
||||
noticeMsg := fmt.Sprintf("%s processing failed", t)
|
||||
if noticeErr := noticeenvelope.NewFrom(noticeMsg).Write(l); noticeErr != nil {
|
||||
log.E.F(
|
||||
"%s failed to send error notice after %s processing failure: %v",
|
||||
|
||||
@@ -56,6 +56,8 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
whitelist:
|
||||
// Create an independent context for this connection
|
||||
// This context will be cancelled when the connection closes or server shuts down
|
||||
ctx, cancel := context.WithCancel(s.Ctx)
|
||||
defer cancel()
|
||||
var err error
|
||||
@@ -107,7 +109,8 @@ whitelist:
|
||||
log.D.F("AUTH challenge sent successfully to %s", remote)
|
||||
}
|
||||
ticker := time.NewTicker(DefaultPingWait)
|
||||
go s.Pinger(ctx, conn, ticker, cancel)
|
||||
// Don't pass cancel to Pinger - it should not be able to cancel the connection context
|
||||
go s.Pinger(ctx, conn, ticker)
|
||||
defer func() {
|
||||
log.D.F("closing websocket connection from %s", remote)
|
||||
|
||||
@@ -117,7 +120,11 @@ whitelist:
|
||||
|
||||
// Cancel all subscriptions for this connection
|
||||
log.D.F("cancelling subscriptions for %s", remote)
|
||||
listener.publishers.Receive(&W{Cancel: true})
|
||||
listener.publishers.Receive(&W{
|
||||
Cancel: true,
|
||||
Conn: listener.conn,
|
||||
remote: listener.remote,
|
||||
})
|
||||
|
||||
// Log detailed connection statistics
|
||||
dur := time.Since(listener.startTime)
|
||||
@@ -155,6 +162,11 @@ whitelist:
|
||||
typ, msg, err = conn.Read(ctx)
|
||||
|
||||
if err != nil {
|
||||
// Check if the error is due to context cancellation
|
||||
if err == context.Canceled || strings.Contains(err.Error(), "context canceled") {
|
||||
log.T.F("connection from %s cancelled (context done): %v", remote, err)
|
||||
return
|
||||
}
|
||||
if strings.Contains(
|
||||
err.Error(), "use of closed network connection",
|
||||
) {
|
||||
@@ -233,12 +245,12 @@ whitelist:
|
||||
|
||||
func (s *Server) Pinger(
|
||||
ctx context.Context, conn *websocket.Conn, ticker *time.Ticker,
|
||||
cancel context.CancelFunc,
|
||||
) {
|
||||
defer func() {
|
||||
log.D.F("pinger shutting down")
|
||||
cancel()
|
||||
ticker.Stop()
|
||||
// DO NOT call cancel here - the pinger should not be able to cancel the connection context
|
||||
// The connection handler will cancel the context when the connection is actually closing
|
||||
}()
|
||||
var err error
|
||||
pingCount := 0
|
||||
|
||||
43
main.go
43
main.go
@@ -6,9 +6,7 @@ import (
|
||||
"net/http"
|
||||
pp "net/http/pprof"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -26,33 +24,7 @@ import (
|
||||
"next.orly.dev/pkg/version"
|
||||
)
|
||||
|
||||
// openBrowser attempts to open the specified URL in the default browser.
|
||||
// It supports multiple platforms including Linux, macOS, and Windows.
|
||||
func openBrowser(url string) {
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
err = exec.Command(
|
||||
"rundll32", "url.dll,FileProtocolHandler", url,
|
||||
).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", url).Start()
|
||||
default:
|
||||
log.W.F("unsupported platform for opening browser: %s", runtime.GOOS)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.E.F("failed to open browser: %v", err)
|
||||
} else {
|
||||
log.I.F("opened browser to %s", url)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU() * 4)
|
||||
var err error
|
||||
var cfg *config.C
|
||||
if cfg, err = config.New(); chk.T(err) {
|
||||
@@ -80,11 +52,6 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// If OpenPprofWeb is true and profiling is enabled, we need to ensure HTTP profiling is also enabled
|
||||
if cfg.OpenPprofWeb && cfg.Pprof != "" && !cfg.PprofHTTP {
|
||||
log.I.F("enabling HTTP pprof server to support web viewer")
|
||||
cfg.PprofHTTP = true
|
||||
}
|
||||
// Ensure profiling is stopped on interrupts (SIGINT/SIGTERM) as well as on normal exit
|
||||
var profileStopOnce sync.Once
|
||||
profileStop := func() {}
|
||||
@@ -318,16 +285,6 @@ func main() {
|
||||
defer cancelShutdown()
|
||||
_ = ppSrv.Shutdown(shutdownCtx)
|
||||
}()
|
||||
|
||||
// Open the pprof web viewer if enabled
|
||||
if cfg.OpenPprofWeb && cfg.Pprof != "" {
|
||||
pprofURL := "http://localhost:6060/debug/pprof/"
|
||||
go func() {
|
||||
// Wait a moment for the server to start
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
openBrowser(pprofURL)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Start health check HTTP server if configured
|
||||
|
||||
@@ -20,31 +20,50 @@ package text
|
||||
// - A form feed, 0x0C, as \f
|
||||
//
|
||||
// UTF-8 should be used for encoding.
|
||||
//
|
||||
// NOTE: We also escape all other control characters (0x00-0x1F excluding those above)
|
||||
// to ensure valid JSON, even though NIP-01 doesn't require it. This prevents
|
||||
// JSON parsing errors when events with binary data in content are sent to relays.
|
||||
func NostrEscape(dst, src []byte) []byte {
|
||||
l := len(src)
|
||||
for i := 0; i < l; i++ {
|
||||
c := src[i]
|
||||
switch {
|
||||
case c == '"':
|
||||
if c == '"' {
|
||||
dst = append(dst, '\\', '"')
|
||||
case c == '\\':
|
||||
} else if c == '\\' {
|
||||
// if i+1 < l && src[i+1] == 'u' || i+1 < l && src[i+1] == '/' {
|
||||
if i+1 < l && src[i+1] == 'u' {
|
||||
dst = append(dst, '\\')
|
||||
} else {
|
||||
dst = append(dst, '\\', '\\')
|
||||
}
|
||||
case c == '\b':
|
||||
} else if c == '\b' {
|
||||
dst = append(dst, '\\', 'b')
|
||||
case c == '\t':
|
||||
} else if c == '\t' {
|
||||
dst = append(dst, '\\', 't')
|
||||
case c == '\n':
|
||||
} else if c == '\n' {
|
||||
dst = append(dst, '\\', 'n')
|
||||
case c == '\f':
|
||||
} else if c == '\f' {
|
||||
dst = append(dst, '\\', 'f')
|
||||
case c == '\r':
|
||||
} else if c == '\r' {
|
||||
dst = append(dst, '\\', 'r')
|
||||
default:
|
||||
} else if c < 32 {
|
||||
// Escape all other control characters (0x00-0x1F except those handled above) as \uXXXX
|
||||
// This ensures valid JSON even when content contains binary data
|
||||
dst = append(dst, '\\', 'u', '0', '0')
|
||||
hexHigh := (c >> 4) & 0x0F
|
||||
hexLow := c & 0x0F
|
||||
if hexHigh < 10 {
|
||||
dst = append(dst, byte('0'+hexHigh))
|
||||
} else {
|
||||
dst = append(dst, byte('a'+(hexHigh-10)))
|
||||
}
|
||||
if hexLow < 10 {
|
||||
dst = append(dst, byte('0'+hexLow))
|
||||
} else {
|
||||
dst = append(dst, byte('a'+(hexLow-10)))
|
||||
}
|
||||
} else {
|
||||
dst = append(dst, c)
|
||||
}
|
||||
}
|
||||
@@ -91,14 +110,46 @@ func NostrUnescape(dst []byte) (b []byte) {
|
||||
dst[w] = '\r'
|
||||
w++
|
||||
|
||||
// special cases for non-nip-01 specified json escapes (must be
|
||||
// preserved for ID generation).
|
||||
case c == 'u':
|
||||
dst[w] = '\\'
|
||||
w++
|
||||
dst[w] = 'u'
|
||||
w++
|
||||
case c == '/':
|
||||
// special cases for non-nip-01 specified json escapes (must be
|
||||
// preserved for ID generation).
|
||||
case c == 'u':
|
||||
// Check if this is a \u0000-\u001F sequence we generated
|
||||
if r+4 < len(dst) && dst[r+1] == '0' && dst[r+2] == '0' {
|
||||
// Extract hex digits
|
||||
hexHigh := dst[r+3]
|
||||
hexLow := dst[r+4]
|
||||
|
||||
var val byte
|
||||
if hexHigh >= '0' && hexHigh <= '9' {
|
||||
val = (hexHigh - '0') << 4
|
||||
} else if hexHigh >= 'a' && hexHigh <= 'f' {
|
||||
val = (hexHigh - 'a' + 10) << 4
|
||||
} else if hexHigh >= 'A' && hexHigh <= 'F' {
|
||||
val = (hexHigh - 'A' + 10) << 4
|
||||
}
|
||||
|
||||
if hexLow >= '0' && hexLow <= '9' {
|
||||
val |= hexLow - '0'
|
||||
} else if hexLow >= 'a' && hexLow <= 'f' {
|
||||
val |= hexLow - 'a' + 10
|
||||
} else if hexLow >= 'A' && hexLow <= 'F' {
|
||||
val |= hexLow - 'A' + 10
|
||||
}
|
||||
|
||||
// Only decode if it's a control character (0x00-0x1F)
|
||||
if val < 32 {
|
||||
dst[w] = val
|
||||
w++
|
||||
r += 4 // Skip the u00XX part
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Not our generated \u0000-\u001F, preserve as-is
|
||||
dst[w] = '\\'
|
||||
w++
|
||||
dst[w] = 'u'
|
||||
w++
|
||||
case c == '/':
|
||||
dst[w] = '\\'
|
||||
w++
|
||||
dst[w] = '/'
|
||||
|
||||
@@ -431,7 +431,12 @@ func (p *P) checkRulePolicy(access string, ev *event.E, rule Rule, loggedInPubke
|
||||
pTags := ev.Tags.GetAll([]byte("p"))
|
||||
found := false
|
||||
for _, pTag := range pTags {
|
||||
if bytes.Equal(pTag.Value(), loggedInPubkey) {
|
||||
// pTag.Value() returns hex-encoded string; decode to bytes
|
||||
pt, err := hex.Dec(string(pTag.Value()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(pt, loggedInPubkey) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -176,7 +176,8 @@ func TestCheckKindsPolicy(t *testing.T) {
|
||||
func TestCheckRulePolicy(t *testing.T) {
|
||||
// Create test event
|
||||
testEvent := createTestEvent("test-event-id", "test-pubkey", "test content", 1)
|
||||
addTag(testEvent, "p", "test-pubkey-2")
|
||||
// Add p tag with hex-encoded pubkey
|
||||
addTag(testEvent, "p", hex.Enc([]byte("test-pubkey-2")))
|
||||
addTag(testEvent, "expiration", "1234567890")
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -27,6 +27,8 @@ const (
|
||||
ReconnectDelay = 5 * time.Second
|
||||
// MaxReconnectDelay is the maximum delay between reconnection attempts
|
||||
MaxReconnectDelay = 5 * time.Minute
|
||||
// BlackoutPeriod is the duration to blacklist a relay after MaxReconnectDelay is reached
|
||||
BlackoutPeriod = 24 * time.Hour
|
||||
)
|
||||
|
||||
// Spider manages connections to admin relays and syncs events for followed pubkeys
|
||||
@@ -64,8 +66,12 @@ type RelayConnection struct {
|
||||
subscriptions map[string]*BatchSubscription
|
||||
|
||||
// Disconnection tracking
|
||||
lastDisconnect time.Time
|
||||
reconnectDelay time.Duration
|
||||
lastDisconnect time.Time
|
||||
reconnectDelay time.Duration
|
||||
connectionStartTime time.Time
|
||||
|
||||
// Blackout tracking for IP filters
|
||||
blackoutUntil time.Time
|
||||
}
|
||||
|
||||
// BatchSubscription represents a subscription for a batch of pubkeys
|
||||
@@ -261,6 +267,20 @@ func (rc *RelayConnection) manage(followList [][]byte) {
|
||||
default:
|
||||
}
|
||||
|
||||
// Check if relay is blacked out
|
||||
if rc.isBlackedOut() {
|
||||
log.D.F("spider: %s is blacked out until %v", rc.url, rc.blackoutUntil)
|
||||
select {
|
||||
case <-rc.ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Until(rc.blackoutUntil)):
|
||||
// Blackout expired, reset delay and try again
|
||||
rc.reconnectDelay = ReconnectDelay
|
||||
log.I.F("spider: blackout period ended for %s, retrying", rc.url)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Attempt to connect
|
||||
if err := rc.connect(); chk.E(err) {
|
||||
log.W.F("spider: failed to connect to %s: %v", rc.url, err)
|
||||
@@ -269,7 +289,9 @@ func (rc *RelayConnection) manage(followList [][]byte) {
|
||||
}
|
||||
|
||||
log.I.F("spider: connected to %s", rc.url)
|
||||
rc.connectionStartTime = time.Now()
|
||||
rc.reconnectDelay = ReconnectDelay // Reset delay on successful connection
|
||||
rc.blackoutUntil = time.Time{} // Clear blackout on successful connection
|
||||
|
||||
// Create subscriptions for follow list
|
||||
rc.createSubscriptions(followList)
|
||||
@@ -278,6 +300,19 @@ func (rc *RelayConnection) manage(followList [][]byte) {
|
||||
<-rc.client.Context().Done()
|
||||
|
||||
log.W.F("spider: disconnected from %s: %v", rc.url, rc.client.ConnectionCause())
|
||||
|
||||
// Check if disconnection happened very quickly (likely IP filter)
|
||||
connectionDuration := time.Since(rc.connectionStartTime)
|
||||
const quickDisconnectThreshold = 30 * time.Second
|
||||
if connectionDuration < quickDisconnectThreshold {
|
||||
log.W.F("spider: quick disconnection from %s after %v (likely IP filter)", rc.url, connectionDuration)
|
||||
// Don't reset the delay, keep the backoff
|
||||
rc.waitBeforeReconnect()
|
||||
} else {
|
||||
// Normal disconnection, reset backoff for future connections
|
||||
rc.reconnectDelay = ReconnectDelay
|
||||
}
|
||||
|
||||
rc.handleDisconnection()
|
||||
|
||||
// Clean up
|
||||
@@ -306,13 +341,21 @@ func (rc *RelayConnection) waitBeforeReconnect() {
|
||||
case <-time.After(rc.reconnectDelay):
|
||||
}
|
||||
|
||||
// Exponential backoff
|
||||
// Exponential backoff - double every time
|
||||
rc.reconnectDelay *= 2
|
||||
if rc.reconnectDelay > MaxReconnectDelay {
|
||||
rc.reconnectDelay = MaxReconnectDelay
|
||||
|
||||
// If backoff exceeds 5 minutes, blackout for 24 hours
|
||||
if rc.reconnectDelay >= MaxReconnectDelay {
|
||||
rc.blackoutUntil = time.Now().Add(BlackoutPeriod)
|
||||
log.W.F("spider: max backoff exceeded for %s (reached %v), blacking out for 24 hours", rc.url, rc.reconnectDelay)
|
||||
}
|
||||
}
|
||||
|
||||
// isBlackedOut returns true if the relay is currently blacked out
|
||||
func (rc *RelayConnection) isBlackedOut() bool {
|
||||
return !rc.blackoutUntil.IsZero() && time.Now().Before(rc.blackoutUntil)
|
||||
}
|
||||
|
||||
// handleDisconnection records disconnection time for catch-up logic
|
||||
func (rc *RelayConnection) handleDisconnection() {
|
||||
now := time.Now()
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.19.5
|
||||
v0.19.8
|
||||
@@ -7,7 +7,7 @@ set -e
|
||||
|
||||
# Configuration
|
||||
GO_VERSION="1.23.1"
|
||||
GOROOT="$HOME/.local/go"
|
||||
GOROOT="$HOME/go"
|
||||
GOPATH="$HOME"
|
||||
GOBIN="$HOME/.local/bin"
|
||||
GOENV_FILE="$HOME/.goenv"
|
||||
@@ -84,13 +84,11 @@ install_go() {
|
||||
local download_url="https://golang.org/dl/${go_archive}"
|
||||
|
||||
# Create directories
|
||||
mkdir -p "$HOME/.local"
|
||||
mkdir -p "$GOPATH"
|
||||
mkdir -p "$GOBIN"
|
||||
|
||||
# Download and extract Go
|
||||
# Change to home directory and download Go
|
||||
log_info "Downloading Go from $download_url..."
|
||||
cd /tmp
|
||||
cd ~
|
||||
wget -q "$download_url" || {
|
||||
log_error "Failed to download Go"
|
||||
exit 1
|
||||
@@ -104,8 +102,7 @@ install_go() {
|
||||
|
||||
# Extract Go
|
||||
log_info "Extracting Go to $GOROOT..."
|
||||
tar -xf "$go_archive" -C "$HOME/.local/"
|
||||
mv "$HOME/.local/go" "$GOROOT"
|
||||
tar -xf "$go_archive"
|
||||
|
||||
# Clean up
|
||||
rm -f "$go_archive"
|
||||
|
||||
1
scripts/secp256k1
Submodule
1
scripts/secp256k1
Submodule
Submodule scripts/secp256k1 added at 0cdc758a56
167
test-relay-connection.js
Executable file
167
test-relay-connection.js
Executable file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Test script to verify websocket connections are not closed prematurely
|
||||
// This is a Node.js test script that can be run with: node test-relay-connection.js
|
||||
|
||||
import { NostrWebSocket } from '@nostr-dev-kit/ndk';
|
||||
|
||||
const RELAY = process.env.RELAY || 'ws://localhost:8080';
|
||||
const MAX_CONNECTIONS = 10;
|
||||
const TEST_DURATION = 30000; // 30 seconds
|
||||
|
||||
let connectionsClosed = 0;
|
||||
let connectionsOpened = 0;
|
||||
let messagesReceived = 0;
|
||||
let errors = 0;
|
||||
|
||||
const stats = {
|
||||
premature: 0,
|
||||
normal: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
class TestConnection {
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
this.ws = null;
|
||||
this.closed = false;
|
||||
this.openTime = null;
|
||||
this.closeTime = null;
|
||||
this.lastError = null;
|
||||
}
|
||||
|
||||
connect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ws = new NostrWebSocket(RELAY);
|
||||
|
||||
this.ws.addEventListener('open', () => {
|
||||
this.openTime = Date.now();
|
||||
connectionsOpened++;
|
||||
console.log(`[Connection ${this.id}] Opened`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.ws.addEventListener('close', (event) => {
|
||||
this.closeTime = Date.now();
|
||||
this.closed = true;
|
||||
connectionsClosed++;
|
||||
const duration = this.closeTime - this.openTime;
|
||||
console.log(`[Connection ${this.id}] Closed: code=${event.code}, reason="${event.reason || ''}", duration=${duration}ms`);
|
||||
|
||||
if (duration < 5000 && event.code !== 1000) {
|
||||
stats.premature++;
|
||||
console.log(`[Connection ${this.id}] PREMATURE CLOSE DETECTED: duration=${duration}ms < 5s`);
|
||||
} else {
|
||||
stats.normal++;
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.addEventListener('error', (error) => {
|
||||
this.lastError = error;
|
||||
stats.errors++;
|
||||
console.error(`[Connection ${this.id}] Error:`, error);
|
||||
});
|
||||
|
||||
this.ws.addEventListener('message', (event) => {
|
||||
messagesReceived++;
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(`[Connection ${this.id}] Message:`, data[0]);
|
||||
} catch (e) {
|
||||
console.log(`[Connection ${this.id}] Message (non-JSON):`, event.data);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(reject, 5000); // Timeout after 5 seconds if not opened
|
||||
});
|
||||
}
|
||||
|
||||
sendReq() {
|
||||
if (this.ws && !this.closed) {
|
||||
this.ws.send(JSON.stringify(['REQ', `test-sub-${this.id}`, { kinds: [1], limit: 10 }]));
|
||||
console.log(`[Connection ${this.id}] Sent REQ`);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.ws && !this.closed) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runTest() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Testing Relay Connection Stability');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Relay: ${RELAY}`);
|
||||
console.log(`Duration: ${TEST_DURATION}ms`);
|
||||
console.log(`Connections: ${MAX_CONNECTIONS}`);
|
||||
console.log('='.repeat(60));
|
||||
console.log();
|
||||
|
||||
const connections = [];
|
||||
|
||||
// Open connections
|
||||
console.log('Opening connections...');
|
||||
for (let i = 0; i < MAX_CONNECTIONS; i++) {
|
||||
const conn = new TestConnection(i);
|
||||
try {
|
||||
await conn.connect();
|
||||
connections.push(conn);
|
||||
} catch (error) {
|
||||
console.error(`Failed to open connection ${i}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Opened ${connections.length} connections`);
|
||||
console.log();
|
||||
|
||||
// Send requests from each connection
|
||||
console.log('Sending REQ messages...');
|
||||
for (const conn of connections) {
|
||||
conn.sendReq();
|
||||
}
|
||||
|
||||
// Wait and let connections run
|
||||
console.log(`Waiting ${TEST_DURATION / 1000}s...`);
|
||||
await new Promise(resolve => setTimeout(resolve, TEST_DURATION));
|
||||
|
||||
// Close all connections
|
||||
console.log('Closing all connections...');
|
||||
for (const conn of connections) {
|
||||
conn.close();
|
||||
}
|
||||
|
||||
// Wait for close events
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Print results
|
||||
console.log();
|
||||
console.log('='.repeat(60));
|
||||
console.log('Test Results:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Connections Opened: ${connectionsOpened}`);
|
||||
console.log(`Connections Closed: ${connectionsClosed}`);
|
||||
console.log(`Messages Received: ${messagesReceived}`);
|
||||
console.log();
|
||||
console.log('Closure Analysis:');
|
||||
console.log(`- Premature Closes: ${stats.premature}`);
|
||||
console.log(`- Normal Closes: ${stats.normal}`);
|
||||
console.log(`- Errors: ${stats.errors}`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
if (stats.premature > 0) {
|
||||
console.error('FAILED: Detected premature connection closures!');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('PASSED: No premature connection closures detected.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
runTest().catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
57
test-websocket-close.js
Executable file
57
test-websocket-close.js
Executable file
@@ -0,0 +1,57 @@
|
||||
import { NostrWebSocket } from '@nostr-dev-kit/ndk';
|
||||
|
||||
const RELAY = process.env.RELAY || 'ws://localhost:8080';
|
||||
|
||||
async function testConnectionClosure() {
|
||||
console.log('Testing websocket connection closure issues...');
|
||||
console.log('Connecting to:', RELAY);
|
||||
|
||||
// Create multiple connections to test concurrency
|
||||
const connections = [];
|
||||
const results = { connected: 0, closed: 0, errors: 0 };
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const ws = new NostrWebSocket(RELAY);
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
console.log(`Connection ${i} opened`);
|
||||
results.connected++;
|
||||
});
|
||||
|
||||
ws.addEventListener('close', (event) => {
|
||||
console.log(`Connection ${i} closed:`, event.code, event.reason);
|
||||
results.closed++;
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (error) => {
|
||||
console.error(`Connection ${i} error:`, error);
|
||||
results.errors++;
|
||||
});
|
||||
|
||||
connections.push(ws);
|
||||
}
|
||||
|
||||
// Wait a bit then send REQs
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Send some REQ messages
|
||||
for (const ws of connections) {
|
||||
ws.send(JSON.stringify(['REQ', 'test-sub', { kinds: [1] }]));
|
||||
}
|
||||
|
||||
// Wait and observe behavior
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
console.log('\nTest Results:');
|
||||
console.log(`- Connected: ${results.connected}`);
|
||||
console.log(`- Closed prematurely: ${results.closed}`);
|
||||
console.log(`- Errors: ${results.errors}`);
|
||||
|
||||
// Close all connections
|
||||
for (const ws of connections) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
testConnectionClosure().catch(console.error);
|
||||
|
||||
Reference in New Issue
Block a user