You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Emit an event when the client receives TURN servers (#2529)
* Emit an event when the client receives TURN servers * Add tests * Fix lints
This commit is contained in:
@ -14,8 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { MatrixClient, ClientEvent } from "../../src/client";
|
||||
import { Filter } from "../../src/filter";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||
import {
|
||||
@ -35,10 +37,16 @@ import * as testUtils from "../test-utils/test-utils";
|
||||
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||
import { ContentHelpers, Room } from "../../src";
|
||||
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
jest.mock("../../src/webrtc/call", () => ({
|
||||
...jest.requireActual("../../src/webrtc/call"),
|
||||
supportsMatrixCall: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
const userId = "@alice:bar";
|
||||
const identityServerUrl = "https://identity.server";
|
||||
@ -160,6 +168,24 @@ describe("MatrixClient", function() {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
function makeClient() {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
request: function() {} as any, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
});
|
||||
// FIXME: We shouldn't be yanking http like this.
|
||||
client.http = [
|
||||
"authedRequest", "getContentUri", "request", "uploadContent",
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
@ -177,21 +203,7 @@ describe("MatrixClient", function() {
|
||||
store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true));
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
request: function() {} as any, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
});
|
||||
// FIXME: We shouldn't be yanking http like this.
|
||||
client.http = [
|
||||
"authedRequest", "getContentUri", "request", "uploadContent",
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
makeClient();
|
||||
|
||||
// set reasonable working defaults
|
||||
acceptKeepalives = true;
|
||||
@ -1299,6 +1311,93 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("pollingTurnServers", () => {
|
||||
afterEach(() => {
|
||||
mocked(supportsMatrixCall).mockReset();
|
||||
});
|
||||
|
||||
it("is false if the client isn't started", () => {
|
||||
expect(client.clientRunning).toBe(false);
|
||||
expect(client.pollingTurnServers).toBe(false);
|
||||
});
|
||||
|
||||
it("is false if VoIP is not supported", async () => {
|
||||
mocked(supportsMatrixCall).mockReturnValue(false);
|
||||
makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock
|
||||
await client.startClient();
|
||||
expect(client.pollingTurnServers).toBe(false);
|
||||
});
|
||||
|
||||
it("is true if VoIP is supported", async () => {
|
||||
mocked(supportsMatrixCall).mockReturnValue(true);
|
||||
makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock
|
||||
await client.startClient();
|
||||
expect(client.pollingTurnServers).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkTurnServers", () => {
|
||||
beforeAll(() => {
|
||||
mocked(supportsMatrixCall).mockReturnValue(true);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mocked(supportsMatrixCall).mockReset();
|
||||
});
|
||||
|
||||
it("emits an event when new TURN creds are found", async () => {
|
||||
const turnServer = {
|
||||
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=",
|
||||
};
|
||||
jest.spyOn(client, "turnServer").mockResolvedValue(turnServer);
|
||||
|
||||
const events: any[][] = [];
|
||||
const onTurnServers = (...args) => events.push(args);
|
||||
client.on(ClientEvent.TurnServers, onTurnServers);
|
||||
expect(await client.checkTurnServers()).toBe(true);
|
||||
client.off(ClientEvent.TurnServers, onTurnServers);
|
||||
expect(events).toEqual([[[{
|
||||
urls: turnServer.uris,
|
||||
username: turnServer.username,
|
||||
credential: turnServer.password,
|
||||
}]]]);
|
||||
});
|
||||
|
||||
it("emits an event when an error occurs", async () => {
|
||||
const error = new Error(":(");
|
||||
jest.spyOn(client, "turnServer").mockRejectedValue(error);
|
||||
|
||||
const events: any[][] = [];
|
||||
const onTurnServersError = (...args) => events.push(args);
|
||||
client.on(ClientEvent.TurnServersError, onTurnServersError);
|
||||
expect(await client.checkTurnServers()).toBe(false);
|
||||
client.off(ClientEvent.TurnServersError, onTurnServersError);
|
||||
expect(events).toEqual([[error, false]]); // non-fatal
|
||||
});
|
||||
|
||||
it("considers 403 errors fatal", async () => {
|
||||
const error = { httpStatus: 403 };
|
||||
jest.spyOn(client, "turnServer").mockRejectedValue(error);
|
||||
|
||||
const events: any[][] = [];
|
||||
const onTurnServersError = (...args) => events.push(args);
|
||||
client.on(ClientEvent.TurnServersError, onTurnServersError);
|
||||
expect(await client.checkTurnServers()).toBe(false);
|
||||
client.off(ClientEvent.TurnServersError, onTurnServersError);
|
||||
expect(events).toEqual([[error, true]]); // fatal
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptAndSendToDevices", () => {
|
||||
it("throws an error if crypto is unavailable", () => {
|
||||
client.crypto = undefined;
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015-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.
|
||||
@ -506,7 +506,7 @@ interface ITurnServerResponse {
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
interface ITurnServer {
|
||||
export interface ITurnServer {
|
||||
urls: string[];
|
||||
username: string;
|
||||
credential: string;
|
||||
@ -791,6 +791,8 @@ export enum ClientEvent {
|
||||
DeleteRoom = "deleteRoom",
|
||||
SyncUnexpectedError = "sync.unexpectedError",
|
||||
ClientWellKnown = "WellKnown.client",
|
||||
TurnServers = "turnServers",
|
||||
TurnServersError = "turnServers.error",
|
||||
}
|
||||
|
||||
type RoomEvents = RoomEvent.Name
|
||||
@ -861,6 +863,8 @@ export type ClientEventHandlerMap = {
|
||||
[ClientEvent.DeleteRoom]: (roomId: string) => void;
|
||||
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
|
||||
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
|
||||
[ClientEvent.TurnServers]: (servers: ITurnServer[]) => void;
|
||||
[ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void;
|
||||
} & RoomEventHandlerMap
|
||||
& RoomStateEventHandlerMap
|
||||
& CryptoEventHandlerMap
|
||||
@ -937,7 +941,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
protected clientWellKnownPromise: Promise<IClientWellKnown>;
|
||||
protected turnServers: ITurnServer[] = [];
|
||||
protected turnServersExpiry = 0;
|
||||
protected checkTurnServersIntervalID: ReturnType<typeof setInterval>;
|
||||
protected checkTurnServersIntervalID: ReturnType<typeof setInterval> | null = null;
|
||||
protected exportedOlmDeviceToImport: IExportedOlmDevice;
|
||||
protected txnCtr = 0;
|
||||
protected mediaHandler = new MediaHandler(this);
|
||||
@ -1230,6 +1234,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.callEventHandler = null;
|
||||
|
||||
global.clearInterval(this.checkTurnServersIntervalID);
|
||||
this.checkTurnServersIntervalID = null;
|
||||
|
||||
if (this.clientWellKnownIntervalID !== undefined) {
|
||||
global.clearInterval(this.clientWellKnownIntervalID);
|
||||
}
|
||||
@ -6343,6 +6349,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.turnServersExpiry;
|
||||
}
|
||||
|
||||
public get pollingTurnServers(): boolean {
|
||||
return this.checkTurnServersIntervalID !== null;
|
||||
}
|
||||
|
||||
// XXX: Intended private, used in code.
|
||||
public async checkTurnServers(): Promise<boolean> {
|
||||
if (!this.canSupportVoip) {
|
||||
@ -6370,17 +6380,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// The TTL is in seconds but we work in ms
|
||||
this.turnServersExpiry = Date.now() + (res.ttl * 1000);
|
||||
credentialsGood = true;
|
||||
this.emit(ClientEvent.TurnServers, this.turnServers);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to get TURN URIs", err);
|
||||
// If we get a 403, there's no point in looping forever.
|
||||
if (err.httpStatus === 403) {
|
||||
// We got a 403, so there's no point in looping forever.
|
||||
logger.info("TURN access unavailable for this account: stopping credentials checks");
|
||||
if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID);
|
||||
this.checkTurnServersIntervalID = null;
|
||||
this.emit(ClientEvent.TurnServersError, err, true); // fatal
|
||||
} else {
|
||||
// otherwise, if we failed for whatever reason, try again the next time we're called.
|
||||
this.emit(ClientEvent.TurnServersError, err, false); // non-fatal
|
||||
}
|
||||
}
|
||||
// otherwise, if we failed for whatever reason, try again the next time we're called.
|
||||
}
|
||||
|
||||
return credentialsGood;
|
||||
|
Reference in New Issue
Block a user