Skip to content
Merged
15 changes: 11 additions & 4 deletions internal/federation/handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ func MakeRespMakeKnock(s *Server, room *ServerRoom, userID string) (resp gomatri
// expectPartialState should be true if we should expect the incoming send_join
// request to use the partial_state flag, per MSC3706. In that case, we reply
// with only the critical subset of the room state.
func SendJoinRequestsHandler(s *Server, w http.ResponseWriter, req *http.Request, expectPartialState bool) {
//
// omitServersInRoom should be false to respond to partial_state joins with the complete list of
// servers in the room. When omitServersInRoom is true, a misbehaving server is simulated and only
// the current server is returned to the joining server.
func SendJoinRequestsHandler(s *Server, w http.ResponseWriter, req *http.Request, expectPartialState bool, omitServersInRoom bool) {
fedReq, errResp := gomatrixserverlib.VerifyHTTPRequest(
req, time.Now(), gomatrixserverlib.ServerName(s.serverName), s.keyRing,
)
Expand Down Expand Up @@ -172,7 +176,10 @@ func SendJoinRequestsHandler(s *Server, w http.ResponseWriter, req *http.Request
authEvents := room.AuthChainForEvents(stateEvents)

// get servers in room *before* the join event
serversInRoom := room.ServersInRoom()
serversInRoom := []string{s.serverName}
if !omitServersInRoom {
serversInRoom = room.ServersInRoom()
}

// insert the join event into the room state
room.AddEvent(event)
Expand Down Expand Up @@ -205,7 +212,7 @@ func HandleMakeSendJoinRequests() func(*Server) {
})).Methods("GET")

s.mux.Handle("/_matrix/federation/v2/send_join/{roomID}/{eventID}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
SendJoinRequestsHandler(s, w, req, false)
SendJoinRequestsHandler(s, w, req, false, false)
})).Methods("PUT")
}
}
Expand All @@ -218,7 +225,7 @@ func HandlePartialStateMakeSendJoinRequests() func(*Server) {
})).Methods("GET")

s.mux.Handle("/_matrix/federation/v2/send_join/{roomID}/{eventID}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
SendJoinRequestsHandler(s, w, req, true)
SendJoinRequestsHandler(s, w, req, true, false)
})).Methods("PUT")
}
}
Expand Down
242 changes: 242 additions & 0 deletions tests/federation_room_join_partial_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1719,6 +1719,248 @@ func TestPartialStateJoin(t *testing.T) {
mustNotReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie received device list update unexpectedly.")
t.Log("@charlie and @derek received device list update.")
})

// setupIncorrectlyAcceptedKick joins the homeserver under test to a room, then joins
// @elsie:server2 and sends an invalid event to kick @elsie:server2 from the room.
// As a side effect, @derek is promoted to admin and leaves the room before the homeserver
// under test joins.
setupIncorrectlyAcceptedKick := func(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably comment that this causes derek to leave the room?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, let's do that.

t *testing.T, deployment *docker.Deployment, alice *client.CSAPI,
server1 *federation.Server, server2 *federation.Server,
deviceListUpdateChannel1 chan gomatrixserverlib.DeviceListUpdateEvent,
deviceListUpdateChannel2 chan gomatrixserverlib.DeviceListUpdateEvent,
room *federation.ServerRoom,
) (syncToken string, psjResult partialStateJoinResult) {
derek := server1.UserID("derek")
elsie := server2.UserID("elsie")

// The room starts with @charlie:server1 and @derek:server1 in it.
// @derek:server1 becomes an admin.
var powerLevelsContent map[string]interface{}
json.Unmarshal(room.CurrentState("m.room.power_levels", "").Content(), &powerLevelsContent)
powerLevelsContent["users"].(map[string]interface{})[derek] = 100
room.AddEvent(server1.MustCreateEvent(t, room, b.Event{
Type: "m.room.power_levels",
StateKey: b.Ptr(""),
Sender: server1.UserID("charlie"),
Content: powerLevelsContent,
}))

// @derek:server1 leaves the room.
derekJoinEvent := room.CurrentState("m.room.member", derek)
derekLeaveEvent := createLeaveEvent(t, server1, room, derek)
room.AddEvent(derekLeaveEvent)

// @alice:hs1 joins the room.
psjResult = beginPartialStateJoin(t, server1, room, alice)

// @elsie:server2 joins the room.
// Make server1 send the event to the homeserver, since server2's rooms list isn't set
// up right and it can't answer queries about events in the room.
joinEvent := createJoinEvent(t, server2, room, elsie)
room.AddEvent(joinEvent)
server1.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{joinEvent.JSON()}, nil)
syncToken = awaitEventViaSync(t, alice, room.RoomID, joinEvent.EventID(), "")

// Both servers should receive device list updates.
renameDevice(t, alice, "A new device name 1")
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel1, "@charlie and @derek did not receive device list update.")
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie did not receive device list update.")
t.Log("@charlie, @derek and @elsie received device list update.")

// @derek:server1 "kicks" @elsie:server2.
badKickEvent := server1.MustCreateEvent(t, room, b.Event{
Type: "m.room.member",
StateKey: b.Ptr(elsie),
Sender: derek,
Content: map[string]interface{}{"membership": "leave"},
AuthEvents: room.EventIDsOrReferences([]*gomatrixserverlib.Event{
room.CurrentState("m.room.create", ""),
room.CurrentState("m.room.power_levels", ""),
derekJoinEvent,
}),
})
room.Timeline = append(room.Timeline, badKickEvent)
room.Depth = badKickEvent.Depth()
room.ForwardExtremities = []string{badKickEvent.EventID()}
server1.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{badKickEvent.JSON()}, nil)
awaitEventViaSync(t, alice, room.RoomID, badKickEvent.EventID(), syncToken)

return syncToken, psjResult
}

// setupAnotherSharedRoomThenLeave has @alice:hs1 create a public room, @elsie:server2 join
// the public room, then leave the partial state room.
// Returns @alice:hs1's sync token after @elsie:server2 has left the partial state room.
setupAnotherSharedRoomThenLeave := func(
t *testing.T, deployment *docker.Deployment, alice *client.CSAPI,
server1 *federation.Server, server2 *federation.Server,
partialStateRoom *federation.ServerRoom, syncToken string,
) string {
elsie := server2.UserID("elsie")

// @alice:hs1 creates a public room.
roomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})

// @elsie:server2 joins the room.
server2.MustJoinRoom(t, deployment, "hs1", roomID, elsie)
alice.MustSyncUntil(t,
client.SyncReq{
Since: syncToken,
Filter: buildLazyLoadingSyncFilter(nil),
},
client.SyncJoinedTo(elsie, roomID),
)

// @elsie:server2 leaves the room.
// Make server1 send the event to the homeserver, since server2's rooms list isn't set
// up right and it can't answer queries about events in the room.
leaveEvent := createLeaveEvent(t, server2, partialStateRoom, elsie)
partialStateRoom.AddEvent(leaveEvent)
server1.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{leaveEvent.JSON()}, nil)
syncToken = awaitEventViaSync(t, alice, partialStateRoom.RoomID, leaveEvent.EventID(), syncToken)

return syncToken
}

// testMissedDeviceListUpdateSentOncePartialJoinCompletes takes a room where hs1 incorrectly
// believes @elsie:server2 not to be present and tests that server2 receives missed device
// list updates once hs1's partial state join has completed.
testMissedDeviceListUpdateSentOncePartialJoinCompletes := func(
t *testing.T, deployment *docker.Deployment, alice *client.CSAPI,
server1 *federation.Server, server2 *federation.Server,
deviceListUpdateChannel1 chan gomatrixserverlib.DeviceListUpdateEvent,
deviceListUpdateChannel2 chan gomatrixserverlib.DeviceListUpdateEvent,
room *federation.ServerRoom, psjResult partialStateJoinResult, syncToken string,
withLeave bool,
) {
// The homeserver under test incorrectly believes @elsie:server2 is not in the room.
// @elsie:server2 should miss device list updates.
renameDevice(t, alice, "A new device name 2")
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel1, "@charlie and @derek did not receive device list update.")
mustNotReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie received device list update unexpectedly.")
t.Log("@charlie and @derek received device list update.")

if withLeave {
// @elsie:server2 joins a room shared with @alice:hs1 and leaves the partial state room.
// The homeserver under test cannot simply use the current state of the room to
// determine which device list updates it must send out once the partial state join
// completes.
setupAnotherSharedRoomThenLeave(t, deployment, alice, server1, server2, room, syncToken)
}

// Finish the partial state join.
psjResult.FinishStateRequest()
awaitPartialStateJoinCompletion(t, room, alice)

// @elsie:server2 must receive missed device list updates.
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie did not receive missed device list update.")
t.Log("@elsie received missed device list update.")

// Both homeservers should receive device list updates again.
renameDevice(t, alice, "A new device name 3")
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel1, "@charlie and @derek did not receive device list update.")
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie did not receive device list update.")
t.Log("@charlie, @derek and @elsie received device list update.")
}

// test that device list updates are sent to remote homeservers incorrectly believed not to
// be in a room with partial state once the partial state join completes.
t.Run("Device list updates reach incorrectly kicked servers once partial state join completes", func(t *testing.T) {
alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(t, deployment, "t26alice")
defer cleanup()

// The room starts with @charlie:server1 and @derek:server1 in it.
// @t26alice:hs1 joins the room, followed by @elsie:server2.
// @elsie:server2 is kicked with an invalid event.
syncToken, psjResult := setupIncorrectlyAcceptedKick(t, deployment, alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room)
defer psjResult.Destroy()

// @t26alice:hs1 sends out a device list update which is missed by @elsie:server2.
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
testMissedDeviceListUpdateSentOncePartialJoinCompletes(t, deployment, alice,
server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room,
psjResult, syncToken, false,
)
})

// test that device list updates are sent to remote homeservers incorrectly believed not to
// be in a room with partial state once the partial state join completes, even if the remote
// homeserver leaves the room beforehand.
t.Run("Device list updates reach incorrectly kicked servers once partial state join completes even though remote server left room", func(t *testing.T) {
alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(t, deployment, "t27alice")
defer cleanup()

// The room starts with @charlie:server1 and @derek:server1 in it.
// @t27alice:hs1 joins the room, followed by @elsie:server2.
// @elsie:server2 is kicked with an invalid event.
syncToken, psjResult := setupIncorrectlyAcceptedKick(t, deployment, alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room)
defer psjResult.Destroy()

// @t27alice:hs1 sends out a device list update which is missed by @elsie:server2.
// @elsie:server2 joins another room shared with @t27alice:hs1 and leaves the partial state room.
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
testMissedDeviceListUpdateSentOncePartialJoinCompletes(t, deployment, alice,
server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room,
psjResult, syncToken, true,
)
})

// handleSendJoinRequestsWithIncompleteServersInRoom responds to `/send_join` requests with a minimal `servers_in_room` list.
handleSendJoinRequestsWithIncompleteServersInRoom := func(server *federation.Server) {
server.Mux().Handle("/_matrix/federation/v2/send_join/{roomID}/{eventID}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Tell the joining server there are no other servers in the room.
federation.SendJoinRequestsHandler(server, w, req, true, true)
})).Methods("PUT")
}

// test that device list updates are sent to remote homeservers incorrectly omitted from the
// `/send_join` response once the partial state join completes.
t.Run("Device list updates reach incorrectly absent servers once partial state join completes", func(t *testing.T) {
alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(
t, deployment, "t28alice", handleSendJoinRequestsWithIncompleteServersInRoom,
)
defer cleanup()

// The room starts with @charlie:server1 and @derek:server1 in it.
// @elsie:server2 joins the room, followed by @t28alice:hs1.
// server1 does not tell hs1 that server2 is in the room.
room.AddEvent(createJoinEvent(t, server2, room, server2.UserID("elsie")))
psjResult := beginPartialStateJoin(t, server1, room, alice)
defer psjResult.Destroy()

// @t28alice:hs1 sends out a device list update which is missed by @elsie:server2.
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
testMissedDeviceListUpdateSentOncePartialJoinCompletes(t, deployment, alice,
server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room,
psjResult, "", false,
)
})

// test that device list updates are sent to remote homeservers incorrectly omitted from the
// `/send_join` response once the partial state join completes, even if the remote
// homeserver leaves the room beforehand.
t.Run("Device list updates reach incorrectly absent servers once partial state join completes even though remote server left room", func(t *testing.T) {
alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(
t, deployment, "t29alice", handleSendJoinRequestsWithIncompleteServersInRoom,
)
defer cleanup()

// The room starts with @charlie:server1 and @derek:server1 in it.
// @elsie:server2 joins the room, followed by @t29alice:hs1.
// server1 does not tell hs1 that server2 is in the room.
room.AddEvent(createJoinEvent(t, server2, room, server2.UserID("elsie")))
psjResult := beginPartialStateJoin(t, server1, room, alice)
defer psjResult.Destroy()

// @t29alice:hs1 sends out a device list update which is missed by @elsie:server2.
// @elsie:server2 joins another room shared with @t29alice:hs1 and leaves the partial state room.
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
testMissedDeviceListUpdateSentOncePartialJoinCompletes(t, deployment, alice,
server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room,
psjResult, "", true,
)
})
})
}

Expand Down
2 changes: 1 addition & 1 deletion tests/federation_room_join_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestJoinViaRoomIDAndServerName(t *testing.T) {
w.WriteHeader(502)
return
}
federation.SendJoinRequestsHandler(srv, w, req, false)
federation.SendJoinRequestsHandler(srv, w, req, false, false)
})).Methods("PUT")

ver := alice.GetDefaultRoomVersion(t)
Expand Down