Implement MSC3846: Allowing widgets to access TURN servers (#9061)

* Implement MSC3819: Allowing widgets to send/receive to-device messages

* Don't change the room events and state events drivers

* Implement MSC3846: Allowing widgets to access TURN servers

* Update to latest matrix-widget-api changes

* Support sending encrypted to-device messages

* Yield a TURN server immediately

* Use queueToDevice for better reliability

* Update types for latest WidgetDriver changes

* Upgrade matrix-widget-api

* Add tests

* Test StopGapWidget

* Fix a potential memory leak

* Add tests

* Empty commit to retry CI
This commit is contained in:
Robin 2022-08-10 09:26:42 -04:00 committed by GitHub
parent 103b60dfb5
commit 28ed87bffe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 106 additions and 4 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 2022 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.
@ -20,6 +20,7 @@ import {
IOpenIDCredentials,
IOpenIDUpdate,
ISendEventDetails,
ITurnServer,
IRoomEvent,
MatrixCapabilities,
OpenIDRequestState,
@ -30,6 +31,7 @@ import {
WidgetEventCapability,
WidgetKind,
} from "matrix-widget-api";
import { ClientEvent, ITurnServer as IClientTurnServer } from "matrix-js-sdk/src/client";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { IContent, IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
@ -62,6 +64,12 @@ function setRememberedCapabilitiesForWidget(widget: Widget, caps: Capability[])
localStorage.setItem(`widget_${widget.id}_approved_caps`, JSON.stringify(caps));
}
const normalizeTurnServer = ({ urls, username, credential }: IClientTurnServer): ITurnServer => ({
uris: urls,
username,
password: credential,
});
export class StopGapWidgetDriver extends WidgetDriver {
private allowedCapabilities: Set<Capability>;
@ -326,4 +334,36 @@ export class StopGapWidgetDriver extends WidgetDriver {
public async navigate(uri: string): Promise<void> {
navigateToPermalink(uri);
}
public async* getTurnServers(): AsyncGenerator<ITurnServer> {
const client = MatrixClientPeg.get();
if (!client.pollingTurnServers || !client.getTurnServers().length) return;
let setTurnServer: (server: ITurnServer) => void;
let setError: (error: Error) => void;
const onTurnServers = ([server]: IClientTurnServer[]) => setTurnServer(normalizeTurnServer(server));
const onTurnServersError = (error: Error, fatal: boolean) => { if (fatal) setError(error); };
client.on(ClientEvent.TurnServers, onTurnServers);
client.on(ClientEvent.TurnServersError, onTurnServersError);
try {
const initialTurnServer = client.getTurnServers()[0];
yield normalizeTurnServer(initialTurnServer);
// Repeatedly listen for new TURN servers until an error occurs or
// the caller stops this generator
while (true) {
yield await new Promise<ITurnServer>((resolve, reject) => {
setTurnServer = resolve;
setError = reject;
});
}
} finally {
// The loop was broken - clean up
client.off(ClientEvent.TurnServers, onTurnServers);
client.off(ClientEvent.TurnServersError, onTurnServersError);
}
}
}

View file

@ -15,8 +15,8 @@ limitations under the License.
*/
import { mocked, MockedObject } from "jest-mock";
import { Widget, WidgetKind, WidgetDriver } from "matrix-widget-api";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Widget, WidgetKind, WidgetDriver, ITurnServer } from "matrix-widget-api";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "matrix-js-sdk/src/client";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
@ -76,4 +76,59 @@ describe("StopGapWidgetDriver", () => {
expect(client.encryptAndSendToDevices.mock.calls).toMatchSnapshot();
});
});
describe("getTurnServers", () => {
it("stops if VoIP isn't supported", async () => {
jest.spyOn(client, "pollingTurnServers", "get").mockReturnValue(false);
const servers = driver.getTurnServers();
expect(await servers.next()).toEqual({ value: undefined, done: true });
});
it("stops if the homeserver provides no TURN servers", async () => {
const servers = driver.getTurnServers();
expect(await servers.next()).toEqual({ value: undefined, done: true });
});
it("gets TURN servers", async () => {
const server1: ITurnServer = {
uris: [
"turn:turn.example.com:3478?transport=udp",
"turn:10.20.30.40:3478?transport=tcp",
"turns:10.20.30.40:443?transport=tcp",
],
username: "1443779631:@user:example.com",
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
};
const server2: ITurnServer = {
uris: [
"turn:turn.example.com:3478?transport=udp",
"turn:10.20.30.40:3478?transport=tcp",
"turns:10.20.30.40:443?transport=tcp",
],
username: "1448999322:@user:example.com",
password: "hunter2",
};
const clientServer1: IClientTurnServer = {
urls: server1.uris,
username: server1.username,
credential: server1.password,
};
const clientServer2: IClientTurnServer = {
urls: server2.uris,
username: server2.username,
credential: server2.password,
};
client.getTurnServers.mockReturnValue([clientServer1]);
const servers = driver.getTurnServers();
expect(await servers.next()).toEqual({ value: server1, done: false });
const nextServer = servers.next();
client.getTurnServers.mockReturnValue([clientServer2]);
client.emit(ClientEvent.TurnServers, [clientServer2]);
expect(await nextServer).toEqual({ value: server2, done: false });
await servers.return(undefined);
});
});
});

View file

@ -74,7 +74,7 @@ export function createTestClient(): MatrixClient {
const eventEmitter = new EventEmitter();
let txnId = 1;
return {
const client = {
getHomeserverUrl: jest.fn(),
getIdentityServerUrl: jest.fn(),
getDomain: jest.fn().mockReturnValue("matrix.org"),
@ -118,6 +118,7 @@ export function createTestClient(): MatrixClient {
getThirdpartyProtocols: jest.fn().mockResolvedValue({}),
getClientWellKnown: jest.fn().mockReturnValue(null),
supportsVoip: jest.fn().mockReturnValue(true),
getTurnServers: jest.fn().mockReturnValue([]),
getTurnServersExpiry: jest.fn().mockReturnValue(2 ^ 32),
getThirdpartyUser: jest.fn().mockResolvedValue([]),
getAccountData: (type) => {
@ -173,6 +174,12 @@ export function createTestClient(): MatrixClient {
queueToDevice: jest.fn().mockResolvedValue(undefined),
encryptAndSendToDevices: jest.fn().mockResolvedValue(undefined),
} as unknown as MatrixClient;
Object.defineProperty(client, "pollingTurnServers", {
configurable: true,
get: () => true,
});
return client;
}
type MakeEventPassThruProps = {