From e99a0224d08eafb95eb1bfa1629bfe0eac9bcde4 Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:17:28 +0100 Subject: [PATCH 01/10] Add missing `t.Helper()` marking to helper functions --- tests/federation_room_join_partial_state_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index 3a939996..36561bb2 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -36,6 +36,8 @@ import ( func TestPartialStateJoin(t *testing.T) { // createTestServer spins up a federation server suitable for the tests in this file createTestServer := func(t *testing.T, deployment *docker.Deployment) *federation.Server { + t.Helper() + return federation.NewServer(t, deployment, federation.HandleKeyRequests(), federation.HandlePartialStateMakeSendJoinRequests(), @@ -52,6 +54,8 @@ func TestPartialStateJoin(t *testing.T) { // createTestRoom creates a room on the complement server suitable for many of the tests in this file createTestRoom := func(t *testing.T, server *federation.Server, roomVer gomatrixserverlib.RoomVersion) *federation.ServerRoom { + t.Helper() + // create the room on the complement server, with charlie and derek as members serverRoom := server.MustMakeRoom(t, roomVer, federation.InitialRoomEvents(roomVer, server.UserID("charlie"))) serverRoom.AddEvent(server.MustCreateEvent(t, serverRoom, b.Event{ @@ -67,6 +71,8 @@ func TestPartialStateJoin(t *testing.T) { // getSyncToken gets the latest sync token getSyncToken := func(t *testing.T, alice *client.CSAPI) string { + t.Helper() + _, syncToken := alice.MustSync(t, client.SyncReq{ Filter: buildLazyLoadingSyncFilter(nil), @@ -1561,6 +1567,8 @@ func testReceiveEventDuringPartialStateJoin( // awaitEventViaSync waits for alice to be able to see a given event via an incremental lazy-loading // /sync and returns the new sync token after func awaitEventViaSync(t *testing.T, alice *client.CSAPI, roomID string, eventID string, syncToken string) string { + t.Helper() + // check that a lazy-loading sync can see the event syncToken = alice.MustSyncUntil(t, client.SyncReq{ @@ -1577,6 +1585,8 @@ func awaitEventViaSync(t *testing.T, alice *client.CSAPI, roomID string, eventID // awaitEventArrival waits for alice to be able to see a given event via /event func awaitEventArrival(t *testing.T, timeout time.Duration, alice *client.CSAPI, roomID string, eventID string) { + t.Helper() + // Alice should be able to see the event with an /event request. We might have to try it a few times. alice.DoFunc(t, "GET", []string{"_matrix", "client", "r0", "rooms", roomID, "event", eventID}, client.WithRetryUntil(timeout, func(res *http.Response) bool { From e0dd523526dcacc6bc30bd6e40f0fc5e6a899151 Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:34:14 +0100 Subject: [PATCH 02/10] Add comment to `createTestRoom` --- tests/federation_room_join_partial_state_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index 36561bb2..942b8a94 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -53,6 +53,7 @@ func TestPartialStateJoin(t *testing.T) { } // createTestRoom creates a room on the complement server suitable for many of the tests in this file + // The room starts with @charlie and @derek in it createTestRoom := func(t *testing.T, server *federation.Server, roomVer gomatrixserverlib.RoomVersion) *federation.ServerRoom { t.Helper() From fd921f2dfdf5bac06e5d0e77e7639495d2e11d58 Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:37:54 +0100 Subject: [PATCH 03/10] Add `createJoin/LeaveEvent` helper functions --- ...federation_room_join_partial_state_test.go | 83 +++++++++---------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index 942b8a94..300a4ac6 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -52,6 +52,41 @@ func TestPartialStateJoin(t *testing.T) { ) } + // createMemberEvent creates a membership event for the given user + createMembershipEvent := func( + t *testing.T, signingServer *federation.Server, room *federation.ServerRoom, userId string, + membership string, + ) *gomatrixserverlib.Event { + t.Helper() + + return signingServer.MustCreateEvent(t, room, b.Event{ + Type: "m.room.member", + StateKey: b.Ptr(userId), + Sender: userId, + Content: map[string]interface{}{ + "membership": membership, + }, + }) + } + + // createJoinEvent creates a join event for the given user + createJoinEvent := func( + t *testing.T, signingServer *federation.Server, room *federation.ServerRoom, userId string, + ) *gomatrixserverlib.Event { + t.Helper() + + return createMembershipEvent(t, signingServer, room, userId, "join") + } + + // createLeaveEvent creates a leave event for the given user + createLeaveEvent := func( + t *testing.T, signingServer *federation.Server, room *federation.ServerRoom, userId string, + ) *gomatrixserverlib.Event { + t.Helper() + + return createMembershipEvent(t, signingServer, room, userId, "leave") + } + // createTestRoom creates a room on the complement server suitable for many of the tests in this file // The room starts with @charlie and @derek in it createTestRoom := func(t *testing.T, server *federation.Server, roomVer gomatrixserverlib.RoomVersion) *federation.ServerRoom { @@ -59,14 +94,7 @@ func TestPartialStateJoin(t *testing.T) { // create the room on the complement server, with charlie and derek as members serverRoom := server.MustMakeRoom(t, roomVer, federation.InitialRoomEvents(roomVer, server.UserID("charlie"))) - serverRoom.AddEvent(server.MustCreateEvent(t, serverRoom, b.Event{ - Type: "m.room.member", - StateKey: b.Ptr(server.UserID("derek")), - Sender: server.UserID("derek"), - Content: map[string]interface{}{ - "membership": "join", - }, - })) + serverRoom.AddEvent(createJoinEvent(t, server, serverRoom, server.UserID("derek"))) return serverRoom } @@ -992,25 +1020,11 @@ func TestPartialStateJoin(t *testing.T) { serverRoom := server.MustMakeRoom(t, roomVer, initialRoomEvents) // derek joins - derekJoinEvent := server.MustCreateEvent(t, serverRoom, b.Event{ - Type: "m.room.member", - StateKey: &derek, - Sender: derek, - Content: map[string]interface{}{ - "membership": "join", - }, - }) + derekJoinEvent := createJoinEvent(t, server, serverRoom, derek) serverRoom.AddEvent(derekJoinEvent) // ... and leaves again - derekLeaveEvent := server.MustCreateEvent(t, serverRoom, b.Event{ - Type: "m.room.member", - StateKey: &derek, - Sender: derek, - Content: map[string]interface{}{ - "membership": "leave", - }, - }) + derekLeaveEvent := createLeaveEvent(t, server, serverRoom, derek) serverRoom.AddEvent(derekLeaveEvent) psjResult := beginPartialStateJoin(t, server, serverRoom, alice) @@ -1085,30 +1099,15 @@ func TestPartialStateJoin(t *testing.T) { serverRoom := server.MustMakeRoom(t, roomVer, initialRoomEvents) // derek joins - derekJoinEvent := server.MustCreateEvent(t, serverRoom, b.Event{ - Type: "m.room.member", - StateKey: &derek, - Sender: derek, - Content: map[string]interface{}{"membership": "join"}, - }) + derekJoinEvent := createJoinEvent(t, server, serverRoom, derek) serverRoom.AddEvent(derekJoinEvent) // ... and leaves again - derekLeaveEvent := server.MustCreateEvent(t, serverRoom, b.Event{ - Type: "m.room.member", - StateKey: &derek, - Sender: derek, - Content: map[string]interface{}{"membership": "leave"}, - }) + derekLeaveEvent := createLeaveEvent(t, server, serverRoom, derek) serverRoom.AddEvent(derekLeaveEvent) // Elsie joins - elsieJoinEvent := server.MustCreateEvent(t, serverRoom, b.Event{ - Type: "m.room.member", - StateKey: &elsie, - Sender: elsie, - Content: map[string]interface{}{"membership": "join"}, - }) + elsieJoinEvent := createJoinEvent(t, server, serverRoom, elsie) serverRoom.AddEvent(elsieJoinEvent) psjResult := beginPartialStateJoin(t, server, serverRoom, alice) From 83ff595cc62fe0993881083a670f44f1b7fd500a Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:38:13 +0100 Subject: [PATCH 04/10] Add `awaitPartialStateJoinCompletion` helper function --- tests/federation_room_join_partial_state_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index 300a4ac6..c8cd02b7 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -111,6 +111,16 @@ func TestPartialStateJoin(t *testing.T) { return syncToken } + // awaitPartialStateJoinCompletion waits until the joined room is no longer partial-stated + awaitPartialStateJoinCompletion := func( + t *testing.T, room *federation.ServerRoom, user *client.CSAPI, + ) { + t.Helper() + + user.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(user.UserID, room.RoomID)) + t.Logf("%s's partial state join to %s completed.", user.UserID, room.RoomID) + } + deployment := Deploy(t, b.BlueprintAlice) defer deployment.Destroy(t) From 4eb69d655035eb5592e855b825f78b1d1ef7642d Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:31:48 +0100 Subject: [PATCH 05/10] Accept extra route handlers in `createTestServer` --- ...federation_room_join_partial_state_test.go | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index c8cd02b7..0be7192a 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -35,20 +35,23 @@ import ( func TestPartialStateJoin(t *testing.T) { // createTestServer spins up a federation server suitable for the tests in this file - createTestServer := func(t *testing.T, deployment *docker.Deployment) *federation.Server { + createTestServer := func(t *testing.T, deployment *docker.Deployment, opts ...func(*federation.Server)) *federation.Server { t.Helper() return federation.NewServer(t, deployment, - federation.HandleKeyRequests(), - federation.HandlePartialStateMakeSendJoinRequests(), - federation.HandleEventRequests(), - federation.HandleTransactionRequests( - func(e *gomatrixserverlib.Event) { - t.Fatalf("Received unexpected PDU: %s", string(e.JSON())) - }, - // the homeserver under test may send us presence when the joining user syncs - nil, - ), + append( + opts, // `opts` goes first so that it can override any of the following handlers + federation.HandleKeyRequests(), + federation.HandlePartialStateMakeSendJoinRequests(), + federation.HandleEventRequests(), + federation.HandleTransactionRequests( + func(e *gomatrixserverlib.Event) { + t.Fatalf("Received unexpected PDU: %s", string(e.JSON())) + }, + // the homeserver under test may send us presence when the joining user syncs + nil, + ), + )..., ) } From c42f9c9ad0a89bfd97150760bcd792ee85cbbb38 Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:41:12 +0100 Subject: [PATCH 06/10] Add helpers for outgoing device list update tests --- ...federation_room_join_partial_state_test.go | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index 0be7192a..ce5cf9d7 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -1489,6 +1489,122 @@ func TestPartialStateJoin(t *testing.T) { t.Errorf("SendKnock: non-HTTPError: %v", err) } }) + + t.Run("Outgoing device list updates", func(t *testing.T) { + // setupOutgoingDeviceListUpdateTest sets up two complement homeservers. + // A room is created on the first complement server, containing only local users. + // Returns channels for device list updates arriving at the complement homeservers, which + // can be used with `mustReceiveDeviceListUpdate` and `mustNotReceiveDeviceListUpdate`. + setupOutgoingDeviceListUpdateTest := func( + t *testing.T, deployment *docker.Deployment, aliceLocalpart string, + opts ...func(*federation.Server), + ) ( + alice *client.CSAPI, server1 *federation.Server, server2 *federation.Server, + deviceListUpdateChannel1 chan gomatrixserverlib.DeviceListUpdateEvent, + deviceListUpdateChannel2 chan gomatrixserverlib.DeviceListUpdateEvent, + room *federation.ServerRoom, cleanup func(), + ) { + alice = deployment.RegisterUser(t, "hs1", aliceLocalpart, "secret", false) + + deviceListUpdateChannel1 = make(chan gomatrixserverlib.DeviceListUpdateEvent) + deviceListUpdateChannel2 = make(chan gomatrixserverlib.DeviceListUpdateEvent) + + createDeviceListUpdateTestServer := func( + t *testing.T, deployment *docker.Deployment, + deviceListUpdateChannel chan gomatrixserverlib.DeviceListUpdateEvent, + opts ...func(*federation.Server), + ) *federation.Server { + return createTestServer(t, deployment, + append( + opts, // `opts` goes first so that it can override any of the following handlers + federation.HandleEventAuthRequests(), + federation.HandleTransactionRequests( + func(e *gomatrixserverlib.Event) { + t.Fatalf("Received unexpected PDU: %s", string(e.JSON())) + }, + func(e gomatrixserverlib.EDU) { + if e.Type == "m.presence" { + return + } + if e.Type != "m.device_list_update" { + t.Fatalf("Received unexpected EDU: %s", e) + } + + var deviceListUpdate gomatrixserverlib.DeviceListUpdateEvent + json.Unmarshal(e.Content, &deviceListUpdate) + deviceListUpdateChannel <- deviceListUpdate + }, + ), + )..., + ) + } + + server1 = createDeviceListUpdateTestServer(t, deployment, deviceListUpdateChannel1, opts...) + server2 = createDeviceListUpdateTestServer(t, deployment, deviceListUpdateChannel2, opts...) + cancel1 := server1.Listen() + cancel2 := server2.Listen() + + room = createTestRoom(t, server1, alice.GetDefaultRoomVersion(t)) + + cleanup = func() { + cancel1() + cancel2() + close(deviceListUpdateChannel1) + close(deviceListUpdateChannel2) + } + return + } + + // renameDevice triggers an outgoing device list update + // We may want to rewrite this to update keys instead in the future. + renameDevice := func(t *testing.T, user *client.CSAPI, displayName string) { + t.Helper() + + user.MustDoFunc( + t, + "PUT", + []string{"_matrix", "client", "v3", "devices", user.DeviceID}, + client.WithJSONBody( + t, + map[string]interface{}{ + "display_name": displayName, + }, + ), + ) + + t.Logf("%s sent device list update.", user.UserID) + } + + // mustReceiveDeviceListUpdate checks that a complement homeserver has received a device + // list update since the last call. Only consumes a single device list update. + mustReceiveDeviceListUpdate := func( + t *testing.T, channel chan gomatrixserverlib.DeviceListUpdateEvent, errFormat string, + args ...interface{}, + ) { + t.Helper() + + select { + case <-time.After(1 * time.Second): + t.Fatalf(errFormat, args...) + case <-channel: + } + } + + // mustNotReceiveDeviceListUpdate checks that a complement homeserver has not received a + // device list update since the last call. + mustNotReceiveDeviceListUpdate := func( + t *testing.T, channel chan gomatrixserverlib.DeviceListUpdateEvent, errFormat string, + args ...interface{}, + ) { + t.Helper() + + select { + case <-time.After(1 * time.Second): + case <-channel: + t.Fatalf(errFormat, args...) + } + } + }) } // test reception of an event over federation during a resync From 39394c74a95b45829636757e8831ed1013248322 Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:43:03 +0100 Subject: [PATCH 07/10] Test happy cases for outgoing device list updates --- ...federation_room_join_partial_state_test.go | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index ce5cf9d7..47d1e9fe 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -1604,6 +1604,121 @@ func TestPartialStateJoin(t *testing.T) { t.Fatalf(errFormat, args...) } } + + // test that device list updates are sent to the remote homeservers listed in the + // `/send_join` response in a room with partial state. + t.Run("Device list updates reach all servers in partial state rooms", func(t *testing.T) { + alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(t, deployment, "t23alice") + defer cleanup() + + // The room starts with @charlie:server1 and @derek:server1 in it. + // @elsie:server2 joins the room before @t23alice:hs1. + room.AddEvent(createJoinEvent(t, server2, room, server2.UserID("elsie"))) + + // @t23alice:hs1 joins the room. + psjResult := beginPartialStateJoin(t, server1, room, alice) + defer psjResult.Destroy() + + // Both homeservers 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.") + + // Finish the partial state join. + psjResult.FinishStateRequest() + awaitPartialStateJoinCompletion(t, room, alice) + + // Both homeservers should still receive device list updates. + renameDevice(t, alice, "A new device name 2") + 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 additionally sent to remote homeservers that join after + // the local homeserver. + t.Run("Device list updates reach newly joined servers in partial state rooms", func(t *testing.T) { + alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(t, deployment, "t24alice") + defer cleanup() + + // The room starts with @charlie:server1 and @derek:server1 in it. + // @t24alice:hs1 joins the room. + psjResult := beginPartialStateJoin(t, server1, room, alice) + defer psjResult.Destroy() + + // Only server1 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.") + mustNotReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie received device list update unexpectedly.") + t.Log("@charlie and @derek received device list update.") + + // @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, server2.UserID("elsie")) + room.AddEvent(joinEvent) + server1.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{joinEvent.JSON()}, nil) + awaitEventViaSync(t, alice, room.RoomID, joinEvent.EventID(), "") + + // Both servers should receive device list updates now. + renameDevice(t, alice, "A new device name 2") + 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.") + + // Finish the partial state join. + psjResult.FinishStateRequest() + awaitPartialStateJoinCompletion(t, room, alice) + + // Both homeservers should still receive device list updates. + 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 the remote homeservers listed in the + // `/send_join` response in a room with partial state, even after they leave. The homeserver + // under test must do so, as it has no way of knowing that a remote homeserver has no more + // users in the room. + t.Run("Device list updates no longer reach departed servers after partial state join completes", func(t *testing.T) { + alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(t, deployment, "t25alice") + defer cleanup() + + // The room starts with @charlie:server1 and @derek:server1 in it. + // @elsie:server2 joins the room before @t25alice:hs1. + room.AddEvent(createJoinEvent(t, server2, room, server2.UserID("elsie"))) + + // @t25alice:hs1 joins the room. + psjResult := beginPartialStateJoin(t, server1, room, alice) + defer psjResult.Destroy() + + // @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, room, server2.UserID("elsie")) + room.AddEvent(leaveEvent) + server1.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{leaveEvent.JSON()}, nil) + awaitEventViaSync(t, alice, room.RoomID, leaveEvent.EventID(), "") + + // Both homeservers should receive device list updates, since hs1 cannot know that + // @elsie was the last user from server2 in the room. + 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.") + + // Finish the partial state join. + psjResult.FinishStateRequest() + awaitPartialStateJoinCompletion(t, room, alice) + + // @elsie:server2 should no longer receive 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.") + }) }) } From e99b6c69eace7cc1faade4c74c6a7b558001f902 Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:14:32 +0100 Subject: [PATCH 08/10] Add `SendJoinRequestsHandler` option to omit other servers in room --- internal/federation/handle.go | 15 +++++++++++---- tests/federation_room_join_test.go | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/federation/handle.go b/internal/federation/handle.go index 91a21e83..9121b5e1 100644 --- a/internal/federation/handle.go +++ b/internal/federation/handle.go @@ -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, ) @@ -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) @@ -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") } } @@ -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") } } diff --git a/tests/federation_room_join_test.go b/tests/federation_room_join_test.go index a4694770..af5def59 100644 --- a/tests/federation_room_join_test.go +++ b/tests/federation_room_join_test.go @@ -62,7 +62,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) From e2afad0e079fae08e99eda2ba832eaa691e59612 Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:47:17 +0100 Subject: [PATCH 09/10] Test outgoing device lists when we incorrectly think a server is absent --- ...federation_room_join_partial_state_test.go | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index 47d1e9fe..fa3c6a81 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -1719,6 +1719,246 @@ 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. + setupIncorrectlyAcceptedKick := 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, + ) (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, + ) + }) }) } From 61ffe04baf1590274d9e30732684b9a1d80270e7 Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Wed, 28 Sep 2022 13:15:54 +0100 Subject: [PATCH 10/10] fixup: note side effects of setupIncorrectlyAcceptedKick --- tests/federation_room_join_partial_state_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index fa3c6a81..feab40fe 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -1722,6 +1722,8 @@ func TestPartialStateJoin(t *testing.T) { // 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( t *testing.T, deployment *docker.Deployment, alice *client.CSAPI, server1 *federation.Server, server2 *federation.Server,