tinyrelayfile
tinyrelay/internal/gates/gates.go/

internal/gates/gates.go raw
1package gates
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8 "sync"
9
10 "github.com/FelineStateMachine/tinyrelay/internal/community"
11 "github.com/FelineStateMachine/tinyrelay/internal/event"
12 "github.com/FelineStateMachine/tinyrelay/internal/policy"
13 "github.com/FelineStateMachine/tinyrelay/internal/relay"
14 "github.com/FelineStateMachine/tinyrelay/internal/storage"
15 "github.com/dlclark/regexp2"
16)
17
18// Config supplies the policy and optional durable services used by Gate.
19// Store and Community enable checks that depend on persisted relay state.
20type Config struct {
21 Store *storage.Store
22 Community *community.Service
23 Policy func() policy.Policy
24 Slug string
25 ArtifactVisible func(context.Context, event.Event, relay.Session) bool
26}
27
28// Gate evaluates event admission and visibility for one tenant configuration.
29type Gate struct {
30 cfg Config
31 agents agentLimiter
32}
33type artifactCheckContextKey struct{}
34type artifactCheckState struct {
35 seen map[string]struct{}
36 depth int
37}
38
39// agentLimiter is the per-agent sliding one-minute counter. It lives in
40// memory: a restart forgives the last minute, which is the cheap and honest
41// trade for a limit that never touches the database on the hot path.
42type agentLimiter struct {
43 mu sync.Mutex
44 recent map[string][]int64
45}
46
47func (l *agentLimiter) allow(agent string, limit int, now int64) bool {
48 l.mu.Lock()
49 defer l.mu.Unlock()
50 if l.recent == nil {
51 l.recent = map[string][]int64{}
52 }
53 kept := l.recent[agent][:0]
54 for _, at := range l.recent[agent] {
55 if at > now-60 {
56 kept = append(kept, at)
57 }
58 }
59 if len(kept) >= limit {
60 l.recent[agent] = kept
61 return false
62 }
63 l.recent[agent] = append(kept, now)
64 return true
65}
66
67// agentAdmission applies an agent grant to an event from a key whose only
68// standing here is that grant. Keys with a human role keep that role's
69// permissions. Events from agents are never stamped or rewritten; they are
70// kept as signed or refused with a restricted reason.
71func (g *Gate) agentAdmission(ctx context.Context, e event.Event, now int64) error {
72 if g.cfg.Community == nil {
73 return nil
74 }
75 grant, ok, err := g.cfg.Community.AgentGrant(ctx, e.PubKey)
76 if err != nil {
77 return fmt.Errorf("check agent grant: %w", err)
78 }
79 if !ok {
80 if community.IsAgentGrantRequest(e) {
81 return errors.New("restricted: grant requests require an active agent grant")
82 }
83 return nil
84 }
85 role, err := g.cfg.Community.Role(ctx, e.PubKey)
86 if err != nil {
87 return fmt.Errorf("check agent role: %w", err)
88 }
89 if role != "" && role != "agent" {
90 if community.IsAgentGrantRequest(e) {
91 return errors.New("restricted: only the granted agent may request a grant change")
92 }
93 return nil
94 }
95 if community.IsAgentGrantRequest(e) {
96 if _, reviewErr := g.cfg.Community.ReviewAgentGrantRequest(ctx, e, now); reviewErr != nil {
97 return reviewErr
98 }
99 }
100 if !community.IsAgentGrantRequest(e) {
101 if err := grant.Check(e, now); err != nil {
102 return err
103 }
104 }
105 if err := g.jobReply(ctx, e, now, true); err != nil {
106 return err
107 }
108 if !g.agents.allow(e.PubKey, grant.Scope.Rate, now) {
109 return fmt.Errorf("restricted: agent grant does not allow more than %d events per minute", grant.Scope.Rate)
110 }
111 return nil
112}
113
114// agentGrantShape checks a grant event's signer and tags before it is stored.
115func (g *Gate) agentGrantShape(ctx context.Context, e event.Event, now int64) error {
116 if e.Kind != event.KIND_AGENT_GRANT {
117 return nil
118 }
119 if g.cfg.Community == nil {
120 return errors.New("restricted: agent grants need the membership service")
121 }
122 role, err := g.cfg.Community.Role(ctx, e.PubKey)
123 if err != nil {
124 return fmt.Errorf("check grant signer: %w", err)
125 }
126 if !community.CanGrantAgents(role) {
127 return errors.New("restricted: only the owner or a moderator can grant an agent")
128 }
129 if event.Tag(e, "grant-request") != "" || event.Tag(e, "grant-base") != "" {
130 if err := g.cfg.Community.ValidateAgentGrantReplacement(ctx, e, now); err != nil {
131 return err
132 }
133 }
134 _, err = community.ParseAgentGrant(e, now)
135 return err
136}
137
138func New(cfg Config) (*Gate, error) {
139 if cfg.Policy == nil {
140 return nil, errors.New("gates: policy function is required")
141 }
142 return &Gate{cfg: cfg}, nil
143}
144
145// Write validates a client-published event against protocol shape, tenant
146// policy, membership, bans, room access, retention and proof-of-work rules.
147func (g *Gate) Write(ctx context.Context, e event.Event, s relay.Session, now int64) error {
148 p := g.cfg.Policy()
149 if e.Kind == event.KIND_AUTH {
150 return errors.New("blocked: kind 22242 is only accepted inside AUTH")
151 }
152 if e.Kind == event.KIND_MARMOT_GROUP || e.Kind == event.KIND_MARMOT_KEY_PACKAGE {
153 if !p.Features.Marmot {
154 return errors.New("restricted: Marmot transport is switched off on this relay")
155 }
156 if err := MarmotShape(e); err != nil {
157 return err
158 }
159 if e.Kind == event.KIND_MARMOT_GROUP {
160 if contains(s.PubKeys, e.PubKey) {
161 return errors.New("invalid: kind 445 must use a fresh ephemeral author")
162 }
163 if g.cfg.Store != nil {
164 result, err := g.cfg.Store.Query(ctx, event.Filter{Kinds: []int{event.KIND_MARMOT_GROUP}, Authors: []string{e.PubKey}}, storage.QueryOptions{Now: now, Access: storage.Access{All: true}, Limit: 1})
165 if err != nil {
166 return fmt.Errorf("check Marmot ephemeral author: %w", err)
167 }
168 if len(result.Events) > 0 {
169 return errors.New("invalid: kind 445 ephemeral author was already used")
170 }
171 }
172 }
173 }
174 if err := event.Validate(e); err != nil {
175 return err
176 }
177 if err := nip43Shape(e, now); err != nil {
178 return err
179 }
180 if err := jobFeature(p, e); err != nil {
181 return err
182 }
183 if err := JobShape(e); err != nil {
184 return err
185 }
186 if p.MaxFuture > 0 && e.CreatedAt > now+p.MaxFuture {
187 return errors.New("invalid: event creation date is too far off from the current time")
188 }
189 if exp := event.Expiration(e); exp > 0 && exp <= now && e.Kind != event.KIND_AGENT_GRANT {
190 return errors.New("invalid: event has already expired")
191 }
192 if g.cfg.Community != nil {
193 if s.RemoteIP != "" {
194 blocked, err := g.cfg.Community.IsIPBlocked(ctx, s.RemoteIP)
195 if err != nil {
196 return fmt.Errorf("check IP block: %w", err)
197 }
198 if blocked {
199 return errors.New("blocked: this IP address is blocked")
200 }
201 }
202 banned, err := g.cfg.Community.IsBanned(ctx, e.PubKey)
203 if err != nil {
204 return fmt.Errorf("check pubkey ban: %w", err)
205 }
206 if banned {
207 return errors.New("blocked: this pubkey is banned")
208 }
209 banned, err = g.cfg.Community.IsEventBanned(ctx, e.ID)
210 if err != nil {
211 return fmt.Errorf("check event ban: %w", err)
212 }
213 if banned {
214 return errors.New("blocked: this event is banned")
215 }
216 }
217 if err := g.agentGrantShape(ctx, e, now); err != nil {
218 return err
219 }
220 if err := g.agentAdmission(ctx, e, now); err != nil {
221 return err
222 }
223 if where := blockedWord(p, e); where != "" && !g.isModerator(ctx, e.PubKey) {
224 return errors.New("blocked: " + where)
225 }
226 if e.Kind == event.KIND_NOSTR_CONNECT {
227 if !p.Features.Signer {
228 return errors.New("restricted: signer traffic is switched off")
229 }
230 return nil
231 }
232 if err := g.roomWrite(ctx, e); err != nil {
233 return err
234 }
235 if p.Inbox.Targeted && !inboxAdmission(p, e) {
236 return errors.New("blocked: inbox event must target the relay owner")
237 }
238 if hasTag(e, "-") && !contains(s.PubKeys, e.PubKey) {
239 return errors.New("auth-required: this event may only be published by its author")
240 }
241 writeAccess := accessFor(ctx, g.cfg.Community, p, relay.Session{PubKeys: []string{e.PubKey}})
242 if p.Writes == "wot" && !writeAccess.Member && !writeAccess.Owner && !g.wotAllows(ctx, e.PubKey, p.Owner) {
243 return errors.New("restricted: this relay only accepts events from its members and the people they follow")
244 }
245 if e.Kind == event.KIND_MARMOT_GROUP {
246 principal, err := g.Principal(ctx, e, s)
247 if err != nil {
248 return err
249 }
250 if g.cfg.Community != nil {
251 banned, banErr := g.cfg.Community.IsBanned(ctx, principal)
252 if banErr != nil {
253 return fmt.Errorf("check Marmot principal ban: %w", banErr)
254 }
255 if banned {
256 return errors.New("blocked: Marmot account is banned")
257 }
258 }
259 writeAccess = accessFor(ctx, g.cfg.Community, p, relay.Session{PubKeys: []string{principal}})
260 }
261 if err := jobWriter(e, writeAccess); err != nil {
262 return err
263 }
264 if err := g.jobReply(ctx, e, now, false); err != nil {
265 return err
266 }
267 ownerReplaceable := writeAccess.Owner && (event.IsReplaceable(e.Kind) || event.IsAddressable(e.Kind))
268 allowed, blocked, rulesErr := g.kindRules(ctx, p, e.Kind)
269 if rulesErr != nil {
270 return fmt.Errorf("read kind rules: %w", rulesErr)
271 }
272 if blocked || !allowed {
273 if !ownerReplaceable {
274 return errors.New("blocked: this relay does not accept this event kind")
275 }
276 }
277 membershipEvent := e.Kind == event.KIND_JOIN || e.Kind == event.KIND_LEAVE || e.Kind == event.KIND_NIP43_JOIN || e.Kind == event.KIND_NIP43_LEAVE
278 if !ownerReplaceable && !membershipEvent && !policy.CanWrite(p, e, writeAccess) && !g.guestPass(ctx, p, e, writeAccess, now) {
279 return errors.New("blocked: this event is not allowed by the relay write policy")
280 }
281 if days := g.RetentionDays(ctx, e.Kind); days > 0 && !protectedKind(e.Kind) && !(ownerReplaceable) && e.CreatedAt < now-int64(days)*86400 {
282 return errors.New("blocked: event is older than this relay's retention window")
283 }
284 if e.Kind == event.KIND_WRAP && g.isDMInbox(ctx, p) && g.cfg.Community != nil && len(event.TagValues(e, "p")) > 0 {
285 valid := false
286 for _, recipient := range event.TagValues(e, "p") {
287 member, err := g.cfg.Community.IsMember(ctx, recipient)
288 if err == nil && member {
289 valid = true
290 }
291 }
292 if !valid {
293 return errors.New("blocked: gift wrap recipient is not local")
294 }
295 }
296 if p.MinPow > 0 && event.Difficulty(e.ID) < p.MinPow {
297 return fmt.Errorf("pow: difficulty %d is less than %d", event.Difficulty(e.ID), p.MinPow)
298 }
299 if p.MinPow > 0 && event.CommittedDifficulty(e) > 0 && event.CommittedDifficulty(e) < p.MinPow {
300 return fmt.Errorf("pow: committed target %d is less than %d", event.CommittedDifficulty(e), p.MinPow)
301 }
302 return nil
303}
304
305func inboxAdmission(p policy.Policy, e event.Event) bool {
306 if e.PubKey == p.Owner && (e.Kind == 0 || e.Kind == 3 || e.Kind == 10002) {
307 return true
308 }
309 for _, tag := range e.Tags {
310 if len(tag) >= 2 && tag[0] == "p" && tag[1] == p.Owner {
311 return true
312 }
313 }
314 return false
315}
316
317// Import validates an event received from another host using protocol shape,
318// safety, ban, agent and room existence checks. The caller records its origin
319// in storage after this common gate succeeds.
320func (g *Gate) Import(ctx context.Context, e event.Event, now int64) error {
321 p := g.cfg.Policy()
322 if err := event.Validate(e); err != nil {
323 return err
324 }
325 if err := nip43Shape(e, now); err != nil {
326 return err
327 }
328 if err := jobFeature(p, e); err != nil {
329 return err
330 }
331 if err := JobShape(e); err != nil {
332 return err
333 }
334 if p.MaxFuture > 0 && e.CreatedAt > now+p.MaxFuture {
335 return errors.New("invalid: event creation date is too far off from the current time")
336 }
337 if exp := event.Expiration(e); exp > 0 && exp <= now && e.Kind != event.KIND_AGENT_GRANT {
338 return errors.New("invalid: event has already expired")
339 }
340 if g.cfg.Community != nil {
341 banned, err := g.cfg.Community.IsBanned(ctx, e.PubKey)
342 if err != nil {
343 return fmt.Errorf("check pubkey ban: %w", err)
344 }
345 if banned {
346 return errors.New("blocked: this pubkey is banned")
347 }
348 banned, err = g.cfg.Community.IsEventBanned(ctx, e.ID)
349 if err != nil {
350 return fmt.Errorf("check event ban: %w", err)
351 }
352 if banned {
353 return errors.New("blocked: this event is banned")
354 }
355 }
356 if err := g.agentGrantShape(ctx, e, now); err != nil {
357 return err
358 }
359 if err := g.agentAdmission(ctx, e, now); err != nil {
360 return err
361 }
362 if where := blockedWord(p, e); where != "" && !g.isModerator(ctx, e.PubKey) {
363 return errors.New("blocked: " + where)
364 }
365 if e.Kind == event.KIND_AUTH {
366 return errors.New("blocked: kind 22242 is only accepted inside AUTH")
367 }
368 if e.Kind == event.KIND_NOSTR_CONNECT && !p.Features.Signer {
369 return errors.New("restricted: signer traffic is switched off")
370 }
371 if e.Kind == event.KIND_MARMOT_GROUP || e.Kind == event.KIND_MARMOT_KEY_PACKAGE {
372 if !p.Features.Marmot {
373 return errors.New("restricted: Marmot transport is switched off on this relay")
374 }
375 if err := MarmotShape(e); err != nil {
376 return err
377 }
378 }
379 if err := g.roomExists(ctx, e); err != nil {
380 return err
381 }
382 return nil
383}
384
385// roomID names the room an event belongs to, or nothing for events outside
386// the room dialect. Marmot group messages reuse the h tag for other ends.
387func (g *Gate) roomID(e event.Event) string {
388 if e.Kind == event.KIND_MARMOT_GROUP {
389 return ""
390 }
391 return event.Tag(e, "h")
392}
393
394// roomExists is the common room check: an h tag must name the slug room or
395// a live room. Room creation is the one event allowed to name a new room.
396func (g *Gate) roomExists(ctx context.Context, e event.Event) error {
397 id := g.roomID(e)
398 if id == "" || id == g.cfg.Slug || e.Kind == event.KIND_CREATE_GROUP {
399 return nil
400 }
401 if g.cfg.Community == nil {
402 return fmt.Errorf("blocked: this relay hosts one group: %s", g.cfg.Slug)
403 }
404 room, err := g.cfg.Community.Room(ctx, id)
405 if err != nil {
406 return fmt.Errorf("check room: %w", err)
407 }
408 if !room.Live() {
409 return fmt.Errorf("invalid: unknown room %s", id)
410 }
411 return nil
412}
413
414// roomWrite applies room membership to client writes. The slug room keeps
415// the tenant's own rules. In other rooms, management events are checked by
416// the room handler; every other event needs room membership, which an open
417// room grants to any tenant member.
418func (g *Gate) roomWrite(ctx context.Context, e event.Event) error {
419 if err := g.roomExists(ctx, e); err != nil {
420 return err
421 }
422 id := g.roomID(e)
423 if id != "" && community.RoomNoticeKind(e.Kind) {
424 return errors.New("blocked: member notices are signed by the relay")
425 }
426 if id == "" || id == g.cfg.Slug || e.Kind == event.KIND_CREATE_GROUP || community.RoomAdminKind(e.Kind) {
427 return nil
428 }
429 allowed, _, err := g.cfg.Community.RoomWriteAllowed(ctx, id, e.PubKey)
430 if err != nil {
431 return fmt.Errorf("check room membership: %w", err)
432 }
433 if !allowed {
434 return fmt.Errorf("restricted: not a member of room %s", id)
435 }
436 return nil
437}
438
439// roomVisible applies room access to reads. Members-only rooms, including
440// the relay's own 39000 to 39002 records for them, are served only to room
441// members, the tenant owner and moderators.
442func (g *Gate) roomVisible(ctx context.Context, e event.Event, s relay.Session) bool {
443 id := g.roomID(e)
444 if id == "" && (e.Kind == event.KIND_GROUP_METADATA || e.Kind == event.KIND_GROUP_ADMINS || e.Kind == event.KIND_GROUP_MEMBERS) {
445 id = event.Tag(e, "d")
446 }
447 if id == "" || id == g.cfg.Slug || g.cfg.Community == nil {
448 return true
449 }
450 visible, err := g.cfg.Community.RoomVisible(ctx, id, s.PubKeys)
451 return err == nil && visible
452}
453
454// membersOnlyRoomFilter reports whether a filter asks for a members-only
455// room, so an unauthenticated subscriber is told to AUTH instead of waiting
456// on an empty stream.
457func (g *Gate) membersOnlyRoomFilter(ctx context.Context, f event.Filter) bool {
458 if g.cfg.Community == nil {
459 return false
460 }
461 for _, id := range f.Tags["h"] {
462 if id == g.cfg.Slug {
463 continue
464 }
465 room, err := g.cfg.Community.Room(ctx, id)
466 if err == nil && room.Live() && room.Access == community.RoomMembers {
467 return true
468 }
469 }
470 return false
471}
472
473func (g *Gate) kindRules(ctx context.Context, p policy.Policy, kind int) (bool, bool, error) {
474 if g.cfg.Community == nil {
475 return len(p.AllowedKinds) == 0 || containsInt(p.AllowedKinds, kind), containsInt(p.BlockedKinds, kind), nil
476 }
477 allowedAny, blocked := false, false
478 value, err := g.cfg.Community.Execute(ctx, p.Owner, "listallowedkinds", nil)
479 if err != nil {
480 return false, false, err
481 }
482 if values, ok := value.([]int); ok {
483 allowedAny = len(values) == 0 || containsInt(values, kind)
484 } else {
485 allowedAny = true
486 }
487 value, err = g.cfg.Community.Execute(ctx, p.Owner, "listblockedkinds", nil)
488 if err != nil {
489 return false, false, err
490 }
491 if values, ok := value.([]int); ok {
492 blocked = containsInt(values, kind)
493 }
494 return allowedAny, blocked, nil
495}
496
497// RetentionDays returns the configured retention period for kind, using the
498// wildcard rule when no exact rule exists.
499func (g *Gate) RetentionDays(ctx context.Context, kind int) int {
500 if g.cfg.Community == nil {
501 wildcard := 0
502 for _, rule := range g.cfg.Policy().Retention {
503 if rule.Kind == kind {
504 return rule.Days
505 }
506 if rule.Kind < 0 {
507 wildcard = rule.Days
508 }
509 }
510 return wildcard
511 }
512 value, err := g.cfg.Community.Execute(ctx, g.cfg.Policy().Owner, "listretention", nil)
513 if err != nil {
514 return 0
515 }
516 if rules, ok := value.([]map[string]int); ok {
517 wildcard := 0
518 for _, rule := range rules {
519 if rule["kind"] == kind {
520 return rule["days"]
521 }
522 if rule["kind"] < 0 {
523 wildcard = rule["days"]
524 }
525 }
526 return wildcard
527 }
528 return 0
529}
530
531func protectedKind(kind int) bool {
532 switch kind {
533 case 0, 3, 10002, 9735, 13534, 8000, 8001, 9000, 9001, 33534, 39000, 39001, 39002, 39003, 39005:
534 return true
535 }
536 return false
537}
538
539func nip43Shape(e event.Event, now int64) error {
540 if e.Kind != event.KIND_NIP43_JOIN && e.Kind != event.KIND_NIP43_LEAVE {
541 return nil
542 }
543 if delta := e.CreatedAt - now; delta > 300 || delta < -300 {
544 return errors.New("invalid: NIP-43 request is outside the allowed time window")
545 }
546 if e.Kind == event.KIND_NIP43_LEAVE {
547 protected := 0
548 for _, tag := range e.Tags {
549 if len(tag) > 0 && tag[0] == "-" {
550 protected++
551 if len(tag) != 1 {
552 return errors.New("invalid: NIP-43 leave protected tag must be empty")
553 }
554 }
555 }
556 if protected != 1 {
557 return errors.New("invalid: NIP-43 leave needs one protected tag")
558 }
559 }
560 // A join without a claim tag is an access request held for the owner's
561 // review, so the claim stays optional.
562 return nil
563}
564
565func (g *Gate) wotAllows(ctx context.Context, target, owner string) bool {
566 if g.cfg.Store == nil {
567 return false
568 }
569 principals := []string{owner}
570 if g.cfg.Community != nil {
571 members, err := g.cfg.Community.Execute(ctx, owner, "listmembers", nil)
572 if err == nil {
573 if list, ok := members.([]community.Member); ok {
574 for _, member := range list {
575 principals = append(principals, member.PubKey)
576 }
577 }
578 }
579 }
580 seen := map[string]bool{}
581 for _, principal := range principals {
582 if seen[principal] {
583 continue
584 }
585 seen[principal] = true
586 result, err := g.cfg.Store.Query(ctx, event.Filter{Authors: []string{principal}, Kinds: []int{3}}, storage.QueryOptions{Now: 0, Access: storage.Access{All: true}})
587 if err != nil || len(result.Events) == 0 {
588 continue
589 }
590 latest := result.Events[0]
591 for _, tag := range latest.Tags {
592 if len(tag) > 1 && tag[0] == "p" && tag[1] == target {
593 return true
594 }
595 }
596 }
597 return false
598}
599
600func (g *Gate) isDMInbox(ctx context.Context, p policy.Policy) bool {
601 if p.Reads != "members" || g.cfg.Community == nil {
602 return p.Reads == "members" && containsInt(p.AllowedKinds, event.KIND_WRAP)
603 }
604 value, err := g.cfg.Community.Execute(ctx, p.Owner, "listallowedkinds", nil)
605 if err != nil {
606 return false
607 }
608 values, ok := value.([]int)
609 return ok && containsInt(values, event.KIND_WRAP)
610}
611
612func (g *Gate) guestPass(ctx context.Context, p policy.Policy, e event.Event, a policy.Access, now int64) bool {
613 if !a.Member && !a.Owner && p.Writes != "open" {
614 if containsInt(p.OpenKinds, e.Kind) {
615 return true
616 }
617 if !p.GuestReplies || (e.Kind != 1 && e.Kind != 1111) || g.cfg.Store == nil {
618 return false
619 }
620 for _, id := range append(event.TagValues(e, "e"), event.TagValues(e, "E")...) {
621 result, err := g.cfg.Store.Query(ctx, event.Filter{IDs: []string{id}}, storage.QueryOptions{Now: now, Access: storage.Access{All: true}, Limit: 1})
622 if err == nil && len(result.Events) > 0 {
623 author := result.Events[0].PubKey
624 if author == p.Owner {
625 return true
626 }
627 if g.cfg.Community != nil {
628 member, memberErr := g.cfg.Community.IsMember(ctx, author)
629 if memberErr == nil && member {
630 return true
631 }
632 }
633 }
634 }
635 }
636 return false
637}
638
639// Read validates a subscription request and returns an AUTH hint when a
640// subscriber may receive private or members-only results after authenticating.
641func (g *Gate) Read(ctx context.Context, filters []event.Filter, s relay.Session) (bool, error) {
642 p := g.cfg.Policy()
643 if p.Features.Signer && len(filters) > 0 {
644 signerOnly := true
645 for _, f := range filters {
646 if len(f.Kinds) != 1 || f.Kinds[0] != event.KIND_NOSTR_CONNECT {
647 signerOnly = false
648 }
649 }
650 if signerOnly {
651 return false, nil
652 }
653 }
654 if p.Features.Search == "off" {
655 for _, f := range filters {
656 if f.Search != "" {
657 return false, errors.New("unsupported: search is switched off on this relay")
658 }
659 }
660 }
661 a := accessFor(ctx, g.cfg.Community, p, s)
662 if p.Reads == "auth" && len(a.PubKeys) == 0 {
663 return false, errors.New("auth-required: this relay requires AUTH")
664 }
665 if p.Reads == "members" && !a.Member && !a.Owner {
666 return false, errors.New("auth-required: this relay is members-only")
667 }
668 privateOnly := true
669 authHint := false
670 for _, f := range filters {
671 if len(a.PubKeys) == 0 && g.membersOnlyRoomFilter(ctx, f) {
672 authHint = true
673 }
674 if len(f.Kinds) == 0 {
675 privateOnly = false
676 authHint = true
677 continue
678 }
679 seenPrivate := false
680 for _, k := range f.Kinds {
681 if event.IsPrivate(k) {
682 seenPrivate = true
683 } else {
684 privateOnly = false
685 }
686 }
687 if seenPrivate && len(a.PubKeys) == 0 {
688 authHint = true
689 }
690 }
691 if privateOnly && len(a.PubKeys) == 0 {
692 return false, errors.New("auth-required: private kinds are only served to their recipients")
693 }
694 return authHint, nil
695}
696
697// CanSee reports whether session s may receive e. It applies hidden-event,
698// ban, repository privacy, policy, room and recipient checks.
699func (g *Gate) CanSee(ctx context.Context, e event.Event, s relay.Session, f *event.Filter) bool {
700 p := g.cfg.Policy()
701 if e.Kind == event.KIND_VIEW && g.cfg.ArtifactVisible != nil {
702 state, _ := ctx.Value(artifactCheckContextKey{}).(artifactCheckState)
703 if state.seen == nil {
704 state.seen = map[string]struct{}{}
705 }
706 if _, cycle := state.seen[e.ID]; cycle || state.depth >= 32 {
707 return false
708 }
709 seen := make(map[string]struct{}, len(state.seen)+1)
710 for id := range state.seen {
711 seen[id] = struct{}{}
712 }
713 seen[e.ID] = struct{}{}
714 ctx = context.WithValue(ctx, artifactCheckContextKey{}, artifactCheckState{seen: seen, depth: state.depth + 1})
715 if !g.cfg.ArtifactVisible(ctx, e, s) {
716 return false
717 }
718 }
719 if community.IsAgentGrantRequest(e) {
720 operator := event.Tag(e, "p")
721 if !contains(s.PubKeys, e.PubKey) && !contains(s.PubKeys, operator) {
722 return false
723 }
724 }
725 if g.cfg.Store != nil {
726 var held int
727 if err := g.cfg.Store.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM (SELECT id FROM hidden_events WHERE id=? UNION ALL SELECT id FROM pending_events WHERE id=?)`, e.ID, e.ID).Scan(&held); err != nil || held > 0 {
728 return false
729 }
730 }
731 private, err := privateRepositoryEvent(ctx, g.cfg.Store, e)
732 if err != nil || private && (!p.Features.Grasp08 || p.Reads != "members") {
733 return false
734 }
735 if g.cfg.Community != nil {
736 banned, err := g.cfg.Community.IsEventBanned(ctx, e.ID)
737 if err != nil || banned {
738 return false
739 }
740 banned, err = g.cfg.Community.IsBanned(ctx, e.PubKey)
741 if err != nil || banned {
742 return false
743 }
744 for _, key := range s.PubKeys {
745 banned, err = g.cfg.Community.IsBanned(ctx, key)
746 if err != nil || banned {
747 return false
748 }
749 }
750 }
751 if e.Kind == event.KIND_PUSH_REGISTRATION {
752 return contains(s.PubKeys, e.PubKey)
753 }
754 if e.Kind == event.KIND_NOSTR_CONNECT {
755 return p.Features.Signer && canSeeSignerMessage(e, s, f)
756 }
757 a := accessFor(ctx, g.cfg.Community, p, s)
758 if !policy.CanRead(p, e, a) {
759 return false
760 }
761 if !g.roomVisible(ctx, e, s) {
762 return false
763 }
764 if event.IsPrivate(e.Kind) {
765 for _, key := range s.PubKeys {
766 if key == e.PubKey {
767 return true
768 }
769 }
770 for _, tag := range e.Tags {
771 if len(tag) >= 2 && tag[0] == "p" && contains(s.PubKeys, tag[1]) {
772 return true
773 }
774 }
775 return false
776 }
777 return true
778}
779
780// privateRepositoryEvent also protects legacy state events. Their privacy is
781// inherited from the corresponding replaceable announcement, so checking only
782// the state event's own tags would allow metadata to escape after a restart.
783func privateRepositoryEvent(ctx context.Context, store *storage.Store, e event.Event) (bool, error) {
784 return policy.PrivateRepository(ctx, store, e)
785}
786
787func canSeeSignerMessage(e event.Event, s relay.Session, f *event.Filter) bool {
788 if contains(s.PubKeys, e.PubKey) {
789 return true
790 }
791 for _, recipient := range event.TagValues(e, "p") {
792 if contains(s.PubKeys, recipient) {
793 return true
794 }
795 // The client must receive encrypted replies before it can use its
796 // remote signer to authenticate. Limit this to its addressed inbox.
797 if f != nil && len(f.Kinds) == 1 && f.Kinds[0] == event.KIND_NOSTR_CONNECT && contains(f.Tags["p"], recipient) {
798 return true
799 }
800 }
801 return false
802}
803
804func accessFor(ctx context.Context, c *community.Service, p policy.Policy, s relay.Session) policy.Access {
805 a := policy.Access{PubKeys: append([]string(nil), s.PubKeys...)}
806 a.Owner = contains(s.PubKeys, p.Owner)
807 if c != nil {
808 for _, key := range s.PubKeys {
809 member, err := c.IsMember(ctx, key)
810 if err == nil && member {
811 a.Member = true
812 }
813 role, err := c.Role(ctx, key)
814 if err == nil && role != "" {
815 a.Role = role
816 }
817 }
818 }
819 return a
820}
821
822func (g *Gate) isModerator(ctx context.Context, key string) bool {
823 if g.cfg.Community == nil {
824 return false
825 }
826 role, err := g.cfg.Community.Role(ctx, key)
827 return err == nil && (role == "owner" || role == "moderator")
828}
829
830func blockedWord(p policy.Policy, e event.Event) string {
831 for _, word := range p.BlockedWords {
832 pattern := word
833 regex := false
834 if strings.HasPrefix(pattern, "/") && strings.HasSuffix(pattern, "/") {
835 pattern, regex = pattern[1:len(pattern)-1], true
836 }
837 matches := func(value string) bool {
838 if !regex {
839 return strings.Contains(strings.ToLower(value), strings.ToLower(pattern))
840 }
841 re, err := regexp2.Compile(pattern, regexp2.IgnoreCase)
842 if err != nil {
843 return false
844 }
845 ok, err := re.MatchString(value)
846 return err == nil && ok
847 }
848 if matches(e.Content) {
849 return "content contains a blocked word"
850 }
851 if p.BlockedWordsInTags {
852 for _, tag := range e.Tags {
853 for _, value := range tag {
854 if matches(value) {
855 return "a tag contains a blocked word"
856 }
857 }
858 }
859 }
860 }
861 return ""
862}
863
864func hasTag(e event.Event, name string) bool {
865 for _, tag := range e.Tags {
866 if len(tag) > 0 && tag[0] == name {
867 return true
868 }
869 }
870 return false
871}
872func contains(values []string, wanted string) bool {
873 for _, value := range values {
874 if value == wanted {
875 return true
876 }
877 }
878 return false
879}
880
881func containsInt(values []int, wanted int) bool {
882 for _, value := range values {
883 if value == wanted {
884 return true
885 }
886 }
887 return false
888}
889func proofOfWork(id string) int {
890 bits := 0
891 for _, ch := range id {
892 value := byte(0)
893 switch {
894 case ch >= '0' && ch <= '9':
895 value = byte(ch - '0')
896 case ch >= 'a' && ch <= 'f':
897 value = byte(ch - 'a' + 10)
898 default:
899 return bits
900 }
901 for mask := byte(8); mask > 0 && value&mask == 0; mask >>= 1 {
902 bits++
903 }
904 if value != 0 {
905 break
906 }
907 }
908 return bits
909}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs