Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Implement MSC3846: Allowing widgets to access TURN servers (#9061)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
robintown authored Aug 10, 2022
1 parent 103b60d commit 28ed87b
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 4 deletions.
42 changes: 41 additions & 1 deletion src/stores/widgets/StopGapWidgetDriver.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -20,6 +20,7 @@ import {
IOpenIDCredentials,
IOpenIDUpdate,
ISendEventDetails,
ITurnServer,
IRoomEvent,
MatrixCapabilities,
OpenIDRequestState,
Expand All @@ -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";
Expand Down Expand Up @@ -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>;

Expand Down Expand Up @@ -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);
}
}
}
59 changes: 57 additions & 2 deletions test/stores/widgets/StopGapWidgetDriver-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
});
});
});
9 changes: 8 additions & 1 deletion test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit 28ed87b

Please sign in to comment.