diff --git a/zooid/blossom.go b/zooid/blossom.go index 449a565..92aad98 100644 --- a/zooid/blossom.go +++ b/zooid/blossom.go @@ -57,7 +57,7 @@ func (bl *BlossomStore) Enable(instance *Instance) { return true, "file too large", 413 } - if auth == nil || !instance.HasAccess(auth.PubKey) { + if auth == nil || !instance.Management.IsPubkeyAllowed(auth.PubKey) { return true, "unauthorized", 403 } @@ -65,7 +65,7 @@ func (bl *BlossomStore) Enable(instance *Instance) { } backend.RejectGet = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) { - if auth == nil || !instance.HasAccess(auth.PubKey) { + if auth == nil || !instance.Management.IsPubkeyAllowed(auth.PubKey) { return true, "unauthorized", 403 } @@ -73,7 +73,7 @@ func (bl *BlossomStore) Enable(instance *Instance) { } backend.RejectList = func(ctx context.Context, auth *nostr.Event, pubkey nostr.PubKey) (bool, string, int) { - if auth == nil || !instance.HasAccess(auth.PubKey) { + if auth == nil || !instance.Management.IsPubkeyAllowed(auth.PubKey) { return true, "unauthorized", 403 } @@ -81,7 +81,7 @@ func (bl *BlossomStore) Enable(instance *Instance) { } backend.RejectDelete = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) { - if auth == nil || !instance.HasAccess(auth.PubKey) { + if auth == nil || !instance.Management.IsPubkeyAllowed(auth.PubKey) { return true, "unauthorized", 403 } diff --git a/zooid/groups.go b/zooid/groups.go index 27a2237..8320773 100644 --- a/zooid/groups.go +++ b/zooid/groups.go @@ -1,8 +1,6 @@ package zooid import ( - "iter" - "fiatjaf.com/nostr" ) @@ -16,17 +14,74 @@ func GetGroupIDFromEvent(event nostr.Event) string { return "" } -func MakeGroupMetadataFilter(h string) nostr.Filter { - return nostr.Filter{ +type GroupStore struct { + Config *Config + Events *EventStore +} + +func (g *GroupStore) GetMetadata(h string) nostr.Event { + filter := nostr.Filter{ Kinds: []nostr.Kind{nostr.KindSimpleGroupMetadata}, Tags: nostr.TagMap{ "d": []string{h}, }, } + + for event := range g.Events.QueryEvents(filter, 1) { + return event + } + + return nostr.Event{} } -func MakeGroupEventFilters(h string) []nostr.Filter { - return []nostr.Filter{ +func (g *GroupStore) AddMember(h string, pubkey nostr.PubKey) error { + event := nostr.Event{ + Kind: nostr.KindSimpleGroupPutUser, + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + nostr.Tag{"p", pubkey.Hex()}, + nostr.Tag{"h", h}, + }, + } + + return g.Events.SignAndSaveEvent(event, true) +} + +func (g *GroupStore) RemoveMember(h string, pubkey nostr.PubKey) error { + event := nostr.Event{ + Kind: nostr.KindSimpleGroupRemoveUser, + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + nostr.Tag{"p", pubkey.Hex()}, + nostr.Tag{"h", h}, + }, + } + + return g.Events.SignAndSaveEvent(event, true) +} + +func (g *GroupStore) SetMetadataFromEvent(event nostr.Event) error { + tags := nostr.Tags{} + + for _, tag := range event.Tags { + if len(tag) >= 2 && tag[0] == "h" { + tags = append(tags, nostr.Tag{"d", tag[1]}) + } else { + tags = append(tags, tag) + } + } + + metadataEvent := nostr.Event{ + Kind: nostr.KindSimpleGroupMetadata, + CreatedAt: event.CreatedAt, + Tags: tags, + } + + return g.Events.SignAndSaveEvent(metadataEvent, true) +} + +func (g *GroupStore) DeleteGroup(h string) { + filters := []nostr.Filter{ { Tags: nostr.TagMap{ "d": []string{h}, @@ -38,20 +93,24 @@ func MakeGroupEventFilters(h string) []nostr.Filter { }, }, } + + for _, filter := range filters { + for event := range g.Events.QueryEvents(filter, 0) { + g.Events.DeleteEvent(event.ID) + } + } } -func MakeGroupMembershipCheckFilter(h string, pubkey nostr.PubKey) nostr.Filter { - return nostr.Filter{ +func (g *GroupStore) IsMember(h string, pubkey nostr.PubKey) bool { + filter := nostr.Filter{ Kinds: []nostr.Kind{nostr.KindSimpleGroupPutUser, nostr.KindSimpleGroupRemoveUser}, Tags: nostr.TagMap{ "p": []string{pubkey.Hex()}, "h": []string{h}, }, } -} -func CheckGroupMembership(events iter.Seq[nostr.Event]) bool { - for event := range events { + for event := range g.Events.QueryEvents(filter, 1) { if event.Kind == nostr.KindSimpleGroupPutUser { return true } @@ -64,42 +123,10 @@ func CheckGroupMembership(events iter.Seq[nostr.Event]) bool { return false } -func MakePutUserEvent(h string, pubkey nostr.PubKey) nostr.Event { - return nostr.Event{ - Kind: nostr.KindSimpleGroupPutUser, - CreatedAt: nostr.Now(), - Tags: nostr.Tags{ - nostr.Tag{"p", pubkey.Hex()}, - nostr.Tag{"h", h}, - }, - } -} - -func MakeRemoveUserEvent(h string, pubkey nostr.PubKey) nostr.Event { - return nostr.Event{ - Kind: nostr.KindSimpleGroupRemoveUser, - CreatedAt: nostr.Now(), - Tags: nostr.Tags{ - nostr.Tag{"p", pubkey.Hex()}, - nostr.Tag{"h", h}, - }, - } -} - -func MakeMetadataEvent(event nostr.Event) nostr.Event { - tags := nostr.Tags{} - - for _, tag := range event.Tags { - if len(tag) >= 2 && tag[0] == "h" { - tags = append(tags, nostr.Tag{"d", tag[1]}) - } else { - tags = append(tags, tag) - } - } - - return nostr.Event{ - Kind: nostr.KindSimpleGroupMetadata, - CreatedAt: event.CreatedAt, - Tags: tags, +func (g *GroupStore) HasAccess(h string, pubkey nostr.PubKey) bool { + if !HasTag(g.GetMetadata(h).Tags, "closed") { + return true } + + return g.IsMember(h, pubkey) } diff --git a/zooid/instance.go b/zooid/instance.go index 8a57852..159c498 100644 --- a/zooid/instance.go +++ b/zooid/instance.go @@ -20,6 +20,7 @@ type Instance struct { Events *EventStore Blossom *BlossomStore Management *ManagementStore + Groups *GroupStore } func MakeInstance(filename string) (*Instance, error) { @@ -48,12 +49,18 @@ func MakeInstance(filename string) (*Instance, error) { Events: events, } + groups := &GroupStore{ + Config: config, + Events: events, + } + instance := &Instance{ Relay: relay, Config: config, Events: events, Blossom: blossom, Management: management, + Groups: groups, } // NIP 11 info @@ -82,16 +89,14 @@ func MakeInstance(filename string) (*Instance, error) { // Handlers instance.Relay.OnConnect = instance.OnConnect - instance.Relay.OnEvent = instance.OnEvent + instance.Relay.PreventBroadcast = instance.PreventBroadcast instance.Relay.StoreEvent = instance.StoreEvent instance.Relay.ReplaceEvent = instance.ReplaceEvent instance.Relay.DeleteEvent = instance.DeleteEvent + instance.Relay.OnEvent = instance.OnEvent instance.Relay.OnEventSaved = instance.OnEventSaved - instance.Relay.OnEphemeralEvent = instance.OnEphemeralEvent instance.Relay.OnRequest = instance.OnRequest instance.Relay.QueryStored = instance.QueryStored - instance.Relay.RejectConnection = instance.RejectConnection - instance.Relay.PreventBroadcast = instance.PreventBroadcast // Todo: when there's a new version of khatru // instance.Relay.StartExpirationManager() @@ -129,36 +134,15 @@ func (instance *Instance) Cleanup() { // Utility methods -func (instance *Instance) IsGroupMember(id string, pubkey nostr.PubKey) bool { - filter := MakeGroupMembershipCheckFilter(id, pubkey) - events := instance.Events.QueryEvents(filter, 0) - isMember := CheckGroupMembership(events) +func (instance *Instance) StripSignature(ctx context.Context, event nostr.Event) nostr.Event { + pubkey, _ := khatru.GetAuthed(ctx) - return isMember -} - -func (instance *Instance) HasGroupAccess(id string, pubkey nostr.PubKey) bool { - filter := MakeGroupMetadataFilter(id) - - for event := range instance.Events.QueryEvents(filter, 1) { - if !HasTag(event.Tags, "closed") { - return true - } + if instance.Config.Policy.StripSignatures && !instance.Config.IsAdmin(pubkey) { + var zeroSig [64]byte + event.Sig = zeroSig } - return instance.IsGroupMember(id, pubkey) -} - -func (instance *Instance) IsInternalEvent(event nostr.Event) bool { - if event.Kind == nostr.KindApplicationSpecificData { - tag := event.Tags.Find("d") - - if tag != nil && strings.HasPrefix(tag[1], "zooid/") { - return true - } - } - - return false + return event } func (instance *Instance) AllowRecipientEvent(event nostr.Event) bool { @@ -184,6 +168,41 @@ func (instance *Instance) AllowRecipientEvent(event nostr.Event) bool { return false } +func (instance *Instance) IsInternalEvent(event nostr.Event) bool { + if event.Kind == nostr.KindApplicationSpecificData { + tag := event.Tags.Find("d") + + if tag != nil && strings.HasPrefix(tag[1], "zooid/") { + return true + } + } + + return false +} + +func (instance *Instance) IsReadOnlyEvent(event nostr.Event) bool { + if instance.IsInternalEvent(event) { + return true + } + + readOnlyEventKinds := []nostr.Kind{ + RELAY_ADD_MEMBER, + RELAY_REMOVE_MEMBER, + RELAY_MEMBERS, + } + + return slices.Contains(readOnlyEventKinds, event.Kind) +} + +func (instance *Instance) IsWriteOnlyEvent(event nostr.Event) bool { + writeOnlyEventKinds := []nostr.Kind{ + RELAY_JOIN, + RELAY_LEAVE, + } + + return slices.Contains(writeOnlyEventKinds, event.Kind) +} + func (instance *Instance) GenerateInviteEvent(pubkey nostr.PubKey) nostr.Event { filter := nostr.Filter{ Kinds: []nostr.Kind{RELAY_INVITE}, @@ -210,40 +229,30 @@ func (instance *Instance) GenerateInviteEvent(pubkey nostr.PubKey) nostr.Event { return event } -func (instance *Instance) OnJoinEvent(event nostr.Event) (reject bool, msg string) { - claimTag := event.Tags.Find("claim") - - if claimTag == nil { - return true, "invalid: no claim tag" - } - - filter := nostr.Filter{ - Kinds: []nostr.Kind{RELAY_INVITE}, - } - - for event := range instance.Events.QueryEvents(filter, 0) { - if event.Tags.FindWithValue("claim", claimTag[1]) != nil { - return false, "" - } - } - - return true, "invalid: failed to validate invite code" -} - -func (instance *Instance) GetGroupMetadataEvent(h string) nostr.Event { - for event := range instance.Events.QueryEvents(MakeGroupMetadataFilter(h), 1) { - return event - } - - return nostr.Event{} -} - // Handlers func (instance *Instance) OnConnect(ctx context.Context) { khatru.RequestAuth(ctx) } +func (instance *Instance) PreventBroadcast(ws *khatru.WebSocket, event nostr.Event) bool { + return instance.IsWriteOnlyEvent(event) +} + +func (instance *Instance) StoreEvent(ctx context.Context, event nostr.Event) error { + return instance.Events.SaveEvent(event) +} + +func (instance *Instance) ReplaceEvent(ctx context.Context, event nostr.Event) error { + return instance.Events.ReplaceEvent(event) +} + +func (instance *Instance) DeleteEvent(ctx context.Context, id nostr.ID) error { + return instance.Events.DeleteEvent(id) +} + +// Event publishing + func (instance *Instance) OnEvent(ctx context.Context, event nostr.Event) (reject bool, msg string) { if instance.AllowRecipientEvent(event) { return false, "" @@ -258,15 +267,15 @@ func (instance *Instance) OnEvent(ctx context.Context, event nostr.Event) (rejec } if event.Kind == RELAY_JOIN { - return instance.OnJoinEvent(event) + return instance.Management.ValidateJoinRequest(event) } if !instance.Management.IsPubkeyAllowed(pubkey) { return true, "restricted: you are not a member of this relay" } - if instance.IsInternalEvent(event) { - return true, "invalid: this event is not accepted" + if instance.IsReadOnlyEvent(event) { + return true, "invalid: this event's kind is not accepted" } if slices.Contains(nip29.MetadataEventKinds, event.Kind) { @@ -294,7 +303,7 @@ func (instance *Instance) OnEvent(ctx context.Context, event nostr.Event) (rejec return true, "invalid: h tag is required" } - meta := instance.GetGroupMetadataEvent(h) + meta := instance.Groups.GetMetadata(h) if event.Kind == nostr.KindSimpleGroupCreateGroup { if !IsEmptyEvent(meta) { @@ -304,21 +313,21 @@ func (instance *Instance) OnEvent(ctx context.Context, event nostr.Event) (rejec return true, "invalid: no such group exists" } - if event.Kind == nostr.KindSimpleGroupJoinRequest && instance.IsGroupMember(h, event.PubKey) { + if event.Kind == nostr.KindSimpleGroupJoinRequest && instance.Groups.IsMember(h, event.PubKey) { return true, "duplicate: already a member" } - if event.Kind == nostr.KindSimpleGroupLeaveRequest && !instance.IsGroupMember(h, event.PubKey) { + if event.Kind == nostr.KindSimpleGroupLeaveRequest && !instance.Groups.IsMember(h, event.PubKey) { return true, "duplicate: not currently a member" } } else if h != "" { - meta := instance.GetGroupMetadataEvent(h) + meta := instance.Groups.GetMetadata(h) if IsEmptyEvent(meta) { return true, "invalid: no such group exists" } - if HasTag(meta.Tags, "closed") && !instance.IsGroupMember(h, pubkey) { + if HasTag(meta.Tags, "closed") && !instance.Groups.IsMember(h, pubkey) { return true, "restricted: you are not a member of that group" } } @@ -330,25 +339,7 @@ func (instance *Instance) OnEvent(ctx context.Context, event nostr.Event) (rejec return false, "" } -func (instance *Instance) StoreEvent(ctx context.Context, event nostr.Event) error { - return instance.Events.SaveEvent(event) -} - -func (instance *Instance) ReplaceEvent(ctx context.Context, event nostr.Event) error { - return instance.Events.ReplaceEvent(event) -} - -func (instance *Instance) DeleteEvent(ctx context.Context, id nostr.ID) error { - return instance.Events.DeleteEvent(id) -} - func (instance *Instance) OnEventSaved(ctx context.Context, event nostr.Event) { - addEvent := func(newEvent nostr.Event) { - if err := instance.Events.SignAndSaveEvent(newEvent, true); err != nil { - log.Println(err) - } - } - if event.Kind == RELAY_JOIN { instance.Management.AllowPubkey(event.PubKey) } @@ -359,39 +350,27 @@ func (instance *Instance) OnEventSaved(ctx context.Context, event nostr.Event) { if event.Kind == nostr.KindSimpleGroupJoinRequest && instance.Config.Groups.AutoJoin { h := GetGroupIDFromEvent(event) - meta := instance.GetGroupMetadataEvent(h) + meta := instance.Groups.GetMetadata(h) if !HasTag(meta.Tags, "closed") { - addEvent(MakePutUserEvent(h, event.PubKey)) + instance.Groups.AddMember(h, event.PubKey) } } if event.Kind == nostr.KindSimpleGroupLeaveRequest && instance.Config.Groups.AutoLeave { - addEvent(MakeRemoveUserEvent(GetGroupIDFromEvent(event), event.PubKey)) + instance.Groups.RemoveMember(GetGroupIDFromEvent(event), event.PubKey) } - if event.Kind == nostr.KindSimpleGroupCreateGroup { - addEvent(MakeMetadataEvent(event)) - } - - if event.Kind == nostr.KindSimpleGroupEditMetadata { - addEvent(MakeMetadataEvent(event)) + if event.Kind == nostr.KindSimpleGroupCreateGroup || event.Kind == nostr.KindSimpleGroupEditMetadata { + instance.Groups.SetMetadataFromEvent(event) } if event.Kind == nostr.KindSimpleGroupDeleteGroup { - for _, filter := range MakeGroupEventFilters(GetGroupIDFromEvent(event)) { - for event := range instance.Events.QueryEvents(filter, 0) { - instance.Events.DeleteEvent(event.ID) - } - } + instance.Groups.DeleteGroup(GetGroupIDFromEvent(event)) } } -func (instance *Instance) OnEphemeralEvent(ctx context.Context, event nostr.Event) { - if slices.Contains([]nostr.Kind{RELAY_INVITE, RELAY_JOIN}, event.Kind) { - instance.Events.SaveEvent(event) - } -} +// Requests func (instance *Instance) OnRequest(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { pubkey, ok := khatru.GetAuthed(ctx) @@ -416,30 +395,16 @@ func (instance *Instance) QueryStored(ctx context.Context, filter nostr.Filter) } } } else { - pubkey, isAuthed := khatru.GetAuthed(ctx) - - if !isAuthed { - log.Panic("Unauthorized user was allowed to query events") - } - - stripSignature := func(event nostr.Event) nostr.Event { - if instance.Config.Policy.StripSignatures && !instance.Config.IsAdmin(pubkey) { - var zeroSig [64]byte - event.Sig = zeroSig - } - - return event - } + pubkey, _ := khatru.GetAuthed(ctx) if slices.Contains(filter.Kinds, RELAY_INVITE) && instance.Config.CanInvite(pubkey) { - if !yield(stripSignature(instance.GenerateInviteEvent(pubkey))) { + if !yield(instance.StripSignature(ctx, instance.GenerateInviteEvent(pubkey))) { return } } for event := range instance.Events.QueryEvents(filter, 1000) { - // We save some ephemeral events for bookkeeping, don't return them - if event.Kind.IsEphemeral() { + if instance.IsWriteOnlyEvent(event) { continue } @@ -450,7 +415,7 @@ func (instance *Instance) QueryStored(ctx context.Context, filter nostr.Filter) continue } - if !instance.HasGroupAccess(h, pubkey) { + if !instance.Groups.HasAccess(h, pubkey) { continue } } @@ -459,18 +424,10 @@ func (instance *Instance) QueryStored(ctx context.Context, filter nostr.Filter) continue } - if !yield(stripSignature(event)) { + if !yield(instance.StripSignature(ctx, event)) { return } } } } } - -func (instance *Instance) RejectConnection(r *http.Request) bool { - return false -} - -func (instance *Instance) PreventBroadcast(ws *khatru.WebSocket, event nostr.Event) bool { - return event.Kind == RELAY_JOIN -} diff --git a/zooid/management.go b/zooid/management.go index 4d0036f..fcd6f74 100644 --- a/zooid/management.go +++ b/zooid/management.go @@ -22,11 +22,51 @@ import ( // All actions are idempotent, and won't do anything if conditions are already correct. type ManagementStore struct { - Relay *khatru.Relay Config *Config Events *EventStore } +// Banned events + +func (m *ManagementStore) GetBannedEventItems() []nip86.IDReason { + items := make([]nip86.IDReason, 0) + for tag := range m.Events.GetOrCreateApplicationSpecificData(BANNED_EVENTS).Tags.FindAll("event") { + items = append(items, nip86.IDReason{ + ID: tag[1], + Reason: tag[2], + }) + } + + return items +} + +func (m *ManagementStore) BanEvent(id nostr.ID, reason string) error { + if err := m.Events.DeleteEvent(id); err != nil { + return err + } + + event := m.Events.GetOrCreateApplicationSpecificData(BANNED_EVENTS) + event.Tags = append(event.Tags, nostr.Tag{"event", id.Hex(), reason}) + + return m.Events.SignAndSaveEvent(event, false) +} + +func (m *ManagementStore) AllowEvent(id nostr.ID, reason string) error { + event := m.Events.GetOrCreateApplicationSpecificData(BANNED_EVENTS) + event.Tags = Filter(event.Tags, func(t nostr.Tag) bool { + return t[1] == id.Hex() + }) + + return m.Events.SignAndSaveEvent(event, false) +} + +func (m *ManagementStore) EventIsBanned(id nostr.ID) bool { + event := m.Events.GetOrCreateApplicationSpecificData(BANNED_EVENTS) + tag := event.Tags.FindWithValue("event", id.Hex()) + + return tag != nil +} + // Internal banned pubkeys list func (m *ManagementStore) GetBannedPubkeyItems() []nip86.PubKeyReason { @@ -73,6 +113,13 @@ func (m *ManagementStore) RemoveBannedPubkey(pubkey nostr.PubKey) error { return nil } +func (m *ManagementStore) PubkeyIsBanned(pubkey nostr.PubKey) bool { + event := m.Events.GetOrCreateApplicationSpecificData(BANNED_PUBKEYS) + tag := event.Tags.FindWithValue("pubkey", pubkey.Hex()) + + return tag != nil +} + // Membership func (m *ManagementStore) GetMembers() []nostr.PubKey { @@ -240,45 +287,30 @@ func (m *ManagementStore) AllowPubkey(pubkey nostr.PubKey) error { return nil } -// Banned events +// Joining -func (m *ManagementStore) GetBannedEventItems() []nip86.IDReason { - items := make([]nip86.IDReason, 0) - for tag := range m.Events.GetOrCreateApplicationSpecificData(BANNED_EVENTS).Tags.FindAll("event") { - items = append(items, nip86.IDReason{ - ID: tag[1], - Reason: tag[2], - }) +func (m *ManagementStore) ValidateJoinRequest(event nostr.Event) (reject bool, err string) { + if m.PubkeyIsBanned(event.PubKey) { + return true, "invalid: you have been banned from this relay" } - return items -} + claimTag := event.Tags.Find("claim") -func (m *ManagementStore) BanEvent(id nostr.ID, reason string) error { - if err := m.Events.DeleteEvent(id); err != nil { - return err + if claimTag == nil { + return true, "invalid: no claim tag" } - event := m.Events.GetOrCreateApplicationSpecificData(BANNED_EVENTS) - event.Tags = append(event.Tags, nostr.Tag{"event", id.Hex(), reason}) + filter := nostr.Filter{ + Kinds: []nostr.Kind{RELAY_INVITE}, + } - return m.Events.SignAndSaveEvent(event, false) -} + for event := range m.Events.QueryEvents(filter, 0) { + if event.Tags.FindWithValue("claim", claimTag[1]) != nil { + return false, "" + } + } -func (m *ManagementStore) AllowEvent(id nostr.ID, reason string) error { - event := m.Events.GetOrCreateApplicationSpecificData(BANNED_EVENTS) - event.Tags = Filter(event.Tags, func(t nostr.Tag) bool { - return t[1] == id.Hex() - }) - - return m.Events.SignAndSaveEvent(event, false) -} - -func (m *ManagementStore) EventIsBanned(id nostr.ID) bool { - event := m.Events.GetOrCreateApplicationSpecificData(BANNED_EVENTS) - tag := event.Tags.FindWithValue("event", id.Hex()) - - return tag != nil + return true, "invalid: failed to validate invite code" } // Middleware