Skip to content

Commit 0e88861

Browse files
authored
Faster room joins: Add edge case tests for outgoing device list updates (#474)
Test that outgoing device list updates reach servers incorrectly thought to not be in a partial state room once the room transitions to full state.
1 parent 592f16e commit 0e88861

File tree

3 files changed

+254
-5
lines changed

3 files changed

+254
-5
lines changed

internal/federation/handle.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ func MakeRespMakeKnock(s *Server, room *ServerRoom, userID string) (resp gomatri
119119
// expectPartialState should be true if we should expect the incoming send_join
120120
// request to use the partial_state flag, per MSC3706. In that case, we reply
121121
// with only the critical subset of the room state.
122-
func SendJoinRequestsHandler(s *Server, w http.ResponseWriter, req *http.Request, expectPartialState bool) {
122+
//
123+
// omitServersInRoom should be false to respond to partial_state joins with the complete list of
124+
// servers in the room. When omitServersInRoom is true, a misbehaving server is simulated and only
125+
// the current server is returned to the joining server.
126+
func SendJoinRequestsHandler(s *Server, w http.ResponseWriter, req *http.Request, expectPartialState bool, omitServersInRoom bool) {
123127
fedReq, errResp := gomatrixserverlib.VerifyHTTPRequest(
124128
req, time.Now(), gomatrixserverlib.ServerName(s.serverName), s.keyRing,
125129
)
@@ -172,7 +176,10 @@ func SendJoinRequestsHandler(s *Server, w http.ResponseWriter, req *http.Request
172176
authEvents := room.AuthChainForEvents(stateEvents)
173177

174178
// get servers in room *before* the join event
175-
serversInRoom := room.ServersInRoom()
179+
serversInRoom := []string{s.serverName}
180+
if !omitServersInRoom {
181+
serversInRoom = room.ServersInRoom()
182+
}
176183

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

207214
s.mux.Handle("/_matrix/federation/v2/send_join/{roomID}/{eventID}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
208-
SendJoinRequestsHandler(s, w, req, false)
215+
SendJoinRequestsHandler(s, w, req, false, false)
209216
})).Methods("PUT")
210217
}
211218
}
@@ -218,7 +225,7 @@ func HandlePartialStateMakeSendJoinRequests() func(*Server) {
218225
})).Methods("GET")
219226

220227
s.mux.Handle("/_matrix/federation/v2/send_join/{roomID}/{eventID}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
221-
SendJoinRequestsHandler(s, w, req, true)
228+
SendJoinRequestsHandler(s, w, req, true, false)
222229
})).Methods("PUT")
223230
}
224231
}

tests/federation_room_join_partial_state_test.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,248 @@ func TestPartialStateJoin(t *testing.T) {
17191719
mustNotReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie received device list update unexpectedly.")
17201720
t.Log("@charlie and @derek received device list update.")
17211721
})
1722+
1723+
// setupIncorrectlyAcceptedKick joins the homeserver under test to a room, then joins
1724+
// @elsie:server2 and sends an invalid event to kick @elsie:server2 from the room.
1725+
// As a side effect, @derek is promoted to admin and leaves the room before the homeserver
1726+
// under test joins.
1727+
setupIncorrectlyAcceptedKick := func(
1728+
t *testing.T, deployment *docker.Deployment, alice *client.CSAPI,
1729+
server1 *federation.Server, server2 *federation.Server,
1730+
deviceListUpdateChannel1 chan gomatrixserverlib.DeviceListUpdateEvent,
1731+
deviceListUpdateChannel2 chan gomatrixserverlib.DeviceListUpdateEvent,
1732+
room *federation.ServerRoom,
1733+
) (syncToken string, psjResult partialStateJoinResult) {
1734+
derek := server1.UserID("derek")
1735+
elsie := server2.UserID("elsie")
1736+
1737+
// The room starts with @charlie:server1 and @derek:server1 in it.
1738+
// @derek:server1 becomes an admin.
1739+
var powerLevelsContent map[string]interface{}
1740+
json.Unmarshal(room.CurrentState("m.room.power_levels", "").Content(), &powerLevelsContent)
1741+
powerLevelsContent["users"].(map[string]interface{})[derek] = 100
1742+
room.AddEvent(server1.MustCreateEvent(t, room, b.Event{
1743+
Type: "m.room.power_levels",
1744+
StateKey: b.Ptr(""),
1745+
Sender: server1.UserID("charlie"),
1746+
Content: powerLevelsContent,
1747+
}))
1748+
1749+
// @derek:server1 leaves the room.
1750+
derekJoinEvent := room.CurrentState("m.room.member", derek)
1751+
derekLeaveEvent := createLeaveEvent(t, server1, room, derek)
1752+
room.AddEvent(derekLeaveEvent)
1753+
1754+
// @alice:hs1 joins the room.
1755+
psjResult = beginPartialStateJoin(t, server1, room, alice)
1756+
1757+
// @elsie:server2 joins the room.
1758+
// Make server1 send the event to the homeserver, since server2's rooms list isn't set
1759+
// up right and it can't answer queries about events in the room.
1760+
joinEvent := createJoinEvent(t, server2, room, elsie)
1761+
room.AddEvent(joinEvent)
1762+
server1.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{joinEvent.JSON()}, nil)
1763+
syncToken = awaitEventViaSync(t, alice, room.RoomID, joinEvent.EventID(), "")
1764+
1765+
// Both servers should receive device list updates.
1766+
renameDevice(t, alice, "A new device name 1")
1767+
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel1, "@charlie and @derek did not receive device list update.")
1768+
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie did not receive device list update.")
1769+
t.Log("@charlie, @derek and @elsie received device list update.")
1770+
1771+
// @derek:server1 "kicks" @elsie:server2.
1772+
badKickEvent := server1.MustCreateEvent(t, room, b.Event{
1773+
Type: "m.room.member",
1774+
StateKey: b.Ptr(elsie),
1775+
Sender: derek,
1776+
Content: map[string]interface{}{"membership": "leave"},
1777+
AuthEvents: room.EventIDsOrReferences([]*gomatrixserverlib.Event{
1778+
room.CurrentState("m.room.create", ""),
1779+
room.CurrentState("m.room.power_levels", ""),
1780+
derekJoinEvent,
1781+
}),
1782+
})
1783+
room.Timeline = append(room.Timeline, badKickEvent)
1784+
room.Depth = badKickEvent.Depth()
1785+
room.ForwardExtremities = []string{badKickEvent.EventID()}
1786+
server1.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{badKickEvent.JSON()}, nil)
1787+
awaitEventViaSync(t, alice, room.RoomID, badKickEvent.EventID(), syncToken)
1788+
1789+
return syncToken, psjResult
1790+
}
1791+
1792+
// setupAnotherSharedRoomThenLeave has @alice:hs1 create a public room, @elsie:server2 join
1793+
// the public room, then leave the partial state room.
1794+
// Returns @alice:hs1's sync token after @elsie:server2 has left the partial state room.
1795+
setupAnotherSharedRoomThenLeave := func(
1796+
t *testing.T, deployment *docker.Deployment, alice *client.CSAPI,
1797+
server1 *federation.Server, server2 *federation.Server,
1798+
partialStateRoom *federation.ServerRoom, syncToken string,
1799+
) string {
1800+
elsie := server2.UserID("elsie")
1801+
1802+
// @alice:hs1 creates a public room.
1803+
roomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
1804+
1805+
// @elsie:server2 joins the room.
1806+
server2.MustJoinRoom(t, deployment, "hs1", roomID, elsie)
1807+
alice.MustSyncUntil(t,
1808+
client.SyncReq{
1809+
Since: syncToken,
1810+
Filter: buildLazyLoadingSyncFilter(nil),
1811+
},
1812+
client.SyncJoinedTo(elsie, roomID),
1813+
)
1814+
1815+
// @elsie:server2 leaves the room.
1816+
// Make server1 send the event to the homeserver, since server2's rooms list isn't set
1817+
// up right and it can't answer queries about events in the room.
1818+
leaveEvent := createLeaveEvent(t, server2, partialStateRoom, elsie)
1819+
partialStateRoom.AddEvent(leaveEvent)
1820+
server1.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{leaveEvent.JSON()}, nil)
1821+
syncToken = awaitEventViaSync(t, alice, partialStateRoom.RoomID, leaveEvent.EventID(), syncToken)
1822+
1823+
return syncToken
1824+
}
1825+
1826+
// testMissedDeviceListUpdateSentOncePartialJoinCompletes takes a room where hs1 incorrectly
1827+
// believes @elsie:server2 not to be present and tests that server2 receives missed device
1828+
// list updates once hs1's partial state join has completed.
1829+
testMissedDeviceListUpdateSentOncePartialJoinCompletes := func(
1830+
t *testing.T, deployment *docker.Deployment, alice *client.CSAPI,
1831+
server1 *federation.Server, server2 *federation.Server,
1832+
deviceListUpdateChannel1 chan gomatrixserverlib.DeviceListUpdateEvent,
1833+
deviceListUpdateChannel2 chan gomatrixserverlib.DeviceListUpdateEvent,
1834+
room *federation.ServerRoom, psjResult partialStateJoinResult, syncToken string,
1835+
withLeave bool,
1836+
) {
1837+
// The homeserver under test incorrectly believes @elsie:server2 is not in the room.
1838+
// @elsie:server2 should miss device list updates.
1839+
renameDevice(t, alice, "A new device name 2")
1840+
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel1, "@charlie and @derek did not receive device list update.")
1841+
mustNotReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie received device list update unexpectedly.")
1842+
t.Log("@charlie and @derek received device list update.")
1843+
1844+
if withLeave {
1845+
// @elsie:server2 joins a room shared with @alice:hs1 and leaves the partial state room.
1846+
// The homeserver under test cannot simply use the current state of the room to
1847+
// determine which device list updates it must send out once the partial state join
1848+
// completes.
1849+
setupAnotherSharedRoomThenLeave(t, deployment, alice, server1, server2, room, syncToken)
1850+
}
1851+
1852+
// Finish the partial state join.
1853+
psjResult.FinishStateRequest()
1854+
awaitPartialStateJoinCompletion(t, room, alice)
1855+
1856+
// @elsie:server2 must receive missed device list updates.
1857+
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie did not receive missed device list update.")
1858+
t.Log("@elsie received missed device list update.")
1859+
1860+
// Both homeservers should receive device list updates again.
1861+
renameDevice(t, alice, "A new device name 3")
1862+
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel1, "@charlie and @derek did not receive device list update.")
1863+
mustReceiveDeviceListUpdate(t, deviceListUpdateChannel2, "@elsie did not receive device list update.")
1864+
t.Log("@charlie, @derek and @elsie received device list update.")
1865+
}
1866+
1867+
// test that device list updates are sent to remote homeservers incorrectly believed not to
1868+
// be in a room with partial state once the partial state join completes.
1869+
t.Run("Device list updates reach incorrectly kicked servers once partial state join completes", func(t *testing.T) {
1870+
alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(t, deployment, "t26alice")
1871+
defer cleanup()
1872+
1873+
// The room starts with @charlie:server1 and @derek:server1 in it.
1874+
// @t26alice:hs1 joins the room, followed by @elsie:server2.
1875+
// @elsie:server2 is kicked with an invalid event.
1876+
syncToken, psjResult := setupIncorrectlyAcceptedKick(t, deployment, alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room)
1877+
defer psjResult.Destroy()
1878+
1879+
// @t26alice:hs1 sends out a device list update which is missed by @elsie:server2.
1880+
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
1881+
testMissedDeviceListUpdateSentOncePartialJoinCompletes(t, deployment, alice,
1882+
server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room,
1883+
psjResult, syncToken, false,
1884+
)
1885+
})
1886+
1887+
// test that device list updates are sent to remote homeservers incorrectly believed not to
1888+
// be in a room with partial state once the partial state join completes, even if the remote
1889+
// homeserver leaves the room beforehand.
1890+
t.Run("Device list updates reach incorrectly kicked servers once partial state join completes even though remote server left room", func(t *testing.T) {
1891+
alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(t, deployment, "t27alice")
1892+
defer cleanup()
1893+
1894+
// The room starts with @charlie:server1 and @derek:server1 in it.
1895+
// @t27alice:hs1 joins the room, followed by @elsie:server2.
1896+
// @elsie:server2 is kicked with an invalid event.
1897+
syncToken, psjResult := setupIncorrectlyAcceptedKick(t, deployment, alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room)
1898+
defer psjResult.Destroy()
1899+
1900+
// @t27alice:hs1 sends out a device list update which is missed by @elsie:server2.
1901+
// @elsie:server2 joins another room shared with @t27alice:hs1 and leaves the partial state room.
1902+
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
1903+
testMissedDeviceListUpdateSentOncePartialJoinCompletes(t, deployment, alice,
1904+
server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room,
1905+
psjResult, syncToken, true,
1906+
)
1907+
})
1908+
1909+
// handleSendJoinRequestsWithIncompleteServersInRoom responds to `/send_join` requests with a minimal `servers_in_room` list.
1910+
handleSendJoinRequestsWithIncompleteServersInRoom := func(server *federation.Server) {
1911+
server.Mux().Handle("/_matrix/federation/v2/send_join/{roomID}/{eventID}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
1912+
// Tell the joining server there are no other servers in the room.
1913+
federation.SendJoinRequestsHandler(server, w, req, true, true)
1914+
})).Methods("PUT")
1915+
}
1916+
1917+
// test that device list updates are sent to remote homeservers incorrectly omitted from the
1918+
// `/send_join` response once the partial state join completes.
1919+
t.Run("Device list updates reach incorrectly absent servers once partial state join completes", func(t *testing.T) {
1920+
alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(
1921+
t, deployment, "t28alice", handleSendJoinRequestsWithIncompleteServersInRoom,
1922+
)
1923+
defer cleanup()
1924+
1925+
// The room starts with @charlie:server1 and @derek:server1 in it.
1926+
// @elsie:server2 joins the room, followed by @t28alice:hs1.
1927+
// server1 does not tell hs1 that server2 is in the room.
1928+
room.AddEvent(createJoinEvent(t, server2, room, server2.UserID("elsie")))
1929+
psjResult := beginPartialStateJoin(t, server1, room, alice)
1930+
defer psjResult.Destroy()
1931+
1932+
// @t28alice:hs1 sends out a device list update which is missed by @elsie:server2.
1933+
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
1934+
testMissedDeviceListUpdateSentOncePartialJoinCompletes(t, deployment, alice,
1935+
server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room,
1936+
psjResult, "", false,
1937+
)
1938+
})
1939+
1940+
// test that device list updates are sent to remote homeservers incorrectly omitted from the
1941+
// `/send_join` response once the partial state join completes, even if the remote
1942+
// homeserver leaves the room beforehand.
1943+
t.Run("Device list updates reach incorrectly absent servers once partial state join completes even though remote server left room", func(t *testing.T) {
1944+
alice, server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room, cleanup := setupOutgoingDeviceListUpdateTest(
1945+
t, deployment, "t29alice", handleSendJoinRequestsWithIncompleteServersInRoom,
1946+
)
1947+
defer cleanup()
1948+
1949+
// The room starts with @charlie:server1 and @derek:server1 in it.
1950+
// @elsie:server2 joins the room, followed by @t29alice:hs1.
1951+
// server1 does not tell hs1 that server2 is in the room.
1952+
room.AddEvent(createJoinEvent(t, server2, room, server2.UserID("elsie")))
1953+
psjResult := beginPartialStateJoin(t, server1, room, alice)
1954+
defer psjResult.Destroy()
1955+
1956+
// @t29alice:hs1 sends out a device list update which is missed by @elsie:server2.
1957+
// @elsie:server2 joins another room shared with @t29alice:hs1 and leaves the partial state room.
1958+
// @elsie:server2 must receive missed device list updates once the partial state join finishes.
1959+
testMissedDeviceListUpdateSentOncePartialJoinCompletes(t, deployment, alice,
1960+
server1, server2, deviceListUpdateChannel1, deviceListUpdateChannel2, room,
1961+
psjResult, "", true,
1962+
)
1963+
})
17221964
})
17231965
}
17241966

tests/federation_room_join_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func TestJoinViaRoomIDAndServerName(t *testing.T) {
6161
w.WriteHeader(502)
6262
return
6363
}
64-
federation.SendJoinRequestsHandler(srv, w, req, false)
64+
federation.SendJoinRequestsHandler(srv, w, req, false, false)
6565
})).Methods("PUT")
6666

6767
ver := alice.GetDefaultRoomVersion(t)

0 commit comments

Comments
 (0)