|
|
|
@ -25,6 +25,7 @@ type ClusterManager struct { |
|
|
|
db *database.D |
|
|
|
db *database.D |
|
|
|
adminNpubs []string |
|
|
|
adminNpubs []string |
|
|
|
relayIdentityPubkey string // Our relay's identity pubkey (hex)
|
|
|
|
relayIdentityPubkey string // Our relay's identity pubkey (hex)
|
|
|
|
|
|
|
|
selfURLs map[string]bool // URLs discovered to be ourselves (for fast lookups)
|
|
|
|
members map[string]*ClusterMember // keyed by relay URL
|
|
|
|
members map[string]*ClusterMember // keyed by relay URL
|
|
|
|
membersMux sync.RWMutex |
|
|
|
membersMux sync.RWMutex |
|
|
|
pollTicker *time.Ticker |
|
|
|
pollTicker *time.Ticker |
|
|
|
@ -78,6 +79,7 @@ func NewClusterManager(ctx context.Context, db *database.D, adminNpubs []string, |
|
|
|
db: db, |
|
|
|
db: db, |
|
|
|
adminNpubs: adminNpubs, |
|
|
|
adminNpubs: adminNpubs, |
|
|
|
relayIdentityPubkey: relayPubkey, |
|
|
|
relayIdentityPubkey: relayPubkey, |
|
|
|
|
|
|
|
selfURLs: make(map[string]bool), |
|
|
|
members: make(map[string]*ClusterMember), |
|
|
|
members: make(map[string]*ClusterMember), |
|
|
|
pollDone: make(chan struct{}), |
|
|
|
pollDone: make(chan struct{}), |
|
|
|
propagatePrivilegedEvents: propagatePrivilegedEvents, |
|
|
|
propagatePrivilegedEvents: propagatePrivilegedEvents, |
|
|
|
@ -265,17 +267,36 @@ func (cm *ClusterManager) UpdateMembership(relayURLs []string) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Add new members
|
|
|
|
// Add new members (filter out self once at this point)
|
|
|
|
for _, url := range relayURLs { |
|
|
|
for _, url := range relayURLs { |
|
|
|
// Skip if this is our own relay (check via NIP-11 pubkey)
|
|
|
|
// Skip if already exists
|
|
|
|
if cm.isSelfRelay(url) { |
|
|
|
if _, exists := cm.members[url]; exists { |
|
|
|
log.D.F("skipping cluster member (self): %s (pubkey matches our relay identity)", url) |
|
|
|
|
|
|
|
continue |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if _, exists := cm.members[url]; !exists { |
|
|
|
// Fast path: check if we already know this URL is ours
|
|
|
|
// For simplicity, assume HTTP and WebSocket URLs are the same
|
|
|
|
if cm.selfURLs[url] { |
|
|
|
// In practice, you'd need to parse these properly
|
|
|
|
log.I.F("removed self from cluster members (known URL): %s", url) |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Slow path: check via NIP-11 pubkey
|
|
|
|
|
|
|
|
if cm.relayIdentityPubkey != "" { |
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
|
|
|
|
|
|
peerPubkey, err := cm.nip11Cache.GetPubkey(ctx, url) |
|
|
|
|
|
|
|
cancel() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
log.D.F("couldn't fetch NIP-11 for %s, adding to cluster anyway: %v", url, err) |
|
|
|
|
|
|
|
} else if peerPubkey == cm.relayIdentityPubkey { |
|
|
|
|
|
|
|
log.I.F("removed self from cluster members (discovered): %s (pubkey: %s)", url, cm.relayIdentityPubkey) |
|
|
|
|
|
|
|
// Cache this URL as ours for future fast lookups
|
|
|
|
|
|
|
|
cm.selfURLs[url] = true |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add member
|
|
|
|
member := &ClusterMember{ |
|
|
|
member := &ClusterMember{ |
|
|
|
HTTPURL: url, |
|
|
|
HTTPURL: url, |
|
|
|
WebSocketURL: url, // TODO: Convert to WebSocket URL
|
|
|
|
WebSocketURL: url, // TODO: Convert to WebSocket URL
|
|
|
|
@ -285,26 +306,6 @@ func (cm *ClusterManager) UpdateMembership(relayURLs []string) { |
|
|
|
cm.members[url] = member |
|
|
|
cm.members[url] = member |
|
|
|
log.I.F("added cluster member: %s", url) |
|
|
|
log.I.F("added cluster member: %s", url) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// isSelfRelay checks if a relay URL is actually ourselves by comparing NIP-11 pubkeys
|
|
|
|
|
|
|
|
func (cm *ClusterManager) isSelfRelay(relayURL string) bool { |
|
|
|
|
|
|
|
// If we don't have a relay identity pubkey, can't compare
|
|
|
|
|
|
|
|
if cm.relayIdentityPubkey == "" { |
|
|
|
|
|
|
|
return false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
|
|
|
|
|
|
defer cancel() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
peerPubkey, err := cm.nip11Cache.GetPubkey(ctx, relayURL) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
log.D.F("couldn't fetch NIP-11 for %s to check if self: %v", relayURL, err) |
|
|
|
|
|
|
|
return false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return peerPubkey == cm.relayIdentityPubkey |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// HandleMembershipEvent processes a cluster membership event (Kind 39108)
|
|
|
|
// HandleMembershipEvent processes a cluster membership event (Kind 39108)
|
|
|
|
@ -352,19 +353,38 @@ func (cm *ClusterManager) HandleLatestSerial(w http.ResponseWriter, r *http.Requ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Check if request is from ourselves by examining the Referer or Origin header
|
|
|
|
// Check if request is from ourselves by examining the Referer or Origin header
|
|
|
|
|
|
|
|
// Note: Self-members are already filtered out, but this catches edge cases
|
|
|
|
origin := r.Header.Get("Origin") |
|
|
|
origin := r.Header.Get("Origin") |
|
|
|
referer := r.Header.Get("Referer") |
|
|
|
referer := r.Header.Get("Referer") |
|
|
|
|
|
|
|
|
|
|
|
if origin != "" && cm.isSelfRelay(origin) { |
|
|
|
if cm.relayIdentityPubkey != "" && (origin != "" || referer != "") { |
|
|
|
log.D.F("rejecting cluster latest request from self (origin: %s)", origin) |
|
|
|
checkURL := origin |
|
|
|
|
|
|
|
if checkURL == "" { |
|
|
|
|
|
|
|
checkURL = referer |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fast path: check known self-URLs
|
|
|
|
|
|
|
|
if cm.selfURLs[checkURL] { |
|
|
|
|
|
|
|
log.D.F("rejecting cluster latest request from self (known URL): %s", checkURL) |
|
|
|
http.Error(w, "Cannot sync with self", http.StatusBadRequest) |
|
|
|
http.Error(w, "Cannot sync with self", http.StatusBadRequest) |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
if referer != "" && cm.isSelfRelay(referer) { |
|
|
|
|
|
|
|
log.D.F("rejecting cluster latest request from self (referer: %s)", referer) |
|
|
|
// Slow path: verify via NIP-11
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) |
|
|
|
|
|
|
|
peerPubkey, err := cm.nip11Cache.GetPubkey(ctx, checkURL) |
|
|
|
|
|
|
|
cancel() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err == nil && peerPubkey == cm.relayIdentityPubkey { |
|
|
|
|
|
|
|
log.D.F("rejecting cluster latest request from self (discovered): %s", checkURL) |
|
|
|
|
|
|
|
// Cache for future fast lookups
|
|
|
|
|
|
|
|
cm.membersMux.Lock() |
|
|
|
|
|
|
|
cm.selfURLs[checkURL] = true |
|
|
|
|
|
|
|
cm.membersMux.Unlock() |
|
|
|
http.Error(w, "Cannot sync with self", http.StatusBadRequest) |
|
|
|
http.Error(w, "Cannot sync with self", http.StatusBadRequest) |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Get the latest serial from database by querying for the highest serial
|
|
|
|
// Get the latest serial from database by querying for the highest serial
|
|
|
|
latestSerial, err := cm.getLatestSerialFromDB() |
|
|
|
latestSerial, err := cm.getLatestSerialFromDB() |
|
|
|
@ -390,19 +410,38 @@ func (cm *ClusterManager) HandleEventsRange(w http.ResponseWriter, r *http.Reque |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Check if request is from ourselves by examining the Referer or Origin header
|
|
|
|
// Check if request is from ourselves by examining the Referer or Origin header
|
|
|
|
|
|
|
|
// Note: Self-members are already filtered out, but this catches edge cases
|
|
|
|
origin := r.Header.Get("Origin") |
|
|
|
origin := r.Header.Get("Origin") |
|
|
|
referer := r.Header.Get("Referer") |
|
|
|
referer := r.Header.Get("Referer") |
|
|
|
|
|
|
|
|
|
|
|
if origin != "" && cm.isSelfRelay(origin) { |
|
|
|
if cm.relayIdentityPubkey != "" && (origin != "" || referer != "") { |
|
|
|
log.D.F("rejecting cluster events request from self (origin: %s)", origin) |
|
|
|
checkURL := origin |
|
|
|
|
|
|
|
if checkURL == "" { |
|
|
|
|
|
|
|
checkURL = referer |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fast path: check known self-URLs
|
|
|
|
|
|
|
|
if cm.selfURLs[checkURL] { |
|
|
|
|
|
|
|
log.D.F("rejecting cluster events request from self (known URL): %s", checkURL) |
|
|
|
http.Error(w, "Cannot sync with self", http.StatusBadRequest) |
|
|
|
http.Error(w, "Cannot sync with self", http.StatusBadRequest) |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
if referer != "" && cm.isSelfRelay(referer) { |
|
|
|
|
|
|
|
log.D.F("rejecting cluster events request from self (referer: %s)", referer) |
|
|
|
// Slow path: verify via NIP-11
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) |
|
|
|
|
|
|
|
peerPubkey, err := cm.nip11Cache.GetPubkey(ctx, checkURL) |
|
|
|
|
|
|
|
cancel() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err == nil && peerPubkey == cm.relayIdentityPubkey { |
|
|
|
|
|
|
|
log.D.F("rejecting cluster events request from self (discovered): %s", checkURL) |
|
|
|
|
|
|
|
// Cache for future fast lookups
|
|
|
|
|
|
|
|
cm.membersMux.Lock() |
|
|
|
|
|
|
|
cm.selfURLs[checkURL] = true |
|
|
|
|
|
|
|
cm.membersMux.Unlock() |
|
|
|
http.Error(w, "Cannot sync with self", http.StatusBadRequest) |
|
|
|
http.Error(w, "Cannot sync with self", http.StatusBadRequest) |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Parse query parameters
|
|
|
|
// Parse query parameters
|
|
|
|
fromStr := r.URL.Query().Get("from") |
|
|
|
fromStr := r.URL.Query().Get("from") |
|
|
|
|