From 28ed87bffeea84bcb6bb3621c2f9efdefd3cb9d1 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 10 Aug 2022 09:26:42 -0400 Subject: [PATCH] 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 --- src/stores/widgets/StopGapWidgetDriver.ts | 42 ++++++++++++- .../widgets/StopGapWidgetDriver-test.ts | 59 ++++++++++++++++++- test/test-utils/test-utils.ts | 9 ++- 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index ee69f0ca9c..8fe18dbc8c 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -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; @@ -326,4 +334,36 @@ export class StopGapWidgetDriver extends WidgetDriver { public async navigate(uri: string): Promise { navigateToPermalink(uri); } + + public async* getTurnServers(): AsyncGenerator { + 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((resolve, reject) => { + setTurnServer = resolve; + setError = reject; + }); + } + } finally { + // The loop was broken - clean up + client.off(ClientEvent.TurnServers, onTurnServers); + client.off(ClientEvent.TurnServersError, onTurnServersError); + } + } } diff --git a/test/stores/widgets/StopGapWidgetDriver-test.ts b/test/stores/widgets/StopGapWidgetDriver-test.ts index 7dab35052b..7904629428 100644 --- a/test/stores/widgets/StopGapWidgetDriver-test.ts +++ b/test/stores/widgets/StopGapWidgetDriver-test.ts @@ -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); + }); + }); }); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 391683d5d1..fd120a077b 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -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 = {