From 97f2befd1bf6e2cf8109d6c37124721b9cac6688 Mon Sep 17 00:00:00 2001 From: Sam Wedgwood Date: Wed, 12 Jul 2023 22:14:43 +0100 Subject: [PATCH] Refactor room hierarchy walker --- clientapi/routing/room_hierarchy.go | 30 +- federationapi/routing/query.go | 5 +- roomserver/api/api.go | 2 +- roomserver/api/query.go | 72 ++- .../internal/query/query_room_hierarchy.go | 578 ++++++++---------- 5 files changed, 317 insertions(+), 370 deletions(-) diff --git a/clientapi/routing/room_hierarchy.go b/clientapi/routing/room_hierarchy.go index c9f53b17e..cd53aac33 100644 --- a/clientapi/routing/room_hierarchy.go +++ b/clientapi/routing/room_hierarchy.go @@ -30,24 +30,28 @@ import ( ) type RoomHierarchyPaginationCache struct { - cache map[string]roomserverAPI.CachedRoomHierarchyWalker + cache map[string]roomserverAPI.RoomHierarchyWalker mu sync.Mutex } func NewRoomHierarchyPaginationCache() RoomHierarchyPaginationCache { return RoomHierarchyPaginationCache{ - cache: map[string]roomserverAPI.CachedRoomHierarchyWalker{}, + cache: map[string]roomserverAPI.RoomHierarchyWalker{}, } } -func (c *RoomHierarchyPaginationCache) Get(token string) roomserverAPI.CachedRoomHierarchyWalker { +func (c *RoomHierarchyPaginationCache) Get(token string) *roomserverAPI.RoomHierarchyWalker { c.mu.Lock() defer c.mu.Unlock() - line := c.cache[token] - return line + line, ok := c.cache[token] + if ok { + return &line + } else { + return nil + } } -func (c *RoomHierarchyPaginationCache) AddLine(line roomserverAPI.CachedRoomHierarchyWalker) string { +func (c *RoomHierarchyPaginationCache) AddLine(line roomserverAPI.RoomHierarchyWalker) string { c.mu.Lock() defer c.mu.Unlock() token := uuid.NewString() @@ -114,21 +118,21 @@ func QueryRoomHierarchy(req *http.Request, device *userapi.Device, roomIDStr str var walker roomserverAPI.RoomHierarchyWalker if from == "" { // No pagination token provided, so start new hierarchy walker - walker = rsAPI.QueryRoomHierarchy(req.Context(), types.NewDeviceNotServerName(*device), roomID, suggestedOnly, maxDepth) + walker = roomserverAPI.NewRoomHierarchyWalker(types.NewDeviceNotServerName(*device), roomID, suggestedOnly, maxDepth) } else { // Attempt to resume cached walker cachedWalker := paginationCache.Get(from) - if cachedWalker == nil || !cachedWalker.ValidateParams(suggestedOnly, maxDepth) { + if cachedWalker == nil || cachedWalker.SuggestedOnly != suggestedOnly || cachedWalker.MaxDepth != maxDepth { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: spec.InvalidParam("pagination not found for provided token ('from') with given 'max_depth', 'suggested_only' and room ID"), } } - walker = cachedWalker.GetWalker() + walker = *cachedWalker } - discoveredRooms, err := walker.NextPage(limit) + discoveredRooms, nextWalker, err := rsAPI.QueryNextRoomHierarchyPage(req.Context(), walker, limit) if err != nil { switch err.(type) { @@ -147,9 +151,9 @@ func QueryRoomHierarchy(req *http.Request, device *userapi.Device, roomIDStr str } nextBatch := "" - if !walker.Done() { - cacheLine := walker.GetCached() - nextBatch = paginationCache.AddLine(cacheLine) + // nextWalker will be nil if there's no more rooms left to walk + if nextWalker != nil { + nextBatch = paginationCache.AddLine(*nextWalker) } return util.JSONResponse{ diff --git a/federationapi/routing/query.go b/federationapi/routing/query.go index d52695d0f..8b37fbe7b 100644 --- a/federationapi/routing/query.go +++ b/federationapi/routing/query.go @@ -145,9 +145,8 @@ func QueryRoomHierarchy(httpReq *http.Request, request *fclient.FederationReques } } - walker := rsAPI.QueryRoomHierarchy(httpReq.Context(), types.NewServerNameNotDevice(request.Origin()), roomID, suggestedOnly, 1) - - discoveredRooms, err := walker.NextPage(-1) + walker := roomserverAPI.NewRoomHierarchyWalker(types.NewServerNameNotDevice(request.Origin()), roomID, suggestedOnly, 1) + discoveredRooms, _, err := rsAPI.QueryNextRoomHierarchyPage(httpReq.Context(), walker, -1) if err != nil { switch err.(type) { diff --git a/roomserver/api/api.go b/roomserver/api/api.go index a6e0d5d75..2162c0666 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -125,7 +125,7 @@ type QueryEventsAPI interface { } type QueryRoomHierarchyAPI interface { - QueryRoomHierarchy(ctx context.Context, caller types.DeviceOrServerName, roomID spec.RoomID, suggestedOnly bool, maxDepth int) RoomHierarchyWalker + QueryNextRoomHierarchyPage(ctx context.Context, walker RoomHierarchyWalker, limit int) ([]fclient.MSC2946Room, *RoomHierarchyWalker, error) } // API functions required by the syncapi diff --git a/roomserver/api/query.go b/roomserver/api/query.go index a88a40239..ca2b6c922 100644 --- a/roomserver/api/query.go +++ b/roomserver/api/query.go @@ -23,7 +23,6 @@ import ( "strings" "github.com/matrix-org/gomatrixserverlib" - "github.com/matrix-org/gomatrixserverlib/fclient" "github.com/matrix-org/gomatrixserverlib/spec" "github.com/matrix-org/util" @@ -512,24 +511,63 @@ type QueryRoomHierarchyRequest struct { From int `json:"json"` } -// An iterator-like interface for walking a room/space hierarchy, returning each rooms information. +// A struct storing the intermediate state of a room hierarchy query for pagination purposes. // // Used for implementing space summaries / room hierarchies -type RoomHierarchyWalker interface { - // Walk the room hierarchy to retrieve room information until either - // no room left, or provided limit reached. If limit provided is -1, then this is - // treated as no limit. - NextPage(limit int) ([]fclient.MSC2946Room, error) - // Returns true if there are no more rooms left to walk - Done() bool - // Returns a stripped down version of the hiearchy walker suitable for pagination caching - GetCached() CachedRoomHierarchyWalker +// +// Use NewRoomHierarchyWalker on the roomserver API to construct this. +type RoomHierarchyWalker struct { + RootRoomID spec.RoomID + Caller types.DeviceOrServerName + SuggestedOnly bool + MaxDepth int + Processed RoomSet + Unvisited []RoomHierarchyWalkerQueuedRoom } -// Stripped down version of RoomHierarchyWalker suitable for caching (for pagination) -type CachedRoomHierarchyWalker interface { - // Converts this cached walker back into an actual walker, to resume walking from. - GetWalker() RoomHierarchyWalker - // Validates that the given parameters match those stored in the cache - ValidateParams(suggestedOnly bool, maxDepth int) bool +type RoomHierarchyWalkerQueuedRoom struct { + RoomID spec.RoomID + ParentRoomID *spec.RoomID + Depth int + Vias []string // vias to query this room by +} + +func NewRoomHierarchyWalker(caller types.DeviceOrServerName, roomID spec.RoomID, suggestedOnly bool, maxDepth int) RoomHierarchyWalker { + walker := RoomHierarchyWalker{ + RootRoomID: roomID, + Caller: caller, + SuggestedOnly: suggestedOnly, + MaxDepth: maxDepth, + Unvisited: []RoomHierarchyWalkerQueuedRoom{{ + RoomID: roomID, + ParentRoomID: nil, + Depth: 0, + }}, + Processed: NewRoomSet(), + } + + return walker +} + +type RoomSet map[spec.RoomID]struct{} + +func NewRoomSet() RoomSet { + return RoomSet{} +} + +func (s RoomSet) Contains(val spec.RoomID) bool { + _, ok := s[val] + return ok +} + +func (s RoomSet) Add(val spec.RoomID) { + s[val] = struct{}{} +} + +func (s RoomSet) Copy() RoomSet { + copied := make(RoomSet, len(s)) + for k, _ := range s { + copied.Add(k) + } + return copied } diff --git a/roomserver/internal/query/query_room_hierarchy.go b/roomserver/internal/query/query_room_hierarchy.go index 1c2eabdcd..cab552cdc 100644 --- a/roomserver/internal/query/query_room_hierarchy.go +++ b/roomserver/internal/query/query_room_hierarchy.go @@ -22,7 +22,6 @@ import ( "strings" fs "github.com/matrix-org/dendrite/federationapi/api" - "github.com/matrix-org/dendrite/internal/caching" roomserver "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/types" userapi "github.com/matrix-org/dendrite/userapi/api" @@ -33,59 +32,6 @@ import ( "github.com/tidwall/gjson" ) -// Query the hierarchy of a room (A.K.A 'space summary') -// -// This function returns a iterator-like struct over the hierarchy of a room. -func (r *Queryer) QueryRoomHierarchy(ctx context.Context, caller types.DeviceOrServerName, roomID spec.RoomID, suggestedOnly bool, maxDepth int) roomserver.RoomHierarchyWalker { - walker := RoomHierarchyWalker{ - rootRoomID: roomID.String(), - caller: caller, - thisServer: r.Cfg.Global.ServerName, - rsAPI: r, - fsAPI: r.FSAPI, - ctx: ctx, - roomHierarchyCache: r.Cache, - suggestedOnly: suggestedOnly, - maxDepth: maxDepth, - unvisited: []roomVisit{{ - roomID: roomID.String(), - parentRoomID: "", - depth: 0, - }}, - processed: stringSet{}, - } - - return &walker -} - -type stringSet map[string]struct{} - -func (s stringSet) contains(val string) bool { - _, ok := s[val] - return ok -} - -func (s stringSet) add(val string) { - s[val] = struct{}{} -} - -type RoomHierarchyWalker struct { - rootRoomID string // TODO change to spec.RoomID - caller types.DeviceOrServerName - thisServer spec.ServerName - rsAPI *Queryer - fsAPI fs.RoomserverFederationAPI - ctx context.Context - roomHierarchyCache caching.RoomHierarchyCache - suggestedOnly bool - maxDepth int - - processed stringSet - unvisited []roomVisit - - done bool -} - const ( ConstCreateEventContentKey = "type" ConstCreateEventContentValueSpace = "m.space" @@ -96,34 +42,39 @@ const ( // Walk the room hierarchy to retrieve room information until either // no room left, or provided limit reached. If limit provided is -1, then this is // treated as no limit. -func (w *RoomHierarchyWalker) NextPage(limit int) ([]fclient.MSC2946Room, error) { - if authorised, _ := w.authorised(w.rootRoomID, ""); !authorised { - return nil, roomserver.ErrRoomUnknownOrNotAllowed{Err: fmt.Errorf("room is unknown/forbidden")} +func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker roomserver.RoomHierarchyWalker, limit int) ([]fclient.MSC2946Room, *roomserver.RoomHierarchyWalker, error) { + if authorised, _ := authorised(ctx, querier, walker.Caller, walker.RootRoomID, nil); !authorised { + return nil, nil, roomserver.ErrRoomUnknownOrNotAllowed{Err: fmt.Errorf("room is unknown/forbidden")} } var discoveredRooms []fclient.MSC2946Room + // Copy unvisited and processed to avoid modifying walker + unvisited := []roomserver.RoomHierarchyWalkerQueuedRoom{} + copy(unvisited, walker.Unvisited) + processed := walker.Processed.Copy() + // Depth first -> stack data structure - for len(w.unvisited) > 0 { + for len(unvisited) > 0 { if len(discoveredRooms) >= limit { break } // pop the stack - rv := w.unvisited[len(w.unvisited)-1] - w.unvisited = w.unvisited[:len(w.unvisited)-1] + queuedRoom := unvisited[len(unvisited)-1] + unvisited = unvisited[:len(unvisited)-1] // If this room has already been processed, skip. // If this room exceeds the specified depth, skip. - if w.processed.contains(rv.roomID) || rv.roomID == "" || (w.maxDepth > 0 && rv.depth > w.maxDepth) { + if processed.Contains(queuedRoom.RoomID) || (walker.MaxDepth > 0 && queuedRoom.Depth > walker.MaxDepth) { continue } // Mark this room as processed. - w.processed.add(rv.roomID) + processed.Add(queuedRoom.RoomID) // if this room is not a space room, skip. var roomType string - create := w.stateEvent(rv.roomID, spec.MRoomCreate, "") + create := stateEvent(ctx, querier, queuedRoom.RoomID, spec.MRoomCreate, "") if create != nil { // escape the `.`s so gjson doesn't think it's nested roomType = gjson.GetBytes(create.Content(), strings.ReplaceAll(ConstCreateEventContentKey, ".", `\.`)).Str @@ -134,11 +85,11 @@ func (w *RoomHierarchyWalker) NextPage(limit int) ([]fclient.MSC2946Room, error) // If we know about this room and the caller is authorised (joined/world_readable) then pull // events locally - roomExists := w.roomExists(rv.roomID) + roomExists := roomExists(ctx, querier, queuedRoom.RoomID) if !roomExists { // attempt to query this room over federation, as either we've never heard of it before // or we've left it and hence are not authorised (but info may be exposed regardless) - fedRes := w.federatedRoomInfo(rv.roomID, rv.vias) + fedRes := federatedRoomInfo(ctx, querier, walker.Caller, walker.SuggestedOnly, queuedRoom.RoomID, queuedRoom.Vias) if fedRes != nil { discoveredChildEvents = fedRes.Room.ChildrenState discoveredRooms = append(discoveredRooms, fedRes.Room) @@ -150,16 +101,16 @@ func (w *RoomHierarchyWalker) NextPage(limit int) ([]fclient.MSC2946Room, error) // as these children may be rooms we do know about. roomType = ConstCreateEventContentValueSpace } - } else if authorised, isJoinedOrInvited := w.authorised(rv.roomID, rv.parentRoomID); authorised { + } else if authorised, isJoinedOrInvited := authorised(ctx, querier, walker.Caller, queuedRoom.RoomID, queuedRoom.ParentRoomID); authorised { // Get all `m.space.child` state events for this room - events, err := w.childReferences(rv.roomID) + events, err := childReferences(querier, walker.SuggestedOnly, queuedRoom.RoomID) if err != nil { - util.GetLogger(w.ctx).WithError(err).WithField("room_id", rv.roomID).Error("failed to extract references for room") + util.GetLogger(ctx).WithError(err).WithField("room_id", queuedRoom.RoomID).Error("failed to extract references for room") continue } discoveredChildEvents = events - pubRoom := w.publicRoomsChunk(rv.roomID) + pubRoom := publicRoomsChunk(ctx, querier, queuedRoom.RoomID) discoveredRooms = append(discoveredRooms, fclient.MSC2946Room{ PublicRoom: *pubRoom, @@ -192,51 +143,213 @@ func (w *RoomHierarchyWalker) NextPage(limit int) ([]fclient.MSC2946Room, error) }{} ev := discoveredChildEvents[i] _ = json.Unmarshal(ev.Content, &spaceContent) - w.unvisited = append(w.unvisited, roomVisit{ - roomID: ev.StateKey, - parentRoomID: rv.roomID, - depth: rv.depth + 1, - vias: spaceContent.Via, - }) + + childRoomID, err := spec.NewRoomID(ev.StateKey) + + if err != nil { + util.GetLogger(ctx).WithError(err).WithField("invalid_room_id", ev.StateKey).WithField("parent_room_id", queuedRoom.RoomID).Warn("Invalid room ID in m.space.child state event") + } else { + unvisited = append(unvisited, roomserver.RoomHierarchyWalkerQueuedRoom{ + RoomID: *childRoomID, + ParentRoomID: &queuedRoom.RoomID, + Depth: queuedRoom.Depth + 1, + Vias: spaceContent.Via, + }) + } } } - if len(w.unvisited) == 0 { - w.done = true + if len(unvisited) == 0 { + // If no more rooms to walk, then don't return a walker for future pages + return discoveredRooms, nil, nil + } else { + // If there are more rooms to walk, then return a new walker to resume walking from (for querying more pages) + newWalker := roomserver.RoomHierarchyWalker{ + RootRoomID: walker.RootRoomID, + Caller: walker.Caller, + SuggestedOnly: walker.SuggestedOnly, + MaxDepth: walker.MaxDepth, + Unvisited: unvisited, + Processed: processed, + } + + return discoveredRooms, &newWalker, nil } - return discoveredRooms, nil } -func (w *RoomHierarchyWalker) Done() bool { - return w.done -} - -func (w *RoomHierarchyWalker) GetCached() roomserver.CachedRoomHierarchyWalker { - return CachedRoomHierarchyWalker{ - rootRoomID: w.rootRoomID, - caller: w.caller, - thisServer: w.thisServer, - rsAPI: w.rsAPI, - fsAPI: w.fsAPI, - ctx: w.ctx, - cache: w.roomHierarchyCache, - suggestedOnly: w.suggestedOnly, - maxDepth: w.maxDepth, - processed: w.processed, - unvisited: w.unvisited, - done: w.done, +// authorised returns true iff the user is joined this room or the room is world_readable +func authorised(ctx context.Context, querier *Queryer, caller types.DeviceOrServerName, roomID spec.RoomID, parentRoomID *spec.RoomID) (authed, isJoinedOrInvited bool) { + if clientCaller := caller.Device(); clientCaller != nil { + return authorisedUser(ctx, querier, clientCaller, roomID, parentRoomID) + } else { + return authorisedServer(ctx, querier, roomID, *caller.ServerName()), false } } -func (w *RoomHierarchyWalker) stateEvent(roomID, evType, stateKey string) *types.HeaderedEvent { +// authorisedServer returns true iff the server is joined this room or the room is world_readable, public, or knockable +func authorisedServer(ctx context.Context, querier *Queryer, roomID spec.RoomID, callerServerName spec.ServerName) bool { + // Check history visibility / join rules first + hisVisTuple := gomatrixserverlib.StateKeyTuple{ + EventType: spec.MRoomHistoryVisibility, + StateKey: "", + } + joinRuleTuple := gomatrixserverlib.StateKeyTuple{ + EventType: spec.MRoomJoinRules, + StateKey: "", + } + var queryRoomRes roomserver.QueryCurrentStateResponse + err := querier.QueryCurrentState(ctx, &roomserver.QueryCurrentStateRequest{ + RoomID: roomID.String(), + StateTuples: []gomatrixserverlib.StateKeyTuple{ + hisVisTuple, joinRuleTuple, + }, + }, &queryRoomRes) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("failed to QueryCurrentState") + return false + } + hisVisEv := queryRoomRes.StateEvents[hisVisTuple] + if hisVisEv != nil { + hisVis, _ := hisVisEv.HistoryVisibility() + if hisVis == "world_readable" { + return true + } + } + + // check if this room is a restricted room and if so, we need to check if the server is joined to an allowed room ID + // in addition to the actual room ID (but always do the actual one first as it's quicker in the common case) + allowJoinedToRoomIDs := []spec.RoomID{roomID} + joinRuleEv := queryRoomRes.StateEvents[joinRuleTuple] + + if joinRuleEv != nil { + rule, ruleErr := joinRuleEv.JoinRule() + if ruleErr != nil { + util.GetLogger(ctx).WithError(ruleErr).WithField("parent_room_id", roomID).Warn("failed to get join rule") + return false + } + + if rule == spec.Public || rule == spec.Knock { + return true + } + + if rule == spec.Restricted { + allowJoinedToRoomIDs = append(allowJoinedToRoomIDs, restrictedJoinRuleAllowedRooms(ctx, joinRuleEv, "m.room_membership")...) + } + } + + // check if server is joined to any allowed room + for _, allowedRoomID := range allowJoinedToRoomIDs { + var queryRes fs.QueryJoinedHostServerNamesInRoomResponse + err = querier.FSAPI.QueryJoinedHostServerNamesInRoom(ctx, &fs.QueryJoinedHostServerNamesInRoomRequest{ + RoomID: allowedRoomID.String(), + }, &queryRes) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("failed to QueryJoinedHostServerNamesInRoom") + continue + } + for _, srv := range queryRes.ServerNames { + if srv == callerServerName { + return true + } + } + } + + return false +} + +// authorisedUser returns true iff the user is invited/joined this room or the room is world_readable +// or if the room has a public or knock join rule. +// Failing that, if the room has a restricted join rule and belongs to the space parent listed, it will return true. +func authorisedUser(ctx context.Context, querier *Queryer, clientCaller *userapi.Device, roomID spec.RoomID, parentRoomID *spec.RoomID) (authed bool, isJoinedOrInvited bool) { + hisVisTuple := gomatrixserverlib.StateKeyTuple{ + EventType: spec.MRoomHistoryVisibility, + StateKey: "", + } + joinRuleTuple := gomatrixserverlib.StateKeyTuple{ + EventType: spec.MRoomJoinRules, + StateKey: "", + } + roomMemberTuple := gomatrixserverlib.StateKeyTuple{ + EventType: spec.MRoomMember, + StateKey: clientCaller.UserID, + } + var queryRes roomserver.QueryCurrentStateResponse + err := querier.QueryCurrentState(ctx, &roomserver.QueryCurrentStateRequest{ + RoomID: roomID.String(), + StateTuples: []gomatrixserverlib.StateKeyTuple{ + hisVisTuple, joinRuleTuple, roomMemberTuple, + }, + }, &queryRes) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("failed to QueryCurrentState") + return false, false + } + memberEv := queryRes.StateEvents[roomMemberTuple] + if memberEv != nil { + membership, _ := memberEv.Membership() + if membership == spec.Join || membership == spec.Invite { + return true, true + } + } + hisVisEv := queryRes.StateEvents[hisVisTuple] + if hisVisEv != nil { + hisVis, _ := hisVisEv.HistoryVisibility() + if hisVis == "world_readable" { + return true, false + } + } + joinRuleEv := queryRes.StateEvents[joinRuleTuple] + if parentRoomID != nil && joinRuleEv != nil { + var allowed bool + rule, ruleErr := joinRuleEv.JoinRule() + if ruleErr != nil { + util.GetLogger(ctx).WithError(ruleErr).WithField("parent_room_id", parentRoomID).Warn("failed to get join rule") + } else if rule == spec.Public || rule == spec.Knock { + allowed = true + } else if rule == spec.Restricted { + allowedRoomIDs := restrictedJoinRuleAllowedRooms(ctx, joinRuleEv, "m.room_membership") + // check parent is in the allowed set + for _, a := range allowedRoomIDs { + if *parentRoomID == a { + allowed = true + break + } + } + } + if allowed { + // ensure caller is joined to the parent room + var queryRes2 roomserver.QueryCurrentStateResponse + err = querier.QueryCurrentState(ctx, &roomserver.QueryCurrentStateRequest{ + RoomID: parentRoomID.String(), + StateTuples: []gomatrixserverlib.StateKeyTuple{ + roomMemberTuple, + }, + }, &queryRes2) + if err != nil { + util.GetLogger(ctx).WithError(err).WithField("parent_room_id", parentRoomID).Warn("failed to check user is joined to parent room") + } else { + memberEv = queryRes2.StateEvents[roomMemberTuple] + if memberEv != nil { + membership, _ := memberEv.Membership() + if membership == spec.Join { + return true, false + } + } + } + } + } + return false, false +} + +func stateEvent(ctx context.Context, querier *Queryer, roomID spec.RoomID, evType, stateKey string) *types.HeaderedEvent { var queryRes roomserver.QueryCurrentStateResponse tuple := gomatrixserverlib.StateKeyTuple{ EventType: evType, StateKey: stateKey, } - err := w.rsAPI.QueryCurrentState(w.ctx, &roomserver.QueryCurrentStateRequest{ - RoomID: roomID, + err := querier.QueryCurrentState(ctx, &roomserver.QueryCurrentStateRequest{ + RoomID: roomID.String(), StateTuples: []gomatrixserverlib.StateKeyTuple{tuple}, }, &queryRes) if err != nil { @@ -245,14 +358,14 @@ func (w *RoomHierarchyWalker) stateEvent(roomID, evType, stateKey string) *types return queryRes.StateEvents[tuple] } -func (w *RoomHierarchyWalker) roomExists(roomID string) bool { +func roomExists(ctx context.Context, querier *Queryer, roomID spec.RoomID) bool { var queryRes roomserver.QueryServerJoinedToRoomResponse - err := w.rsAPI.QueryServerJoinedToRoom(w.ctx, &roomserver.QueryServerJoinedToRoomRequest{ - RoomID: roomID, - ServerName: w.thisServer, + err := querier.QueryServerJoinedToRoom(ctx, &roomserver.QueryServerJoinedToRoomRequest{ + RoomID: roomID.String(), + ServerName: querier.Cfg.Global.ServerName, }, &queryRes) if err != nil { - util.GetLogger(w.ctx).WithError(err).Error("failed to QueryServerJoinedToRoom") + util.GetLogger(ctx).WithError(err).Error("failed to QueryServerJoinedToRoom") return false } // if the room exists but we aren't in the room then we might have stale data so we want to fetch @@ -262,26 +375,26 @@ func (w *RoomHierarchyWalker) roomExists(roomID string) bool { // federatedRoomInfo returns more of the spaces graph from another server. Returns nil if this was // unsuccessful. -func (w *RoomHierarchyWalker) federatedRoomInfo(roomID string, vias []string) *fclient.MSC2946SpacesResponse { +func federatedRoomInfo(ctx context.Context, querier *Queryer, caller types.DeviceOrServerName, suggestedOnly bool, roomID spec.RoomID, vias []string) *fclient.MSC2946SpacesResponse { // only do federated requests for client requests - if w.caller.Device() == nil { + if caller.Device() == nil { return nil } - resp, ok := w.roomHierarchyCache.GetRoomHierarchy(roomID) + resp, ok := querier.Cache.GetRoomHierarchy(roomID.String()) if ok { - util.GetLogger(w.ctx).Debugf("Returning cached response for %s", roomID) + util.GetLogger(ctx).Debugf("Returning cached response for %s", roomID) return &resp } - util.GetLogger(w.ctx).Debugf("Querying %s via %+v", roomID, vias) - ctx := context.Background() + util.GetLogger(ctx).Debugf("Querying %s via %+v", roomID, vias) + innerCtx := context.Background() // query more of the spaces graph using these servers for _, serverName := range vias { - if serverName == string(w.thisServer) { + if serverName == string(querier.Cfg.Global.ServerName) { continue } - res, err := w.fsAPI.RoomHierarchies(ctx, w.thisServer, spec.ServerName(serverName), roomID, w.suggestedOnly) + res, err := querier.FSAPI.RoomHierarchies(innerCtx, querier.Cfg.Global.ServerName, spec.ServerName(serverName), roomID.String(), suggestedOnly) if err != nil { - util.GetLogger(w.ctx).WithError(err).Warnf("failed to call RoomHierarchies on server %s", serverName) + util.GetLogger(ctx).WithError(err).Warnf("failed to call RoomHierarchies on server %s", serverName) continue } // ensure nil slices are empty as we send this to the client sometimes @@ -295,7 +408,7 @@ func (w *RoomHierarchyWalker) federatedRoomInfo(roomID string, vias []string) *f } res.Children[i] = child } - w.roomHierarchyCache.StoreRoomHierarchy(roomID, res) + querier.Cache.StoreRoomHierarchy(roomID.String(), res) return &res } @@ -303,14 +416,14 @@ func (w *RoomHierarchyWalker) federatedRoomInfo(roomID string, vias []string) *f } // references returns all child references pointing to or from this room. -func (w *RoomHierarchyWalker) childReferences(roomID string) ([]fclient.MSC2946StrippedEvent, error) { +func childReferences(querier *Queryer, suggestedOnly bool, roomID spec.RoomID) ([]fclient.MSC2946StrippedEvent, error) { createTuple := gomatrixserverlib.StateKeyTuple{ EventType: spec.MRoomCreate, StateKey: "", } var res roomserver.QueryCurrentStateResponse - err := w.rsAPI.QueryCurrentState(context.Background(), &roomserver.QueryCurrentStateRequest{ - RoomID: roomID, + err := querier.QueryCurrentState(context.Background(), &roomserver.QueryCurrentStateRequest{ + RoomID: roomID.String(), AllowWildcards: true, StateTuples: []gomatrixserverlib.StateKeyTuple{ createTuple, { @@ -346,7 +459,7 @@ func (w *RoomHierarchyWalker) childReferences(roomID string) ([]fclient.MSC2946S } // if suggested only and this child isn't suggested, skip it. // if suggested only = false we include everything so don't need to check the content. - if w.suggestedOnly && !content.Get("suggested").Bool() { + if suggestedOnly && !content.Get("suggested").Bool() { continue } el = append(el, *strip) @@ -360,174 +473,10 @@ func (w *RoomHierarchyWalker) childReferences(roomID string) ([]fclient.MSC2946S return el, nil } -// authorised returns true iff the user is joined this room or the room is world_readable -func (w *RoomHierarchyWalker) authorised(roomID, parentRoomID string) (authed, isJoinedOrInvited bool) { - if clientCaller := w.caller.Device(); clientCaller != nil { - return w.authorisedUser(roomID, clientCaller, parentRoomID) - } else { - return w.authorisedServer(roomID, *w.caller.ServerName()), false - } -} - -// authorisedServer returns true iff the server is joined this room or the room is world_readable, public, or knockable -func (w *RoomHierarchyWalker) authorisedServer(roomID string, callerServerName spec.ServerName) bool { - // Check history visibility / join rules first - hisVisTuple := gomatrixserverlib.StateKeyTuple{ - EventType: spec.MRoomHistoryVisibility, - StateKey: "", - } - joinRuleTuple := gomatrixserverlib.StateKeyTuple{ - EventType: spec.MRoomJoinRules, - StateKey: "", - } - var queryRoomRes roomserver.QueryCurrentStateResponse - err := w.rsAPI.QueryCurrentState(w.ctx, &roomserver.QueryCurrentStateRequest{ - RoomID: roomID, - StateTuples: []gomatrixserverlib.StateKeyTuple{ - hisVisTuple, joinRuleTuple, - }, - }, &queryRoomRes) +func publicRoomsChunk(ctx context.Context, querier *Queryer, roomID spec.RoomID) *fclient.PublicRoom { + pubRooms, err := roomserver.PopulatePublicRooms(ctx, []string{roomID.String()}, querier) if err != nil { - util.GetLogger(w.ctx).WithError(err).Error("failed to QueryCurrentState") - return false - } - hisVisEv := queryRoomRes.StateEvents[hisVisTuple] - if hisVisEv != nil { - hisVis, _ := hisVisEv.HistoryVisibility() - if hisVis == "world_readable" { - return true - } - } - - // check if this room is a restricted room and if so, we need to check if the server is joined to an allowed room ID - // in addition to the actual room ID (but always do the actual one first as it's quicker in the common case) - allowJoinedToRoomIDs := []string{roomID} - joinRuleEv := queryRoomRes.StateEvents[joinRuleTuple] - - if joinRuleEv != nil { - rule, ruleErr := joinRuleEv.JoinRule() - if ruleErr != nil { - util.GetLogger(w.ctx).WithError(ruleErr).WithField("parent_room_id", roomID).Warn("failed to get join rule") - return false - } - - if rule == spec.Public || rule == spec.Knock { - return true - } - - if rule == spec.Restricted { - allowJoinedToRoomIDs = append(allowJoinedToRoomIDs, w.restrictedJoinRuleAllowedRooms(joinRuleEv, "m.room_membership")...) - } - } - - // check if server is joined to any allowed room - for _, allowedRoomID := range allowJoinedToRoomIDs { - var queryRes fs.QueryJoinedHostServerNamesInRoomResponse - err = w.fsAPI.QueryJoinedHostServerNamesInRoom(w.ctx, &fs.QueryJoinedHostServerNamesInRoomRequest{ - RoomID: allowedRoomID, - }, &queryRes) - if err != nil { - util.GetLogger(w.ctx).WithError(err).Error("failed to QueryJoinedHostServerNamesInRoom") - continue - } - for _, srv := range queryRes.ServerNames { - if srv == callerServerName { - return true - } - } - } - - return false -} - -// authorisedUser returns true iff the user is invited/joined this room or the room is world_readable -// or if the room has a public or knock join rule. -// Failing that, if the room has a restricted join rule and belongs to the space parent listed, it will return true. -func (w *RoomHierarchyWalker) authorisedUser(roomID string, clientCaller *userapi.Device, parentRoomID string) (authed bool, isJoinedOrInvited bool) { - hisVisTuple := gomatrixserverlib.StateKeyTuple{ - EventType: spec.MRoomHistoryVisibility, - StateKey: "", - } - joinRuleTuple := gomatrixserverlib.StateKeyTuple{ - EventType: spec.MRoomJoinRules, - StateKey: "", - } - roomMemberTuple := gomatrixserverlib.StateKeyTuple{ - EventType: spec.MRoomMember, - StateKey: clientCaller.UserID, - } - var queryRes roomserver.QueryCurrentStateResponse - err := w.rsAPI.QueryCurrentState(w.ctx, &roomserver.QueryCurrentStateRequest{ - RoomID: roomID, - StateTuples: []gomatrixserverlib.StateKeyTuple{ - hisVisTuple, joinRuleTuple, roomMemberTuple, - }, - }, &queryRes) - if err != nil { - util.GetLogger(w.ctx).WithError(err).Error("failed to QueryCurrentState") - return false, false - } - memberEv := queryRes.StateEvents[roomMemberTuple] - if memberEv != nil { - membership, _ := memberEv.Membership() - if membership == spec.Join || membership == spec.Invite { - return true, true - } - } - hisVisEv := queryRes.StateEvents[hisVisTuple] - if hisVisEv != nil { - hisVis, _ := hisVisEv.HistoryVisibility() - if hisVis == "world_readable" { - return true, false - } - } - joinRuleEv := queryRes.StateEvents[joinRuleTuple] - if parentRoomID != "" && joinRuleEv != nil { - var allowed bool - rule, ruleErr := joinRuleEv.JoinRule() - if ruleErr != nil { - util.GetLogger(w.ctx).WithError(ruleErr).WithField("parent_room_id", parentRoomID).Warn("failed to get join rule") - } else if rule == spec.Public || rule == spec.Knock { - allowed = true - } else if rule == spec.Restricted { - allowedRoomIDs := w.restrictedJoinRuleAllowedRooms(joinRuleEv, "m.room_membership") - // check parent is in the allowed set - for _, a := range allowedRoomIDs { - if parentRoomID == a { - allowed = true - break - } - } - } - if allowed { - // ensure caller is joined to the parent room - var queryRes2 roomserver.QueryCurrentStateResponse - err = w.rsAPI.QueryCurrentState(w.ctx, &roomserver.QueryCurrentStateRequest{ - RoomID: parentRoomID, - StateTuples: []gomatrixserverlib.StateKeyTuple{ - roomMemberTuple, - }, - }, &queryRes2) - if err != nil { - util.GetLogger(w.ctx).WithError(err).WithField("parent_room_id", parentRoomID).Warn("failed to check user is joined to parent room") - } else { - memberEv = queryRes2.StateEvents[roomMemberTuple] - if memberEv != nil { - membership, _ := memberEv.Membership() - if membership == spec.Join { - return true, false - } - } - } - } - } - return false, false -} - -func (w *RoomHierarchyWalker) publicRoomsChunk(roomID string) *fclient.PublicRoom { - pubRooms, err := roomserver.PopulatePublicRooms(w.ctx, []string{roomID}, w.rsAPI) - if err != nil { - util.GetLogger(w.ctx).WithError(err).Error("failed to PopulatePublicRooms") + util.GetLogger(ctx).WithError(err).Error("failed to PopulatePublicRooms") return nil } if len(pubRooms) == 0 { @@ -536,13 +485,6 @@ func (w *RoomHierarchyWalker) publicRoomsChunk(roomID string) *fclient.PublicRoo return &pubRooms[0] } -type roomVisit struct { - roomID string - parentRoomID string - depth int - vias []string // vias to query this room by -} - func stripped(ev gomatrixserverlib.PDU) *fclient.MSC2946StrippedEvent { if ev.StateKey() == nil { return nil @@ -556,61 +498,25 @@ func stripped(ev gomatrixserverlib.PDU) *fclient.MSC2946StrippedEvent { } } -func (w *RoomHierarchyWalker) restrictedJoinRuleAllowedRooms(joinRuleEv *types.HeaderedEvent, allowType string) (allows []string) { +func restrictedJoinRuleAllowedRooms(ctx context.Context, joinRuleEv *types.HeaderedEvent, allowType string) (allows []spec.RoomID) { rule, _ := joinRuleEv.JoinRule() if rule != spec.Restricted { return nil } var jrContent gomatrixserverlib.JoinRuleContent if err := json.Unmarshal(joinRuleEv.Content(), &jrContent); err != nil { - util.GetLogger(w.ctx).Warnf("failed to check join_rule on room %s: %s", joinRuleEv.RoomID(), err) + util.GetLogger(ctx).Warnf("failed to check join_rule on room %s: %s", joinRuleEv.RoomID(), err) return nil } for _, allow := range jrContent.Allow { if allow.Type == allowType { - allows = append(allows, allow.RoomID) + allowedRoomID, err := spec.NewRoomID(allow.RoomID) + if err != nil { + util.GetLogger(ctx).Warnf("invalid room ID '%s' found in join_rule on room %s: %s", allow.RoomID, joinRuleEv.RoomID(), err) + } else { + allows = append(allows, *allowedRoomID) + } } } return } - -// Stripped down version of RoomHierarchyWalker suitable for caching (For pagination purposes) -// -// TODO remove more stuff -type CachedRoomHierarchyWalker struct { - rootRoomID string - caller types.DeviceOrServerName - thisServer spec.ServerName - rsAPI *Queryer - fsAPI fs.RoomserverFederationAPI - ctx context.Context - cache caching.RoomHierarchyCache - suggestedOnly bool - maxDepth int - - processed stringSet - unvisited []roomVisit - - done bool -} - -func (c CachedRoomHierarchyWalker) GetWalker() roomserver.RoomHierarchyWalker { - return &RoomHierarchyWalker{ - rootRoomID: c.rootRoomID, - caller: c.caller, - thisServer: c.thisServer, - rsAPI: c.rsAPI, - fsAPI: c.fsAPI, - ctx: c.ctx, - roomHierarchyCache: c.cache, - suggestedOnly: c.suggestedOnly, - maxDepth: c.maxDepth, - processed: c.processed, - unvisited: c.unvisited, - done: c.done, - } -} - -func (c CachedRoomHierarchyWalker) ValidateParams(suggestedOnly bool, maxDepth int) bool { - return c.suggestedOnly == suggestedOnly && c.maxDepth == maxDepth -}