1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Break coupling between Room and Crypto.trackRoomDevices (#2998)

`Room` and `Crypto` currently have some tight coupling in the form of a call to
`trackRoomDevices` when out-of-band members are loaded. We can improve this by
instead having Crypto listen out for a `RoomSateEvent.Update` notification.
This commit is contained in:
Richard van der Hoff
2022-12-23 11:03:14 +00:00
committed by GitHub
parent ff1b0e51ea
commit ce776b9989
5 changed files with 137 additions and 12 deletions

View File

@ -24,7 +24,7 @@ import MockHttpBackend from "matrix-mock-request";
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store"; import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
import { logger } from "../src/logger"; import { logger } from "../src/logger";
import { syncPromise } from "./test-utils/test-utils"; import { syncPromise } from "./test-utils/test-utils";
import { createClient } from "../src/matrix"; import { createClient, IStartClientOpts } from "../src/matrix";
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client"; import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
import { MockStorageApi } from "./MockStorageApi"; import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils"; import { encodeUri } from "../src/utils";
@ -79,9 +79,12 @@ export class TestClient {
/** /**
* start the client, and wait for it to initialise. * start the client, and wait for it to initialise.
*/ */
public start(): Promise<void> { public start(opts: IStartClientOpts = {}): Promise<void> {
logger.log(this + ": starting"); logger.log(this + ": starting");
this.httpBackend.when("GET", "/versions").respond(200, {}); this.httpBackend.when("GET", "/versions").respond(200, {
// we have tests that rely on support for lazy-loading members
versions: ["r0.5.0"],
});
this.httpBackend.when("GET", "/pushrules").respond(200, {}); this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload(); this.expectDeviceKeyUpload();
@ -93,6 +96,8 @@ export class TestClient {
this.client.startClient({ this.client.startClient({
// set this so that we can get hold of failed events // set this so that we can get hold of failed events
pendingEventOrdering: PendingEventOrdering.Detached, pendingEventOrdering: PendingEventOrdering.Detached,
...opts,
}); });
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => { return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {

View File

@ -1590,4 +1590,92 @@ describe("megolm", () => {
aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1), aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1),
]); ]);
}); });
describe("Lazy-loading member lists", () => {
let p2pSession: Olm.Session;
beforeEach(async () => {
// set up the aliceTestClient so that it is a room with no known members
aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await aliceTestClient.start({ lazyLoadMembers: true });
aliceTestClient.client.setGlobalErrorOnUnknownDevices(false);
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
await aliceTestClient.flushSync();
p2pSession = await establishOlmSession(aliceTestClient, testOlmAccount);
});
async function expectMembershipRequest(roomId: string, members: string[]): Promise<void> {
const membersPath = `/rooms/${encodeURIComponent(roomId)}/members?not_membership=leave`;
aliceTestClient.httpBackend.when("GET", membersPath).respond(200, {
chunk: [
testUtils.mkMembershipCustom({
membership: "join",
sender: "@bob:xyz",
}),
],
});
await aliceTestClient.httpBackend.flush(membersPath, 1);
}
it("Sending an event initiates a member list sync", async () => {
// we expect a call to the /members list...
const memberListPromise = expectMembershipRequest(ROOM_ID, ["@bob:xyz"]);
// then a request for bob's devices...
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
// then a to-device with the room_key
const inboundGroupSessionPromise = expectSendRoomKey(
aliceTestClient.httpBackend,
"@bob:xyz",
testOlmAccount,
p2pSession,
);
// and finally the megolm message
const megolmMessagePromise = expectSendMegolmMessage(
aliceTestClient.httpBackend,
inboundGroupSessionPromise,
);
// kick it off
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, "test");
await Promise.all([
sendPromise,
megolmMessagePromise,
memberListPromise,
aliceTestClient.httpBackend.flush("/keys/query", 1),
]);
});
it("loading the membership list inhibits a later load", async () => {
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
await Promise.all([room.loadMembersIfNeeded(), expectMembershipRequest(ROOM_ID, ["@bob:xyz"])]);
// expect a request for bob's devices...
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
// then a to-device with the room_key
const inboundGroupSessionPromise = expectSendRoomKey(
aliceTestClient.httpBackend,
"@bob:xyz",
testOlmAccount,
p2pSession,
);
// and finally the megolm message
const megolmMessagePromise = expectSendMegolmMessage(
aliceTestClient.httpBackend,
inboundGroupSessionPromise,
);
// kick it off
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, "test");
await Promise.all([sendPromise, megolmMessagePromise, aliceTestClient.httpBackend.flush("/keys/query", 1)]);
});
});
}); });

View File

@ -89,6 +89,7 @@ import { ISyncResponse } from "../sync-accumulator";
import { ISignatures } from "../@types/signed"; import { ISignatures } from "../@types/signed";
import { IMessage } from "./algorithms/olm"; import { IMessage } from "./algorithms/olm";
import { CryptoBackend } from "../common-crypto/CryptoBackend"; import { CryptoBackend } from "../common-crypto/CryptoBackend";
import { RoomState, RoomStateEvent } from "../models/room-state";
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
@ -2606,14 +2607,23 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
await storeConfigPromise; await storeConfigPromise;
} }
if (!this.lazyLoadMembers) { logger.log(`Enabling encryption in ${roomId}`);
logger.log(
"Enabling encryption in " + roomId + "; " + "starting to track device lists for all users therein",
);
// we don't want to force a download of the full membership list of this room, but as soon as we have that
// list we can start tracking the device list.
if (room.membersLoaded()) {
await this.trackRoomDevicesImpl(room); await this.trackRoomDevicesImpl(room);
} else { } else {
logger.log("Enabling encryption in " + roomId); // wait for the membership list to be loaded
const onState = (_state: RoomState): void => {
room.off(RoomStateEvent.Update, onState);
if (room.membersLoaded()) {
this.trackRoomDevicesImpl(room).catch((e) => {
logger.error(`Error enabling device tracking in ${roomId}`, e);
});
}
};
room.on(RoomStateEvent.Update, onState);
} }
} }
@ -2622,6 +2632,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* *
* @param roomId - The room ID to start tracking devices in. * @param roomId - The room ID to start tracking devices in.
* @returns when all devices for the room have been fetched and marked to track * @returns when all devices for the room have been fetched and marked to track
* @deprecated there's normally no need to call this function: device list tracking
* will be enabled as soon as we have the full membership list.
*/ */
public trackRoomDevices(roomId: string): Promise<void> { public trackRoomDevices(roomId: string): Promise<void> {
const room = this.clientStore.getRoom(roomId); const room = this.clientStore.getRoom(roomId);

View File

@ -623,6 +623,16 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
return this.oobMemberFlags.status === OobStatus.NotStarted; return this.oobMemberFlags.status === OobStatus.NotStarted;
} }
/**
* Check if loading of out-of-band-members has completed
*
* @returns true if the full membership list of this room has been loaded. False if it is not started or is in
* progress.
*/
public outOfBandMembersReady(): boolean {
return this.oobMemberFlags.status === OobStatus.Finished;
}
/** /**
* Mark this room state as waiting for out-of-band members, * Mark this room state as waiting for out-of-band members,
* ensuring it doesn't ask for them to be requested again * ensuring it doesn't ask for them to be requested again

View File

@ -888,6 +888,20 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
return { memberEvents, fromServer }; return { memberEvents, fromServer };
} }
/**
* Check if loading of out-of-band-members has completed
*
* @returns true if the full membership list of this room has been loaded (including if lazy-loading is disabled).
* False if the load is not started or is in progress.
*/
public membersLoaded(): boolean {
if (!this.opts.lazyLoadMembers) {
return true;
}
return this.currentState.outOfBandMembersReady();
}
/** /**
* Preloads the member list in case lazy loading * Preloads the member list in case lazy loading
* of memberships is in use. Can be called multiple times, * of memberships is in use. Can be called multiple times,
@ -909,10 +923,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
const inMemoryUpdate = this.loadMembers() const inMemoryUpdate = this.loadMembers()
.then((result) => { .then((result) => {
this.currentState.setOutOfBandMembers(result.memberEvents); this.currentState.setOutOfBandMembers(result.memberEvents);
// now the members are loaded, start to track the e2e devices if needed
if (this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId)) {
this.client.crypto!.trackRoomDevices(this.roomId);
}
return result.fromServer; return result.fromServer;
}) })
.catch((err) => { .catch((err) => {