Compare commits

..

9 Commits

Author SHA1 Message Date
4f97cb9a42 Improve error handling and logging in message processing
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
- Updated HandleMessage function to include the actual message content in error notices for invalid messages, enhancing clarity for debugging.
- Added nil check in Value method of tag encoder to prevent potential nil pointer dereference.
- Refactored tag handling in spider to utilize hex-encoded pubkeys for improved compatibility and clarity in filter creation.
- bump to v0.19.10
2025-10-28 20:26:09 +00:00
df67538af2 Refactor Marshal function in filter encoder
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
- Simplified the key-value appending logic in the Marshal function by replacing manual string appending with the text.AppendQuote method for better readability and maintainability.
- Updated version number to v0.19.9 to reflect the changes made.
2025-10-28 19:23:27 +00:00
f5d13a6807 Update error handling and logging in message processing
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
- Modified the HandleMessage function to avoid logging actual message content and instead send generic error notices to clients for invalid or malformed messages, enhancing security by preventing exposure of binary data.
- Updated the NostrEscape function to escape all control characters to ensure valid JSON, preventing parsing errors with binary data.
- Adjusted policy checks to handle hex-encoded pubkeys correctly, ensuring compatibility with the updated encoding scheme.
- Introduced blackout period for relay connections after exceeding maximum reconnection delays, improving connection stability and management.
2025-10-28 19:12:02 +00:00
a735bd3d5e bump to v0.19.7
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
2025-10-28 18:43:31 +00:00
0a32cc3125 bump to v0.19.6 2025-10-28 18:42:25 +00:00
7906bb2295 Add WebSocket Connection Testing Scripts
- Introduced two new test scripts: `test-relay-connection.js` and `test-websocket-close.js` to verify WebSocket connection stability and closure behavior.
- `test-relay-connection.js` tests multiple connections, monitors their open/close events, and analyzes premature closures.
- `test-websocket-close.js` focuses on connection closure issues with concurrent connections and logs results for connected, closed, and error states.
- Both scripts utilize the NostrWebSocket from the @nostr-dev-kit/ndk package for testing purposes.
2025-10-28 18:42:18 +00:00
50a8b39ea3 fix broken pprof web by removing for now 2025-10-28 13:32:30 +00:00
45cfd04214 Refactor Go Build Process to Focus on Orly Binary
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
- Updated the Go workflow to only build the orly binary as requested, simplifying the build process.
- Removed the building of additional command-line utilities, which are designated as development tools.
- Added comments for clarity regarding the changes made to the build process.
2025-10-27 19:48:55 +00:00
ced06a9175 Update Go Installation Script and Adjust Directory Structure
- Changed GOROOT path from "$HOME/.local/go" to "$HOME/go" for improved clarity and consistency.
- Removed unnecessary creation of the "$HOME/.local" directory, simplifying the installation process.
- Updated the download and extraction process for Go to use the home directory, enhancing usability.
- Cleaned up the script by removing redundant commands and improving overall readability.
2025-10-27 19:13:22 +00:00
17 changed files with 406 additions and 123 deletions

View File

@@ -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

View File

@@ -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"`

View File

@@ -75,9 +75,7 @@ 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 {
if noticeErr := noticeenvelope.NewFrom(fmt.Sprintf("invalid message format: contains invalid characters: %s", msg)).Write(l); noticeErr != nil {
log.E.F("%s failed to send validation error notice: %v", remote, noticeErr)
}
return
@@ -94,10 +92,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 +130,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",

View File

@@ -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
View File

@@ -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

View File

@@ -208,18 +208,16 @@ func (f *F) Marshal(dst []byte) (b []byte) {
} else {
first = true
}
// append the key with # prefix
dst = append(dst, '"', '#', tKey[0], '"', ':')
dst = append(dst, '[')
for i, value := range values {
dst = append(dst, '"')
dst = append(dst, value...)
dst = append(dst, '"')
if i < len(values)-1 {
dst = append(dst, ',')
}
// append the key with # prefix
dst = append(dst, '"', '#', tKey[0], '"', ':')
dst = append(dst, '[')
for i, value := range values {
dst = text.AppendQuote(dst, value, text.NostrEscape)
if i < len(values)-1 {
dst = append(dst, ',')
}
dst = append(dst, ']')
}
dst = append(dst, ']')
}
}
if f.Since != nil && f.Since.U64() > 0 {

View File

@@ -144,6 +144,7 @@ func (t *T) Key() (key []byte) {
}
func (t *T) Value() (key []byte) {
if t==nil {return}
if len(t.T) > Value {
return t.T[Value]
}

View File

@@ -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] = '/'

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -307,7 +307,7 @@ func (r *Client) ConnectWithTLS(
if r.notices != nil {
r.notices <- env.Message
} else {
log.E.F("NOTICE from %s: '%s'\n", r.URL, env.Message)
log.E.F("NOTICE from %s: '%s'", r.URL, env.Message)
}
case authenvelope.L:
env := authenvelope.NewChallenge()

View File

@@ -2,7 +2,6 @@ package spider
import (
"context"
"encoding/hex"
"fmt"
"sync"
"time"
@@ -12,6 +11,7 @@ import (
"lol.mleku.dev/log"
"next.orly.dev/pkg/database"
"next.orly.dev/pkg/encoders/filter"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/encoders/tag"
"next.orly.dev/pkg/encoders/timestamp"
"next.orly.dev/pkg/interfaces/publisher"
@@ -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()
@@ -371,17 +414,20 @@ func (rc *RelayConnection) createBatchSubscription(batchID string, pubkeys [][]b
}
// Create filters: one for authors, one for p tags
var pTags tag.S
// For #p tag filters, all pubkeys must be in a single tag array as hex-encoded strings
tagElements := [][]byte{[]byte("p")} // First element is the key
for _, pk := range pubkeys {
pTags = append(pTags, tag.NewFromAny("p", pk))
pkHex := hex.EncAppend(nil, pk)
tagElements = append(tagElements, pkHex)
}
pTags := &tag.S{tag.NewFromBytesSlice(tagElements...)}
filters := filter.NewS(
&filter.F{
Authors: tag.NewFromBytesSlice(pubkeys...),
},
&filter.F{
Tags: tag.NewS(pTags...),
Tags: pTags,
},
)
@@ -422,10 +468,6 @@ func (bs *BatchSubscription) handleEvents() {
// Save event to database
if _, err := bs.relay.spider.db.SaveEvent(bs.relay.ctx, ev); err != nil {
if !chk.E(err) {
log.T.F("spider: saved event %s from %s",
hex.EncodeToString(ev.ID[:]), bs.relay.url)
}
} else {
// Publish event if it was newly saved
if bs.relay.spider.pub != nil {
@@ -484,10 +526,14 @@ func (rc *RelayConnection) performCatchup(sub *BatchSubscription, disconnectTime
sinceTs := timestamp.T{V: since.Unix()}
untilTs := timestamp.T{V: until.Unix()}
var pTags tag.S
// Create filters with hex-encoded pubkeys for #p tags
// All pubkeys must be in a single tag array
tagElements := [][]byte{[]byte("p")} // First element is the key
for _, pk := range sub.pubkeys {
pTags = append(pTags, tag.NewFromAny("p", pk))
pkHex := hex.EncAppend(nil, pk)
tagElements = append(tagElements, pkHex)
}
pTags := &tag.S{tag.NewFromBytesSlice(tagElements...)}
filters := filter.NewS(
&filter.F{
@@ -496,7 +542,7 @@ func (rc *RelayConnection) performCatchup(sub *BatchSubscription, disconnectTime
Until: &untilTs,
},
&filter.F{
Tags: tag.NewS(pTags...),
Tags: pTags,
Since: &sinceTs,
Until: &untilTs,
},
@@ -539,7 +585,7 @@ func (rc *RelayConnection) performCatchup(sub *BatchSubscription, disconnectTime
if _, err := rc.spider.db.SaveEvent(rc.ctx, ev); err != nil {
if !chk.E(err) {
log.T.F("spider: catch-up saved event %s from %s",
hex.EncodeToString(ev.ID[:]), rc.url)
hex.Enc(ev.ID[:]), rc.url)
}
} else {
// Publish event if it was newly saved

View File

@@ -1 +1 @@
v0.19.5
v0.19.9

View File

@@ -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

Submodule scripts/secp256k1 added at 0cdc758a56

167
test-relay-connection.js Executable file
View 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
View 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);