Some checks failed
Go / build-and-release (push) Has been cancelled
This commit allows skipping authentication, permission checks, and certain filters (e.g., deletions, expirations) when the ACL mode is set to "none" (open relay mode). It also introduces a configuration option to disable query caching to reduce memory usage. These changes improve operational flexibility for open relay setups and resource-constrained environments.
774 lines
21 KiB
Go
774 lines
21 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"lol.mleku.dev/chk"
|
|
"lol.mleku.dev/log"
|
|
"next.orly.dev/pkg/acl"
|
|
"git.mleku.dev/mleku/nostr/encoders/envelopes/authenvelope"
|
|
"git.mleku.dev/mleku/nostr/encoders/envelopes/eventenvelope"
|
|
"git.mleku.dev/mleku/nostr/encoders/envelopes/noticeenvelope"
|
|
"git.mleku.dev/mleku/nostr/encoders/envelopes/okenvelope"
|
|
"git.mleku.dev/mleku/nostr/encoders/hex"
|
|
"git.mleku.dev/mleku/nostr/encoders/kind"
|
|
"git.mleku.dev/mleku/nostr/encoders/reason"
|
|
"next.orly.dev/pkg/protocol/nip43"
|
|
"next.orly.dev/pkg/utils"
|
|
)
|
|
|
|
// validateLowercaseHexInJSON checks that all hex-encoded fields in the raw JSON are lowercase.
|
|
// NIP-01 specifies that hex encoding must be lowercase.
|
|
// This must be called on the raw message BEFORE unmarshaling, since unmarshal converts
|
|
// hex strings to binary and loses case information.
|
|
// Returns an error message if validation fails, or empty string if valid.
|
|
func validateLowercaseHexInJSON(msg []byte) string {
|
|
// Find and validate "id" field (64 hex chars)
|
|
if err := validateJSONHexField(msg, `"id"`); err != "" {
|
|
return err + " (id)"
|
|
}
|
|
|
|
// Find and validate "pubkey" field (64 hex chars)
|
|
if err := validateJSONHexField(msg, `"pubkey"`); err != "" {
|
|
return err + " (pubkey)"
|
|
}
|
|
|
|
// Find and validate "sig" field (128 hex chars)
|
|
if err := validateJSONHexField(msg, `"sig"`); err != "" {
|
|
return err + " (sig)"
|
|
}
|
|
|
|
// Validate e and p tags in the tags array
|
|
// Tags format: ["e", "hexvalue", ...] or ["p", "hexvalue", ...]
|
|
if err := validateEPTagsInJSON(msg); err != "" {
|
|
return err
|
|
}
|
|
|
|
return "" // Valid
|
|
}
|
|
|
|
// validateJSONHexField finds a JSON field and checks if its hex value contains uppercase.
|
|
func validateJSONHexField(msg []byte, fieldName string) string {
|
|
// Find the field name
|
|
idx := bytes.Index(msg, []byte(fieldName))
|
|
if idx == -1 {
|
|
return "" // Field not found, skip
|
|
}
|
|
|
|
// Find the colon after the field name
|
|
colonIdx := bytes.Index(msg[idx:], []byte(":"))
|
|
if colonIdx == -1 {
|
|
return ""
|
|
}
|
|
|
|
// Find the opening quote of the value
|
|
valueStart := idx + colonIdx + 1
|
|
for valueStart < len(msg) && (msg[valueStart] == ' ' || msg[valueStart] == '\t' || msg[valueStart] == '\n' || msg[valueStart] == '\r') {
|
|
valueStart++
|
|
}
|
|
if valueStart >= len(msg) || msg[valueStart] != '"' {
|
|
return ""
|
|
}
|
|
valueStart++ // Skip the opening quote
|
|
|
|
// Find the closing quote
|
|
valueEnd := valueStart
|
|
for valueEnd < len(msg) && msg[valueEnd] != '"' {
|
|
valueEnd++
|
|
}
|
|
|
|
// Extract the hex value and check for uppercase
|
|
hexValue := msg[valueStart:valueEnd]
|
|
if containsUppercaseHex(hexValue) {
|
|
return "blocked: hex fields may only be lower case, see NIP-01"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// validateEPTagsInJSON checks e and p tags in the JSON for uppercase hex.
|
|
func validateEPTagsInJSON(msg []byte) string {
|
|
// Find the tags array
|
|
tagsIdx := bytes.Index(msg, []byte(`"tags"`))
|
|
if tagsIdx == -1 {
|
|
return "" // No tags
|
|
}
|
|
|
|
// Find the opening bracket of the tags array
|
|
bracketIdx := bytes.Index(msg[tagsIdx:], []byte("["))
|
|
if bracketIdx == -1 {
|
|
return ""
|
|
}
|
|
|
|
tagsStart := tagsIdx + bracketIdx
|
|
|
|
// Scan through to find ["e", ...] and ["p", ...] patterns
|
|
// This is a simplified parser that looks for specific patterns
|
|
pos := tagsStart
|
|
for pos < len(msg) {
|
|
// Look for ["e" or ["p" pattern
|
|
eTagPattern := bytes.Index(msg[pos:], []byte(`["e"`))
|
|
pTagPattern := bytes.Index(msg[pos:], []byte(`["p"`))
|
|
|
|
var tagType string
|
|
var nextIdx int
|
|
|
|
if eTagPattern == -1 && pTagPattern == -1 {
|
|
break // No more e or p tags
|
|
} else if eTagPattern == -1 {
|
|
nextIdx = pos + pTagPattern
|
|
tagType = "p"
|
|
} else if pTagPattern == -1 {
|
|
nextIdx = pos + eTagPattern
|
|
tagType = "e"
|
|
} else if eTagPattern < pTagPattern {
|
|
nextIdx = pos + eTagPattern
|
|
tagType = "e"
|
|
} else {
|
|
nextIdx = pos + pTagPattern
|
|
tagType = "p"
|
|
}
|
|
|
|
// Find the hex value after the tag type
|
|
// Pattern: ["e", "hexvalue" or ["p", "hexvalue"
|
|
commaIdx := bytes.Index(msg[nextIdx:], []byte(","))
|
|
if commaIdx == -1 {
|
|
pos = nextIdx + 4
|
|
continue
|
|
}
|
|
|
|
// Find the opening quote of the hex value
|
|
valueStart := nextIdx + commaIdx + 1
|
|
for valueStart < len(msg) && (msg[valueStart] == ' ' || msg[valueStart] == '\t' || msg[valueStart] == '"') {
|
|
if msg[valueStart] == '"' {
|
|
valueStart++
|
|
break
|
|
}
|
|
valueStart++
|
|
}
|
|
|
|
// Find the closing quote
|
|
valueEnd := valueStart
|
|
for valueEnd < len(msg) && msg[valueEnd] != '"' {
|
|
valueEnd++
|
|
}
|
|
|
|
// Check if this looks like a hex value (64 chars for pubkey/event ID)
|
|
hexValue := msg[valueStart:valueEnd]
|
|
if len(hexValue) == 64 && containsUppercaseHex(hexValue) {
|
|
return fmt.Sprintf("blocked: hex fields may only be lower case, see NIP-01 (%s tag)", tagType)
|
|
}
|
|
|
|
pos = valueEnd + 1
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// containsUppercaseHex checks if a byte slice (representing hex) contains uppercase letters A-F.
|
|
func containsUppercaseHex(b []byte) bool {
|
|
for _, c := range b {
|
|
if c >= 'A' && c <= 'F' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (l *Listener) HandleEvent(msg []byte) (err error) {
|
|
log.D.F("HandleEvent: START handling event: %s", msg)
|
|
|
|
// Validate that all hex fields are lowercase BEFORE unmarshaling
|
|
// (unmarshal converts hex to binary and loses case information)
|
|
if errMsg := validateLowercaseHexInJSON(msg); errMsg != "" {
|
|
log.W.F("HandleEvent: rejecting event with uppercase hex: %s", errMsg)
|
|
// Send NOTICE to alert client developers about the issue
|
|
if noticeErr := noticeenvelope.NewFrom(errMsg).Write(l); noticeErr != nil {
|
|
log.E.F("failed to send NOTICE for uppercase hex: %v", noticeErr)
|
|
}
|
|
// Send OK false with the error message
|
|
if err = okenvelope.NewFrom(
|
|
nil, false,
|
|
reason.Blocked.F(errMsg),
|
|
).Write(l); chk.E(err) {
|
|
return
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// decode the envelope
|
|
env := eventenvelope.NewSubmission()
|
|
log.I.F("HandleEvent: received event message length: %d", len(msg))
|
|
if msg, err = env.Unmarshal(msg); chk.E(err) {
|
|
log.E.F("HandleEvent: failed to unmarshal event: %v", err)
|
|
return
|
|
}
|
|
log.I.F(
|
|
"HandleEvent: successfully unmarshaled event, kind: %d, pubkey: %s, id: %0x",
|
|
env.E.Kind, hex.Enc(env.E.Pubkey), env.E.ID,
|
|
)
|
|
defer func() {
|
|
if env != nil && env.E != nil {
|
|
env.E.Free()
|
|
}
|
|
}()
|
|
|
|
if len(msg) > 0 {
|
|
log.I.F("extra '%s'", msg)
|
|
}
|
|
|
|
// Check if sprocket is enabled and process event through it
|
|
if l.sprocketManager != nil && l.sprocketManager.IsEnabled() {
|
|
if l.sprocketManager.IsDisabled() {
|
|
// Sprocket is disabled due to failure - reject all events
|
|
log.W.F("sprocket is disabled, rejecting event %0x", env.E.ID)
|
|
if err = Ok.Error(
|
|
l, env,
|
|
"sprocket disabled - events rejected until sprocket is restored",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
if !l.sprocketManager.IsRunning() {
|
|
// Sprocket is enabled but not running - reject all events
|
|
log.W.F(
|
|
"sprocket is enabled but not running, rejecting event %0x",
|
|
env.E.ID,
|
|
)
|
|
if err = Ok.Error(
|
|
l, env,
|
|
"sprocket not running - events rejected until sprocket starts",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// Process event through sprocket
|
|
response, sprocketErr := l.sprocketManager.ProcessEvent(env.E)
|
|
if chk.E(sprocketErr) {
|
|
log.E.F("sprocket processing failed: %v", sprocketErr)
|
|
if err = Ok.Error(
|
|
l, env, "sprocket processing failed",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// Handle sprocket response
|
|
switch response.Action {
|
|
case "accept":
|
|
// Continue with normal processing
|
|
log.D.F("sprocket accepted event %0x", env.E.ID)
|
|
case "reject":
|
|
// Return OK false with message
|
|
if err = okenvelope.NewFrom(
|
|
env.Id(), false,
|
|
reason.Error.F(response.Msg),
|
|
).Write(l); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
case "shadowReject":
|
|
// Return OK true but abort processing
|
|
if err = Ok.Ok(l, env, ""); chk.E(err) {
|
|
return
|
|
}
|
|
log.D.F("sprocket shadow rejected event %0x", env.E.ID)
|
|
return
|
|
default:
|
|
log.W.F("unknown sprocket action: %s", response.Action)
|
|
// Default to accept for unknown actions
|
|
}
|
|
}
|
|
|
|
// Check if policy is enabled and process event through it
|
|
if l.policyManager.IsEnabled() {
|
|
|
|
// Check policy for write access
|
|
allowed, policyErr := l.policyManager.CheckPolicy("write", env.E, l.authedPubkey.Load(), l.remote)
|
|
if chk.E(policyErr) {
|
|
log.E.F("policy check failed: %v", policyErr)
|
|
if err = Ok.Error(
|
|
l, env, "policy check failed",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
if !allowed {
|
|
log.D.F("policy rejected event %0x", env.E.ID)
|
|
if err = Ok.Blocked(
|
|
l, env, "event blocked by policy",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
log.D.F("policy allowed event %0x", env.E.ID)
|
|
|
|
// Check ACL policy for managed ACL mode, but skip for peer relay sync events
|
|
if acl.Registry.Active.Load() == "managed" && !l.isPeerRelayPubkey(l.authedPubkey.Load()) {
|
|
allowed, aclErr := acl.Registry.CheckPolicy(env.E)
|
|
if chk.E(aclErr) {
|
|
log.E.F("ACL policy check failed: %v", aclErr)
|
|
if err = Ok.Error(
|
|
l, env, "ACL policy check failed",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
if !allowed {
|
|
log.D.F("ACL policy rejected event %0x", env.E.ID)
|
|
if err = Ok.Blocked(
|
|
l, env, "event blocked by ACL policy",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
log.D.F("ACL policy allowed event %0x", env.E.ID)
|
|
}
|
|
}
|
|
|
|
// check the event ID is correct
|
|
calculatedId := env.E.GetIDBytes()
|
|
if !utils.FastEqual(calculatedId, env.E.ID) {
|
|
if err = Ok.Invalid(
|
|
l, env, "event id is computed incorrectly, "+
|
|
"event has ID %0x, but when computed it is %0x",
|
|
env.E.ID, calculatedId,
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
// validate timestamp - reject events too far in the future (more than 1 hour)
|
|
now := time.Now().Unix()
|
|
if env.E.CreatedAt > now+3600 {
|
|
if err = Ok.Invalid(
|
|
l, env,
|
|
"timestamp too far in the future",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// verify the signature
|
|
var ok bool
|
|
if ok, err = env.Verify(); chk.T(err) {
|
|
if err = Ok.Error(
|
|
l, env, fmt.Sprintf(
|
|
"failed to verify signature: %s",
|
|
err.Error(),
|
|
),
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
} else if !ok {
|
|
if err = Ok.Invalid(
|
|
l, env,
|
|
"signature is invalid",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// Handle NIP-43 special events before ACL checks
|
|
switch env.E.Kind {
|
|
case nip43.KindJoinRequest:
|
|
// Process join request and return early
|
|
if err = l.HandleNIP43JoinRequest(env.E); chk.E(err) {
|
|
log.E.F("failed to process NIP-43 join request: %v", err)
|
|
}
|
|
return
|
|
case nip43.KindLeaveRequest:
|
|
// Process leave request and return early
|
|
if err = l.HandleNIP43LeaveRequest(env.E); chk.E(err) {
|
|
log.E.F("failed to process NIP-43 leave request: %v", err)
|
|
}
|
|
return
|
|
case kind.PolicyConfig.K:
|
|
// Handle policy configuration update events (kind 12345)
|
|
// Only policy admins can update policy configuration
|
|
if err = l.HandlePolicyConfigUpdate(env.E); chk.E(err) {
|
|
log.E.F("failed to process policy config update: %v", err)
|
|
if err = Ok.Error(l, env, err.Error()); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
// Send OK response
|
|
if err = Ok.Ok(l, env, "policy configuration updated"); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
case kind.FollowList.K:
|
|
// Check if this is a follow list update from a policy admin
|
|
// If so, refresh the policy follows cache immediately
|
|
if l.IsPolicyAdminFollowListEvent(env.E) {
|
|
// Process the follow list update (async, don't block)
|
|
go func() {
|
|
if updateErr := l.HandlePolicyAdminFollowListUpdate(env.E); updateErr != nil {
|
|
log.W.F("failed to update policy follows from admin follow list: %v", updateErr)
|
|
}
|
|
}()
|
|
}
|
|
// Continue with normal follow list processing (store the event)
|
|
}
|
|
|
|
// check permissions of user
|
|
log.I.F(
|
|
"HandleEvent: checking ACL permissions for pubkey: %s",
|
|
hex.Enc(l.authedPubkey.Load()),
|
|
)
|
|
|
|
// If ACL mode is "none" and no pubkey is set, use the event's pubkey
|
|
// But if auth is required or AuthToWrite is enabled, always use the authenticated pubkey
|
|
var pubkeyForACL []byte
|
|
if len(l.authedPubkey.Load()) == 0 && acl.Registry.Active.Load() == "none" && !l.Config.AuthRequired && !l.Config.AuthToWrite {
|
|
pubkeyForACL = env.E.Pubkey
|
|
log.I.F(
|
|
"HandleEvent: ACL mode is 'none' and auth not required, using event pubkey for ACL check: %s",
|
|
hex.Enc(pubkeyForACL),
|
|
)
|
|
} else {
|
|
pubkeyForACL = l.authedPubkey.Load()
|
|
}
|
|
|
|
// If auth is required or AuthToWrite is enabled but user is not authenticated, deny access
|
|
if (l.Config.AuthRequired || l.Config.AuthToWrite) && len(l.authedPubkey.Load()) == 0 {
|
|
log.D.F("HandleEvent: authentication required for write operations but user not authenticated")
|
|
if err = okenvelope.NewFrom(
|
|
env.Id(), false,
|
|
reason.AuthRequired.F("authentication required for write operations"),
|
|
).Write(l); chk.E(err) {
|
|
return
|
|
}
|
|
// Send AUTH challenge to prompt authentication
|
|
log.D.F("HandleEvent: sending AUTH challenge to %s", l.remote)
|
|
if err = authenvelope.NewChallengeWith(l.challenge.Load()).
|
|
Write(l); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
accessLevel := acl.Registry.GetAccessLevel(pubkeyForACL, l.remote)
|
|
log.I.F("HandleEvent: ACL access level: %s", accessLevel)
|
|
|
|
// Skip ACL check for admin/owner delete events
|
|
skipACLCheck := false
|
|
if env.E.Kind == kind.EventDeletion.K {
|
|
// Check if the delete event signer is admin or owner
|
|
for _, admin := range l.Admins {
|
|
if utils.FastEqual(admin, env.E.Pubkey) {
|
|
skipACLCheck = true
|
|
log.I.F("HandleEvent: admin delete event - skipping ACL check")
|
|
break
|
|
}
|
|
}
|
|
if !skipACLCheck {
|
|
for _, owner := range l.Owners {
|
|
if utils.FastEqual(owner, env.E.Pubkey) {
|
|
skipACLCheck = true
|
|
log.I.F("HandleEvent: owner delete event - skipping ACL check")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !skipACLCheck {
|
|
switch accessLevel {
|
|
case "none":
|
|
log.D.F(
|
|
"handle event: sending 'OK,false,auth-required...' to %s",
|
|
l.remote,
|
|
)
|
|
if err = okenvelope.NewFrom(
|
|
env.Id(), false,
|
|
reason.AuthRequired.F("auth required for write access"),
|
|
).Write(l); chk.E(err) {
|
|
// return
|
|
}
|
|
log.D.F("handle event: sending challenge to %s", l.remote)
|
|
if err = authenvelope.NewChallengeWith(l.challenge.Load()).
|
|
Write(l); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
case "read":
|
|
log.D.F(
|
|
"handle event: sending 'OK,false,auth-required:...' to %s",
|
|
l.remote,
|
|
)
|
|
if err = okenvelope.NewFrom(
|
|
env.Id(), false,
|
|
reason.AuthRequired.F("auth required for write access"),
|
|
).Write(l); chk.E(err) {
|
|
return
|
|
}
|
|
log.D.F("handle event: sending challenge to %s", l.remote)
|
|
if err = authenvelope.NewChallengeWith(l.challenge.Load()).
|
|
Write(l); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
case "blocked":
|
|
log.D.F(
|
|
"handle event: sending 'OK,false,blocked...' to %s",
|
|
l.remote,
|
|
)
|
|
if err = okenvelope.NewFrom(
|
|
env.Id(), false,
|
|
reason.AuthRequired.F("IP address blocked"),
|
|
).Write(l); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
case "banned":
|
|
log.D.F(
|
|
"handle event: sending 'OK,false,banned...' to %s",
|
|
l.remote,
|
|
)
|
|
if err = okenvelope.NewFrom(
|
|
env.Id(), false,
|
|
reason.AuthRequired.F("pubkey banned"),
|
|
).Write(l); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
default:
|
|
// user has write access or better, continue
|
|
log.I.F("HandleEvent: user has %s access, continuing", accessLevel)
|
|
}
|
|
} else {
|
|
log.I.F("HandleEvent: skipping ACL check for admin/owner delete event")
|
|
}
|
|
|
|
// check if event is ephemeral - if so, deliver and return early
|
|
if kind.IsEphemeral(env.E.Kind) {
|
|
log.D.F("handling ephemeral event %0x (kind %d)", env.E.ID, env.E.Kind)
|
|
// Send OK response for ephemeral events
|
|
if err = Ok.Ok(l, env, ""); chk.E(err) {
|
|
return
|
|
}
|
|
// Deliver the event to subscribers immediately
|
|
clonedEvent := env.E.Clone()
|
|
go l.publishers.Deliver(clonedEvent)
|
|
log.D.F("delivered ephemeral event %0x", env.E.ID)
|
|
return
|
|
}
|
|
log.D.F("processing regular event %0x (kind %d)", env.E.ID, env.E.Kind)
|
|
|
|
// check for protected tag (NIP-70)
|
|
protectedTag := env.E.Tags.GetFirst([]byte("-"))
|
|
if protectedTag != nil && acl.Registry.Active.Load() != "none" {
|
|
// check that the pubkey of the event matches the authed pubkey
|
|
if !utils.FastEqual(l.authedPubkey.Load(), env.E.Pubkey) {
|
|
if err = Ok.Blocked(
|
|
l, env,
|
|
"protected tag may only be published by user authed to the same pubkey",
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
}
|
|
// if the event is a delete, process the delete
|
|
log.I.F(
|
|
"HandleEvent: checking if event is delete - kind: %d, EventDeletion.K: %d",
|
|
env.E.Kind, kind.EventDeletion.K,
|
|
)
|
|
if env.E.Kind == kind.EventDeletion.K {
|
|
log.I.F("processing delete event %0x", env.E.ID)
|
|
|
|
// Store the delete event itself FIRST to ensure it's available for queries
|
|
saveCtx, cancel := context.WithTimeout(
|
|
context.Background(), 30*time.Second,
|
|
)
|
|
defer cancel()
|
|
log.I.F(
|
|
"attempting to save delete event %0x from pubkey %0x", env.E.ID,
|
|
env.E.Pubkey,
|
|
)
|
|
log.I.F("delete event pubkey hex: %s", hex.Enc(env.E.Pubkey))
|
|
if _, err = l.DB.SaveEvent(saveCtx, env.E); err != nil {
|
|
log.E.F("failed to save delete event %0x: %v", env.E.ID, err)
|
|
if strings.HasPrefix(err.Error(), "blocked:") {
|
|
errStr := err.Error()[len("blocked: "):len(err.Error())]
|
|
if err = Ok.Error(
|
|
l, env, errStr,
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
chk.E(err)
|
|
return
|
|
}
|
|
log.I.F("successfully saved delete event %0x", env.E.ID)
|
|
|
|
// Now process the deletion (remove target events)
|
|
if err = l.HandleDelete(env); err != nil {
|
|
log.E.F("HandleDelete failed for event %0x: %v", env.E.ID, err)
|
|
if strings.HasPrefix(err.Error(), "blocked:") {
|
|
errStr := err.Error()[len("blocked: "):len(err.Error())]
|
|
if err = Ok.Error(
|
|
l, env, errStr,
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
// For non-blocked errors, still send OK but log the error
|
|
log.W.F("Delete processing failed but continuing: %v", err)
|
|
} else {
|
|
log.I.F(
|
|
"HandleDelete completed successfully for event %0x", env.E.ID,
|
|
)
|
|
}
|
|
|
|
// Send OK response for delete events
|
|
if err = Ok.Ok(l, env, ""); chk.E(err) {
|
|
return
|
|
}
|
|
|
|
// Deliver the delete event to subscribers
|
|
clonedEvent := env.E.Clone()
|
|
go l.publishers.Deliver(clonedEvent)
|
|
log.D.F("processed delete event %0x", env.E.ID)
|
|
return
|
|
} else {
|
|
// check if the event was deleted
|
|
// Skip deletion check when ACL is "none" (open relay mode)
|
|
if acl.Registry.Active.Load() != "none" {
|
|
// Combine admins and owners for deletion checking
|
|
adminOwners := append(l.Admins, l.Owners...)
|
|
if err = l.DB.CheckForDeleted(env.E, adminOwners); err != nil {
|
|
if strings.HasPrefix(err.Error(), "blocked:") {
|
|
errStr := err.Error()[len("blocked: "):len(err.Error())]
|
|
if err = Ok.Error(
|
|
l, env, errStr,
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// store the event - use a separate context to prevent cancellation issues
|
|
saveCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
// log.I.F("saving event %0x, %s", env.E.ID, env.E.Serialize())
|
|
if _, err = l.DB.SaveEvent(saveCtx, env.E); err != nil {
|
|
if strings.HasPrefix(err.Error(), "blocked:") {
|
|
errStr := err.Error()[len("blocked: "):len(err.Error())]
|
|
if err = Ok.Error(
|
|
l, env, errStr,
|
|
); chk.E(err) {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
chk.E(err)
|
|
return
|
|
}
|
|
|
|
// Handle relay group configuration events
|
|
if l.relayGroupMgr != nil {
|
|
if err := l.relayGroupMgr.ValidateRelayGroupEvent(env.E); err != nil {
|
|
log.W.F("invalid relay group config event %s: %v", hex.Enc(env.E.ID), err)
|
|
}
|
|
// Process the event and potentially update peer lists
|
|
if l.syncManager != nil {
|
|
l.relayGroupMgr.HandleRelayGroupEvent(env.E, l.syncManager)
|
|
}
|
|
}
|
|
|
|
// Handle cluster membership events (Kind 39108)
|
|
if env.E.Kind == 39108 && l.clusterManager != nil {
|
|
if err := l.clusterManager.HandleMembershipEvent(env.E); err != nil {
|
|
log.W.F("invalid cluster membership event %s: %v", hex.Enc(env.E.ID), err)
|
|
}
|
|
}
|
|
|
|
// Update serial for distributed synchronization
|
|
if l.syncManager != nil {
|
|
l.syncManager.UpdateSerial()
|
|
log.D.F("updated serial for event %s", hex.Enc(env.E.ID))
|
|
}
|
|
// Send a success response storing
|
|
if err = Ok.Ok(l, env, ""); chk.E(err) {
|
|
return
|
|
}
|
|
// Deliver the event to subscribers immediately after sending OK response
|
|
// Clone the event to prevent corruption when the original is freed
|
|
clonedEvent := env.E.Clone()
|
|
go l.publishers.Deliver(clonedEvent)
|
|
log.D.F("saved event %0x", env.E.ID)
|
|
var isNewFromAdmin bool
|
|
// Check if event is from admin or owner
|
|
for _, admin := range l.Admins {
|
|
if utils.FastEqual(admin, env.E.Pubkey) {
|
|
isNewFromAdmin = true
|
|
break
|
|
}
|
|
}
|
|
if !isNewFromAdmin {
|
|
for _, owner := range l.Owners {
|
|
if utils.FastEqual(owner, env.E.Pubkey) {
|
|
isNewFromAdmin = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if isNewFromAdmin {
|
|
log.I.F("new event from admin %0x", env.E.Pubkey)
|
|
// if a follow list was saved, reconfigure ACLs now that it is persisted
|
|
if env.E.Kind == kind.FollowList.K ||
|
|
env.E.Kind == kind.RelayListMetadata.K {
|
|
// Run ACL reconfiguration asynchronously to prevent blocking websocket operations
|
|
go func() {
|
|
if err := acl.Registry.Configure(); chk.E(err) {
|
|
log.E.F("failed to reconfigure ACL: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// isPeerRelayPubkey checks if the given pubkey belongs to a peer relay
|
|
func (l *Listener) isPeerRelayPubkey(pubkey []byte) bool {
|
|
if l.syncManager == nil {
|
|
return false
|
|
}
|
|
|
|
peerPubkeyHex := hex.Enc(pubkey)
|
|
|
|
// Check if this pubkey matches any of our configured peer relays' NIP-11 pubkeys
|
|
for _, peerURL := range l.syncManager.GetPeers() {
|
|
if l.syncManager.IsAuthorizedPeer(peerURL, peerPubkeyHex) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|