Skip to content

Commit c629d2f

Browse files
authored
Emit an event when the client receives TURN servers (#2529)
* Emit an event when the client receives TURN servers * Add tests * Fix lints
1 parent 43b4538 commit c629d2f

File tree

2 files changed

+134
-21
lines changed

2 files changed

+134
-21
lines changed

spec/unit/matrix-client.spec.ts

Lines changed: 115 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import { mocked } from "jest-mock";
18+
1719
import { logger } from "../../src/logger";
18-
import { MatrixClient } from "../../src/client";
20+
import { MatrixClient, ClientEvent } from "../../src/client";
1921
import { Filter } from "../../src/filter";
2022
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
2123
import {
@@ -35,10 +37,16 @@ import * as testUtils from "../test-utils/test-utils";
3537
import { makeBeaconInfoContent } from "../../src/content-helpers";
3638
import { M_BEACON_INFO } from "../../src/@types/beacon";
3739
import { ContentHelpers, Room } from "../../src";
40+
import { supportsMatrixCall } from "../../src/webrtc/call";
3841
import { makeBeaconEvent } from "../test-utils/beacon";
3942

4043
jest.useFakeTimers();
4144

45+
jest.mock("../../src/webrtc/call", () => ({
46+
...jest.requireActual("../../src/webrtc/call"),
47+
supportsMatrixCall: jest.fn(() => false),
48+
}));
49+
4250
describe("MatrixClient", function() {
4351
const userId = "@alice:bar";
4452
const identityServerUrl = "https://identity.server";
@@ -160,6 +168,24 @@ describe("MatrixClient", function() {
160168
return new Promise(() => {});
161169
}
162170

171+
function makeClient() {
172+
client = new MatrixClient({
173+
baseUrl: "https://my.home.server",
174+
idBaseUrl: identityServerUrl,
175+
accessToken: "my.access.token",
176+
request: function() {} as any, // NOP
177+
store: store,
178+
scheduler: scheduler,
179+
userId: userId,
180+
});
181+
// FIXME: We shouldn't be yanking http like this.
182+
client.http = [
183+
"authedRequest", "getContentUri", "request", "uploadContent",
184+
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
185+
client.http.authedRequest.mockImplementation(httpReq);
186+
client.http.request.mockImplementation(httpReq);
187+
}
188+
163189
beforeEach(function() {
164190
scheduler = [
165191
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
@@ -177,21 +203,7 @@ describe("MatrixClient", function() {
177203
store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
178204
store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
179205
store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true));
180-
client = new MatrixClient({
181-
baseUrl: "https://my.home.server",
182-
idBaseUrl: identityServerUrl,
183-
accessToken: "my.access.token",
184-
request: function() {} as any, // NOP
185-
store: store,
186-
scheduler: scheduler,
187-
userId: userId,
188-
});
189-
// FIXME: We shouldn't be yanking http like this.
190-
client.http = [
191-
"authedRequest", "getContentUri", "request", "uploadContent",
192-
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
193-
client.http.authedRequest.mockImplementation(httpReq);
194-
client.http.request.mockImplementation(httpReq);
206+
makeClient();
195207

196208
// set reasonable working defaults
197209
acceptKeepalives = true;
@@ -1299,6 +1311,93 @@ describe("MatrixClient", function() {
12991311
});
13001312
});
13011313

1314+
describe("pollingTurnServers", () => {
1315+
afterEach(() => {
1316+
mocked(supportsMatrixCall).mockReset();
1317+
});
1318+
1319+
it("is false if the client isn't started", () => {
1320+
expect(client.clientRunning).toBe(false);
1321+
expect(client.pollingTurnServers).toBe(false);
1322+
});
1323+
1324+
it("is false if VoIP is not supported", async () => {
1325+
mocked(supportsMatrixCall).mockReturnValue(false);
1326+
makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock
1327+
await client.startClient();
1328+
expect(client.pollingTurnServers).toBe(false);
1329+
});
1330+
1331+
it("is true if VoIP is supported", async () => {
1332+
mocked(supportsMatrixCall).mockReturnValue(true);
1333+
makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock
1334+
await client.startClient();
1335+
expect(client.pollingTurnServers).toBe(true);
1336+
});
1337+
});
1338+
1339+
describe("checkTurnServers", () => {
1340+
beforeAll(() => {
1341+
mocked(supportsMatrixCall).mockReturnValue(true);
1342+
});
1343+
1344+
beforeEach(() => {
1345+
makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock
1346+
});
1347+
1348+
afterAll(() => {
1349+
mocked(supportsMatrixCall).mockReset();
1350+
});
1351+
1352+
it("emits an event when new TURN creds are found", async () => {
1353+
const turnServer = {
1354+
uris: [
1355+
"turn:turn.example.com:3478?transport=udp",
1356+
"turn:10.20.30.40:3478?transport=tcp",
1357+
"turns:10.20.30.40:443?transport=tcp",
1358+
],
1359+
username: "1443779631:@user:example.com",
1360+
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
1361+
};
1362+
jest.spyOn(client, "turnServer").mockResolvedValue(turnServer);
1363+
1364+
const events: any[][] = [];
1365+
const onTurnServers = (...args) => events.push(args);
1366+
client.on(ClientEvent.TurnServers, onTurnServers);
1367+
expect(await client.checkTurnServers()).toBe(true);
1368+
client.off(ClientEvent.TurnServers, onTurnServers);
1369+
expect(events).toEqual([[[{
1370+
urls: turnServer.uris,
1371+
username: turnServer.username,
1372+
credential: turnServer.password,
1373+
}]]]);
1374+
});
1375+
1376+
it("emits an event when an error occurs", async () => {
1377+
const error = new Error(":(");
1378+
jest.spyOn(client, "turnServer").mockRejectedValue(error);
1379+
1380+
const events: any[][] = [];
1381+
const onTurnServersError = (...args) => events.push(args);
1382+
client.on(ClientEvent.TurnServersError, onTurnServersError);
1383+
expect(await client.checkTurnServers()).toBe(false);
1384+
client.off(ClientEvent.TurnServersError, onTurnServersError);
1385+
expect(events).toEqual([[error, false]]); // non-fatal
1386+
});
1387+
1388+
it("considers 403 errors fatal", async () => {
1389+
const error = { httpStatus: 403 };
1390+
jest.spyOn(client, "turnServer").mockRejectedValue(error);
1391+
1392+
const events: any[][] = [];
1393+
const onTurnServersError = (...args) => events.push(args);
1394+
client.on(ClientEvent.TurnServersError, onTurnServersError);
1395+
expect(await client.checkTurnServers()).toBe(false);
1396+
client.off(ClientEvent.TurnServersError, onTurnServersError);
1397+
expect(events).toEqual([[error, true]]); // fatal
1398+
});
1399+
});
1400+
13021401
describe("encryptAndSendToDevices", () => {
13031402
it("throws an error if crypto is unavailable", () => {
13041403
client.crypto = undefined;

src/client.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
2+
Copyright 2015-2022 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -506,7 +506,7 @@ interface ITurnServerResponse {
506506
ttl: number;
507507
}
508508

509-
interface ITurnServer {
509+
export interface ITurnServer {
510510
urls: string[];
511511
username: string;
512512
credential: string;
@@ -791,6 +791,8 @@ export enum ClientEvent {
791791
DeleteRoom = "deleteRoom",
792792
SyncUnexpectedError = "sync.unexpectedError",
793793
ClientWellKnown = "WellKnown.client",
794+
TurnServers = "turnServers",
795+
TurnServersError = "turnServers.error",
794796
}
795797

796798
type RoomEvents = RoomEvent.Name
@@ -861,6 +863,8 @@ export type ClientEventHandlerMap = {
861863
[ClientEvent.DeleteRoom]: (roomId: string) => void;
862864
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
863865
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
866+
[ClientEvent.TurnServers]: (servers: ITurnServer[]) => void;
867+
[ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void;
864868
} & RoomEventHandlerMap
865869
& RoomStateEventHandlerMap
866870
& CryptoEventHandlerMap
@@ -937,7 +941,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
937941
protected clientWellKnownPromise: Promise<IClientWellKnown>;
938942
protected turnServers: ITurnServer[] = [];
939943
protected turnServersExpiry = 0;
940-
protected checkTurnServersIntervalID: ReturnType<typeof setInterval>;
944+
protected checkTurnServersIntervalID: ReturnType<typeof setInterval> | null = null;
941945
protected exportedOlmDeviceToImport: IExportedOlmDevice;
942946
protected txnCtr = 0;
943947
protected mediaHandler = new MediaHandler(this);
@@ -1230,6 +1234,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
12301234
this.callEventHandler = null;
12311235

12321236
global.clearInterval(this.checkTurnServersIntervalID);
1237+
this.checkTurnServersIntervalID = null;
1238+
12331239
if (this.clientWellKnownIntervalID !== undefined) {
12341240
global.clearInterval(this.clientWellKnownIntervalID);
12351241
}
@@ -6343,6 +6349,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
63436349
return this.turnServersExpiry;
63446350
}
63456351

6352+
public get pollingTurnServers(): boolean {
6353+
return this.checkTurnServersIntervalID !== null;
6354+
}
6355+
63466356
// XXX: Intended private, used in code.
63476357
public async checkTurnServers(): Promise<boolean> {
63486358
if (!this.canSupportVoip) {
@@ -6370,17 +6380,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
63706380
// The TTL is in seconds but we work in ms
63716381
this.turnServersExpiry = Date.now() + (res.ttl * 1000);
63726382
credentialsGood = true;
6383+
this.emit(ClientEvent.TurnServers, this.turnServers);
63736384
}
63746385
} catch (err) {
63756386
logger.error("Failed to get TURN URIs", err);
6376-
// If we get a 403, there's no point in looping forever.
63776387
if (err.httpStatus === 403) {
6388+
// We got a 403, so there's no point in looping forever.
63786389
logger.info("TURN access unavailable for this account: stopping credentials checks");
63796390
if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID);
63806391
this.checkTurnServersIntervalID = null;
6392+
this.emit(ClientEvent.TurnServersError, err, true); // fatal
6393+
} else {
6394+
// otherwise, if we failed for whatever reason, try again the next time we're called.
6395+
this.emit(ClientEvent.TurnServersError, err, false); // non-fatal
63816396
}
63826397
}
6383-
// otherwise, if we failed for whatever reason, try again the next time we're called.
63846398
}
63856399

63866400
return credentialsGood;

0 commit comments

Comments
 (0)