2023-07-20 14:06:05 +00:00
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package query
import (
"context"
"encoding/json"
Fix panic in `QueryNextRoomHierarchyPage` (#3253)
Sentry reported the following panic:
```
time="2023-11-01T01:33:56.220583478Z" level=error msg="Request panicked!
goroutine 43763845 [running]:
runtime/debug.Stack()
runtime/debug/stack.go:24 +0x5e
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.MakeJSONAPI.Protect.func3.1()
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:98 +0x13e
panic({0x15b5540?, 0x2453560?})
runtime/panic.go:914 +0x21f
github.com/matrix-org/dendrite/internal/httputil.MakeAuthAPI.func1.1()
github.com/matrix-org/dendrite/internal/httputil/httpapi.go:91 +0x4a
panic({0x15b5540?, 0x2453560?})
runtime/panic.go:914 +0x21f
github.com/matrix-org/dendrite/roomserver/internal/query.(*Queryer).QueryNextRoomHierarchyPage(0x413185?, {0x1a576e0, 0xc0436705a0}, {{{0xc01e5fd260, 0x1f}, {0xc01e5fd261, 0x12}, {0xc01e5fd274, 0xb}}, {0xc145cb5200, ...}, ...}, ...)
github.com/matrix-org/dendrite/roomserver/internal/query/query_room_hierarchy.go:116 +0xbfe
github.com/matrix-org/dendrite/clientapi/routing.QueryRoomHierarchy(0xc0be13b200, 0xc144e65dd0, {0xc01e5fd260?, 0x6?}, {0x7faf140639c8, 0xc00059af20}, 0xc08adca000?)
github.com/matrix-org/dendrite/clientapi/routing/room_hierarchy.go:141 +0x68b
github.com/matrix-org/dendrite/clientapi/routing.Setup.func35(0xc03e7d5c20?, 0x17c3a57?)
github.com/matrix-org/dendrite/clientapi/routing/routing.go:534 +0xbe
github.com/matrix-org/dendrite/internal/httputil.MakeAuthAPI.func1(0xc0bd097300)
github.com/matrix-org/dendrite/internal/httputil/httpapi.go:108 +0x5ed
github.com/matrix-org/util.(*jsonRequestHandlerWrapper).OnIncomingRequest(0xc0bd097200?, 0xc13b7d6fc0?)
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:79 +0x19
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.MakeJSONAPI.func2({0x1a54880, 0xc138f28b60}, 0xc0bd097200?)
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:141 +0xaa
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.MakeJSONAPI.Protect.func3({0x1a54880?, 0xc138f28b60?}, 0x17c01d9?)
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:103 +0x63
net/http.HandlerFunc.ServeHTTP(...)
net/http/server.go:2136
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.func1({0x1a54880?, 0xc138f28b60?}, 0xc0bd097100)
github.com/matrix-org/dendrite/internal/httputil/httpapi.go:191 +0x411
net/http.HandlerFunc.ServeHTTP(0xc0bd097000?, {0x1a54880?, 0xc138f28b60?}, 0xbe1348905308878e?)
net/http/server.go:2136 +0x29
github.com/gorilla/mux.(*Router).ServeHTTP(0xc000000000, {0x1a54880, 0xc138f28b60}, 0xc0bd096f00)
github.com/gorilla/mux@v1.8.0/mux.go:210 +0x1c5
github.com/matrix-org/dendrite/setup/base.SetupAndServeHTTP.(*Handler).Handle.(*Handler).handle.func5({0x1a54880, 0xc138f28b60}, 0xc0bd096e00)
github.com/getsentry/sentry-go@v0.14.0/http/sentryhttp.go:103 +0x298
net/http.HandlerFunc.ServeHTTP(0xc0bd096a00?, {0x1a54880?, 0xc138f28b60?}, 0x7fae6812f5d0?)
net/http/server.go:2136 +0x29
github.com/gorilla/mux.(*Router).ServeHTTP(0xc000000a80, {0x1a54880, 0xc138f28b60}, 0xc0bd096900)
github.com/gorilla/mux@v1.8.0/mux.go:210 +0x1c5
net/http.serverHandler.ServeHTTP({0xc02884c4e0?}, {0x1a54880?, 0xc138f28b60?}, 0x6?)
net/http/server.go:2938 +0x8e
net/http.(*conn).serve(0xc1926922d0, {0x1a576e0, 0xc024a6ec90})
net/http/server.go:2009 +0x5f4
created by net/http.(*Server).Serve in goroutine 16979
net/http/server.go:3086 +0x5cb
" context=missing panic="runtime error: invalid memory address or nil pointer dereference"
```
[skip ci]
2023-11-08 13:22:02 +00:00
"errors"
2023-07-20 14:06:05 +00:00
"fmt"
"sort"
fs "github.com/matrix-org/dendrite/federationapi/api"
roomserver "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/roomserver/types"
userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/fclient"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/util"
"github.com/tidwall/gjson"
)
// Traverse the room hierarchy using the provided walker up to the provided limit,
// returning a new walker which can be used to fetch the next page.
//
// If limit is -1, this is treated as no limit, and the entire hierarchy will be traversed.
//
// If returned walker is nil, then there are no more rooms left to traverse. This method does not modify the provided walker, so it
// can be cached.
func ( querier * Queryer ) QueryNextRoomHierarchyPage ( ctx context . Context , walker roomserver . RoomHierarchyWalker , limit int ) ( [ ] fclient . RoomHierarchyRoom , * 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" ) }
}
discoveredRooms := [ ] fclient . RoomHierarchyRoom { }
// Copy unvisited and processed to avoid modifying original walker (which is typically in cache)
unvisited := make ( [ ] roomserver . RoomHierarchyWalkerQueuedRoom , len ( walker . Unvisited ) )
copy ( unvisited , walker . Unvisited )
processed := walker . Processed . Copy ( )
// Depth first -> stack data structure
for len ( unvisited ) > 0 {
if len ( discoveredRooms ) >= limit && limit != - 1 {
break
}
Fix panic in `QueryNextRoomHierarchyPage` (#3253)
Sentry reported the following panic:
```
time="2023-11-01T01:33:56.220583478Z" level=error msg="Request panicked!
goroutine 43763845 [running]:
runtime/debug.Stack()
runtime/debug/stack.go:24 +0x5e
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.MakeJSONAPI.Protect.func3.1()
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:98 +0x13e
panic({0x15b5540?, 0x2453560?})
runtime/panic.go:914 +0x21f
github.com/matrix-org/dendrite/internal/httputil.MakeAuthAPI.func1.1()
github.com/matrix-org/dendrite/internal/httputil/httpapi.go:91 +0x4a
panic({0x15b5540?, 0x2453560?})
runtime/panic.go:914 +0x21f
github.com/matrix-org/dendrite/roomserver/internal/query.(*Queryer).QueryNextRoomHierarchyPage(0x413185?, {0x1a576e0, 0xc0436705a0}, {{{0xc01e5fd260, 0x1f}, {0xc01e5fd261, 0x12}, {0xc01e5fd274, 0xb}}, {0xc145cb5200, ...}, ...}, ...)
github.com/matrix-org/dendrite/roomserver/internal/query/query_room_hierarchy.go:116 +0xbfe
github.com/matrix-org/dendrite/clientapi/routing.QueryRoomHierarchy(0xc0be13b200, 0xc144e65dd0, {0xc01e5fd260?, 0x6?}, {0x7faf140639c8, 0xc00059af20}, 0xc08adca000?)
github.com/matrix-org/dendrite/clientapi/routing/room_hierarchy.go:141 +0x68b
github.com/matrix-org/dendrite/clientapi/routing.Setup.func35(0xc03e7d5c20?, 0x17c3a57?)
github.com/matrix-org/dendrite/clientapi/routing/routing.go:534 +0xbe
github.com/matrix-org/dendrite/internal/httputil.MakeAuthAPI.func1(0xc0bd097300)
github.com/matrix-org/dendrite/internal/httputil/httpapi.go:108 +0x5ed
github.com/matrix-org/util.(*jsonRequestHandlerWrapper).OnIncomingRequest(0xc0bd097200?, 0xc13b7d6fc0?)
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:79 +0x19
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.MakeJSONAPI.func2({0x1a54880, 0xc138f28b60}, 0xc0bd097200?)
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:141 +0xaa
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.MakeJSONAPI.Protect.func3({0x1a54880?, 0xc138f28b60?}, 0x17c01d9?)
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:103 +0x63
net/http.HandlerFunc.ServeHTTP(...)
net/http/server.go:2136
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.func1({0x1a54880?, 0xc138f28b60?}, 0xc0bd097100)
github.com/matrix-org/dendrite/internal/httputil/httpapi.go:191 +0x411
net/http.HandlerFunc.ServeHTTP(0xc0bd097000?, {0x1a54880?, 0xc138f28b60?}, 0xbe1348905308878e?)
net/http/server.go:2136 +0x29
github.com/gorilla/mux.(*Router).ServeHTTP(0xc000000000, {0x1a54880, 0xc138f28b60}, 0xc0bd096f00)
github.com/gorilla/mux@v1.8.0/mux.go:210 +0x1c5
github.com/matrix-org/dendrite/setup/base.SetupAndServeHTTP.(*Handler).Handle.(*Handler).handle.func5({0x1a54880, 0xc138f28b60}, 0xc0bd096e00)
github.com/getsentry/sentry-go@v0.14.0/http/sentryhttp.go:103 +0x298
net/http.HandlerFunc.ServeHTTP(0xc0bd096a00?, {0x1a54880?, 0xc138f28b60?}, 0x7fae6812f5d0?)
net/http/server.go:2136 +0x29
github.com/gorilla/mux.(*Router).ServeHTTP(0xc000000a80, {0x1a54880, 0xc138f28b60}, 0xc0bd096900)
github.com/gorilla/mux@v1.8.0/mux.go:210 +0x1c5
net/http.serverHandler.ServeHTTP({0xc02884c4e0?}, {0x1a54880?, 0xc138f28b60?}, 0x6?)
net/http/server.go:2938 +0x8e
net/http.(*conn).serve(0xc1926922d0, {0x1a576e0, 0xc024a6ec90})
net/http/server.go:2009 +0x5f4
created by net/http.(*Server).Serve in goroutine 16979
net/http/server.go:3086 +0x5cb
" context=missing panic="runtime error: invalid memory address or nil pointer dereference"
```
[skip ci]
2023-11-08 13:22:02 +00:00
// If the context is canceled, we might still have discovered rooms
// return them to the client and let the client know there _may_ be more rooms.
if errors . Is ( ctx . Err ( ) , context . Canceled ) {
break
}
2023-07-20 14:06:05 +00:00
// pop the stack
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 processed . Contains ( queuedRoom . RoomID ) || ( walker . MaxDepth > 0 && queuedRoom . Depth > walker . MaxDepth ) {
continue
}
// Mark this room as processed.
processed . Add ( queuedRoom . RoomID )
// if this room is not a space room, skip.
var roomType string
create := stateEvent ( ctx , querier , queuedRoom . RoomID , spec . MRoomCreate , "" )
if create != nil {
var createContent gomatrixserverlib . CreateContent
err := json . Unmarshal ( create . Content ( ) , & createContent )
if err != nil {
util . GetLogger ( ctx ) . WithError ( err ) . WithField ( "create_content" , create . Content ( ) ) . Warn ( "failed to unmarshal m.room.create event" )
}
roomType = createContent . RoomType
}
// Collect rooms/events to send back (either locally or fetched via federation)
var discoveredChildEvents [ ] fclient . RoomHierarchyStrippedEvent
// If we know about this room and the caller is authorised (joined/world_readable) then pull
// events locally
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 := federatedRoomInfo ( ctx , querier , walker . Caller , walker . SuggestedOnly , queuedRoom . RoomID , queuedRoom . Vias )
if fedRes != nil {
discoveredChildEvents = fedRes . Room . ChildrenState
discoveredRooms = append ( discoveredRooms , fedRes . Room )
if len ( fedRes . Children ) > 0 {
discoveredRooms = append ( discoveredRooms , fedRes . Children ... )
}
// mark this room as a space room as the federated server responded.
// we need to do this so we add the children of this room to the unvisited stack
// as these children may be rooms we do know about.
roomType = spec . MSpace
}
} 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 := childReferences ( ctx , querier , walker . SuggestedOnly , queuedRoom . RoomID )
if err != nil {
util . GetLogger ( ctx ) . WithError ( err ) . WithField ( "room_id" , queuedRoom . RoomID ) . Error ( "failed to extract references for room" )
continue
}
discoveredChildEvents = events
pubRoom := publicRoomsChunk ( ctx , querier , queuedRoom . RoomID )
Fix panic in `QueryNextRoomHierarchyPage` (#3253)
Sentry reported the following panic:
```
time="2023-11-01T01:33:56.220583478Z" level=error msg="Request panicked!
goroutine 43763845 [running]:
runtime/debug.Stack()
runtime/debug/stack.go:24 +0x5e
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.MakeJSONAPI.Protect.func3.1()
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:98 +0x13e
panic({0x15b5540?, 0x2453560?})
runtime/panic.go:914 +0x21f
github.com/matrix-org/dendrite/internal/httputil.MakeAuthAPI.func1.1()
github.com/matrix-org/dendrite/internal/httputil/httpapi.go:91 +0x4a
panic({0x15b5540?, 0x2453560?})
runtime/panic.go:914 +0x21f
github.com/matrix-org/dendrite/roomserver/internal/query.(*Queryer).QueryNextRoomHierarchyPage(0x413185?, {0x1a576e0, 0xc0436705a0}, {{{0xc01e5fd260, 0x1f}, {0xc01e5fd261, 0x12}, {0xc01e5fd274, 0xb}}, {0xc145cb5200, ...}, ...}, ...)
github.com/matrix-org/dendrite/roomserver/internal/query/query_room_hierarchy.go:116 +0xbfe
github.com/matrix-org/dendrite/clientapi/routing.QueryRoomHierarchy(0xc0be13b200, 0xc144e65dd0, {0xc01e5fd260?, 0x6?}, {0x7faf140639c8, 0xc00059af20}, 0xc08adca000?)
github.com/matrix-org/dendrite/clientapi/routing/room_hierarchy.go:141 +0x68b
github.com/matrix-org/dendrite/clientapi/routing.Setup.func35(0xc03e7d5c20?, 0x17c3a57?)
github.com/matrix-org/dendrite/clientapi/routing/routing.go:534 +0xbe
github.com/matrix-org/dendrite/internal/httputil.MakeAuthAPI.func1(0xc0bd097300)
github.com/matrix-org/dendrite/internal/httputil/httpapi.go:108 +0x5ed
github.com/matrix-org/util.(*jsonRequestHandlerWrapper).OnIncomingRequest(0xc0bd097200?, 0xc13b7d6fc0?)
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:79 +0x19
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.MakeJSONAPI.func2({0x1a54880, 0xc138f28b60}, 0xc0bd097200?)
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:141 +0xaa
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.MakeJSONAPI.Protect.func3({0x1a54880?, 0xc138f28b60?}, 0x17c01d9?)
github.com/matrix-org/util@v0.0.0-20221111132719-399730281e66/json.go:103 +0x63
net/http.HandlerFunc.ServeHTTP(...)
net/http/server.go:2136
github.com/matrix-org/dendrite/internal/httputil.MakeExternalAPI.func1({0x1a54880?, 0xc138f28b60?}, 0xc0bd097100)
github.com/matrix-org/dendrite/internal/httputil/httpapi.go:191 +0x411
net/http.HandlerFunc.ServeHTTP(0xc0bd097000?, {0x1a54880?, 0xc138f28b60?}, 0xbe1348905308878e?)
net/http/server.go:2136 +0x29
github.com/gorilla/mux.(*Router).ServeHTTP(0xc000000000, {0x1a54880, 0xc138f28b60}, 0xc0bd096f00)
github.com/gorilla/mux@v1.8.0/mux.go:210 +0x1c5
github.com/matrix-org/dendrite/setup/base.SetupAndServeHTTP.(*Handler).Handle.(*Handler).handle.func5({0x1a54880, 0xc138f28b60}, 0xc0bd096e00)
github.com/getsentry/sentry-go@v0.14.0/http/sentryhttp.go:103 +0x298
net/http.HandlerFunc.ServeHTTP(0xc0bd096a00?, {0x1a54880?, 0xc138f28b60?}, 0x7fae6812f5d0?)
net/http/server.go:2136 +0x29
github.com/gorilla/mux.(*Router).ServeHTTP(0xc000000a80, {0x1a54880, 0xc138f28b60}, 0xc0bd096900)
github.com/gorilla/mux@v1.8.0/mux.go:210 +0x1c5
net/http.serverHandler.ServeHTTP({0xc02884c4e0?}, {0x1a54880?, 0xc138f28b60?}, 0x6?)
net/http/server.go:2938 +0x8e
net/http.(*conn).serve(0xc1926922d0, {0x1a576e0, 0xc024a6ec90})
net/http/server.go:2009 +0x5f4
created by net/http.(*Server).Serve in goroutine 16979
net/http/server.go:3086 +0x5cb
" context=missing panic="runtime error: invalid memory address or nil pointer dereference"
```
[skip ci]
2023-11-08 13:22:02 +00:00
if pubRoom == nil {
util . GetLogger ( ctx ) . WithField ( "room_id" , queuedRoom . RoomID ) . Debug ( "unable to get publicRoomsChunk" )
continue
}
2023-07-20 14:06:05 +00:00
discoveredRooms = append ( discoveredRooms , fclient . RoomHierarchyRoom {
PublicRoom : * pubRoom ,
RoomType : roomType ,
ChildrenState : events ,
} )
// don't walk children if the user is not joined/invited to the space
if ! isJoinedOrInvited {
continue
}
} else {
// room exists but user is not authorised
continue
}
// don't walk the children
// if the parent is not a space room
if roomType != spec . MSpace {
continue
}
// For each referenced room ID in the child events being returned to the caller
// add the room ID to the queue of unvisited rooms. Loop from the beginning.
// We need to invert the order here because the child events are lo->hi on the timestamp,
// so we need to ensure we pop in the same lo->hi order, which won't be the case if we
// insert the highest timestamp last in a stack.
for i := len ( discoveredChildEvents ) - 1 ; i >= 0 ; i -- {
spaceContent := struct {
Via [ ] string ` json:"via" `
} { }
ev := discoveredChildEvents [ i ]
_ = json . Unmarshal ( ev . Content , & spaceContent )
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 ( 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
}
}
// 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
}
}
// 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 ) ... )
}
}
// 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 )
// 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
}
// helper function to fetch a state event
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 := querier . QueryCurrentState ( ctx , & roomserver . QueryCurrentStateRequest {
RoomID : roomID . String ( ) ,
StateTuples : [ ] gomatrixserverlib . StateKeyTuple { tuple } ,
} , & queryRes )
if err != nil {
return nil
}
return queryRes . StateEvents [ tuple ]
}
// returns true if the current server is participating in the provided room
func roomExists ( ctx context . Context , querier * Queryer , roomID spec . RoomID ) bool {
var queryRes roomserver . QueryServerJoinedToRoomResponse
err := querier . QueryServerJoinedToRoom ( ctx , & roomserver . QueryServerJoinedToRoomRequest {
RoomID : roomID . String ( ) ,
ServerName : querier . Cfg . Global . ServerName ,
} , & queryRes )
if err != nil {
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
// it fresh via federation
return queryRes . RoomExists && queryRes . IsInRoom
}
// federatedRoomInfo returns more of the spaces graph from another server. Returns nil if this was
// unsuccessful.
func federatedRoomInfo ( ctx context . Context , querier * Queryer , caller types . DeviceOrServerName , suggestedOnly bool , roomID spec . RoomID , vias [ ] string ) * fclient . RoomHierarchyResponse {
// only do federated requests for client requests
if caller . Device ( ) == nil {
return nil
}
resp , ok := querier . Cache . GetRoomHierarchy ( roomID . String ( ) )
if ok {
util . GetLogger ( ctx ) . Debugf ( "Returning cached response for %s" , roomID )
return & resp
}
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 ( querier . Cfg . Global . ServerName ) {
continue
}
res , err := querier . FSAPI . RoomHierarchies ( innerCtx , querier . Cfg . Global . ServerName , spec . ServerName ( serverName ) , roomID . String ( ) , suggestedOnly )
if err != nil {
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
if res . Room . ChildrenState == nil {
res . Room . ChildrenState = [ ] fclient . RoomHierarchyStrippedEvent { }
}
for i := 0 ; i < len ( res . Children ) ; i ++ {
child := res . Children [ i ]
if child . ChildrenState == nil {
child . ChildrenState = [ ] fclient . RoomHierarchyStrippedEvent { }
}
res . Children [ i ] = child
}
querier . Cache . StoreRoomHierarchy ( roomID . String ( ) , res )
return & res
}
return nil
}
// references returns all child references pointing to or from this room.
func childReferences ( ctx context . Context , querier * Queryer , suggestedOnly bool , roomID spec . RoomID ) ( [ ] fclient . RoomHierarchyStrippedEvent , error ) {
createTuple := gomatrixserverlib . StateKeyTuple {
EventType : spec . MRoomCreate ,
StateKey : "" ,
}
var res roomserver . QueryCurrentStateResponse
err := querier . QueryCurrentState ( context . Background ( ) , & roomserver . QueryCurrentStateRequest {
RoomID : roomID . String ( ) ,
AllowWildcards : true ,
StateTuples : [ ] gomatrixserverlib . StateKeyTuple {
createTuple , {
EventType : spec . MSpaceChild ,
StateKey : "*" ,
} ,
} ,
} , & res )
if err != nil {
return nil , err
}
// don't return any child refs if the room is not a space room
if create := res . StateEvents [ createTuple ] ; create != nil {
var createContent gomatrixserverlib . CreateContent
err := json . Unmarshal ( create . Content ( ) , & createContent )
if err != nil {
util . GetLogger ( ctx ) . WithError ( err ) . WithField ( "create_content" , create . Content ( ) ) . Warn ( "failed to unmarshal m.room.create event" )
}
roomType := createContent . RoomType
if roomType != spec . MSpace {
return [ ] fclient . RoomHierarchyStrippedEvent { } , nil
}
}
delete ( res . StateEvents , createTuple )
el := make ( [ ] fclient . RoomHierarchyStrippedEvent , 0 , len ( res . StateEvents ) )
for _ , ev := range res . StateEvents {
content := gjson . ParseBytes ( ev . Content ( ) )
// only return events that have a `via` key as per MSC1772
// else we'll incorrectly walk redacted events (as the link
// is in the state_key)
if content . Get ( "via" ) . Exists ( ) {
strip := stripped ( ev . PDU )
if strip == nil {
continue
}
// 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 suggestedOnly && ! content . Get ( "suggested" ) . Bool ( ) {
continue
}
el = append ( el , * strip )
}
}
// sort by origin_server_ts as per MSC2946
sort . Slice ( el , func ( i , j int ) bool {
return el [ i ] . OriginServerTS < el [ j ] . OriginServerTS
} )
return el , nil
}
// fetch public room information for provided room
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 ( ctx ) . WithError ( err ) . Error ( "failed to PopulatePublicRooms" )
return nil
}
if len ( pubRooms ) == 0 {
return nil
}
return & pubRooms [ 0 ]
}
func stripped ( ev gomatrixserverlib . PDU ) * fclient . RoomHierarchyStrippedEvent {
if ev . StateKey ( ) == nil {
return nil
}
return & fclient . RoomHierarchyStrippedEvent {
Type : ev . Type ( ) ,
StateKey : * ev . StateKey ( ) ,
Content : ev . Content ( ) ,
Sender : string ( ev . SenderID ( ) ) ,
OriginServerTS : ev . OriginServerTS ( ) ,
}
}
// given join_rule event, return list of rooms where membership of that room allows joining.
func restrictedJoinRuleAllowedRooms ( ctx context . Context , joinRuleEv * types . HeaderedEvent ) ( 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 {
2023-09-15 14:39:06 +00:00
util . GetLogger ( ctx ) . Warnf ( "failed to check join_rule on room %s: %s" , joinRuleEv . RoomID ( ) . String ( ) , err )
2023-07-20 14:06:05 +00:00
return nil
}
for _ , allow := range jrContent . Allow {
if allow . Type == spec . MRoomMembership {
allowedRoomID , err := spec . NewRoomID ( allow . RoomID )
if err != nil {
2023-09-15 14:39:06 +00:00
util . GetLogger ( ctx ) . Warnf ( "invalid room ID '%s' found in join_rule on room %s: %s" , allow . RoomID , joinRuleEv . RoomID ( ) . String ( ) , err )
2023-07-20 14:06:05 +00:00
} else {
allows = append ( allows , * allowedRoomID )
}
}
}
return
}