You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { MatrixClient } from "../../src/client";
|
import { MatrixClient, ClientEvent } from "../../src/client";
|
||||||
import { Filter } from "../../src/filter";
|
import { Filter } from "../../src/filter";
|
||||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||||
import {
|
import {
|
||||||
@ -35,10 +37,16 @@ import * as testUtils from "../test-utils/test-utils";
|
|||||||
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||||
import { ContentHelpers, Room } from "../../src";
|
import { ContentHelpers, Room } from "../../src";
|
||||||
|
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
jest.mock("../../src/webrtc/call", () => ({
|
||||||
|
...jest.requireActual("../../src/webrtc/call"),
|
||||||
|
supportsMatrixCall: jest.fn(() => false),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("MatrixClient", function() {
|
describe("MatrixClient", function() {
|
||||||
const userId = "@alice:bar";
|
const userId = "@alice:bar";
|
||||||
const identityServerUrl = "https://identity.server";
|
const identityServerUrl = "https://identity.server";
|
||||||
@ -160,6 +168,24 @@ describe("MatrixClient", function() {
|
|||||||
return new Promise(() => {});
|
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() {
|
beforeEach(function() {
|
||||||
scheduler = [
|
scheduler = [
|
||||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||||
@ -177,21 +203,7 @@ describe("MatrixClient", function() {
|
|||||||
store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||||
store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||||
store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true));
|
store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true));
|
||||||
client = new MatrixClient({
|
makeClient();
|
||||||
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);
|
|
||||||
|
|
||||||
// set reasonable working defaults
|
// set reasonable working defaults
|
||||||
acceptKeepalives = true;
|
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", () => {
|
describe("encryptAndSendToDevices", () => {
|
||||||
it("throws an error if crypto is unavailable", () => {
|
it("throws an error if crypto is unavailable", () => {
|
||||||
client.crypto = undefined;
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -506,7 +506,7 @@ interface ITurnServerResponse {
|
|||||||
ttl: number;
|
ttl: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITurnServer {
|
export interface ITurnServer {
|
||||||
urls: string[];
|
urls: string[];
|
||||||
username: string;
|
username: string;
|
||||||
credential: string;
|
credential: string;
|
||||||
@ -791,6 +791,8 @@ export enum ClientEvent {
|
|||||||
DeleteRoom = "deleteRoom",
|
DeleteRoom = "deleteRoom",
|
||||||
SyncUnexpectedError = "sync.unexpectedError",
|
SyncUnexpectedError = "sync.unexpectedError",
|
||||||
ClientWellKnown = "WellKnown.client",
|
ClientWellKnown = "WellKnown.client",
|
||||||
|
TurnServers = "turnServers",
|
||||||
|
TurnServersError = "turnServers.error",
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoomEvents = RoomEvent.Name
|
type RoomEvents = RoomEvent.Name
|
||||||
@ -861,6 +863,8 @@ export type ClientEventHandlerMap = {
|
|||||||
[ClientEvent.DeleteRoom]: (roomId: string) => void;
|
[ClientEvent.DeleteRoom]: (roomId: string) => void;
|
||||||
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
|
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
|
||||||
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
|
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
|
||||||
|
[ClientEvent.TurnServers]: (servers: ITurnServer[]) => void;
|
||||||
|
[ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void;
|
||||||
} & RoomEventHandlerMap
|
} & RoomEventHandlerMap
|
||||||
& RoomStateEventHandlerMap
|
& RoomStateEventHandlerMap
|
||||||
& CryptoEventHandlerMap
|
& CryptoEventHandlerMap
|
||||||
@ -937,7 +941,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
protected clientWellKnownPromise: Promise<IClientWellKnown>;
|
protected clientWellKnownPromise: Promise<IClientWellKnown>;
|
||||||
protected turnServers: ITurnServer[] = [];
|
protected turnServers: ITurnServer[] = [];
|
||||||
protected turnServersExpiry = 0;
|
protected turnServersExpiry = 0;
|
||||||
protected checkTurnServersIntervalID: ReturnType<typeof setInterval>;
|
protected checkTurnServersIntervalID: ReturnType<typeof setInterval> | null = null;
|
||||||
protected exportedOlmDeviceToImport: IExportedOlmDevice;
|
protected exportedOlmDeviceToImport: IExportedOlmDevice;
|
||||||
protected txnCtr = 0;
|
protected txnCtr = 0;
|
||||||
protected mediaHandler = new MediaHandler(this);
|
protected mediaHandler = new MediaHandler(this);
|
||||||
@ -1230,6 +1234,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
this.callEventHandler = null;
|
this.callEventHandler = null;
|
||||||
|
|
||||||
global.clearInterval(this.checkTurnServersIntervalID);
|
global.clearInterval(this.checkTurnServersIntervalID);
|
||||||
|
this.checkTurnServersIntervalID = null;
|
||||||
|
|
||||||
if (this.clientWellKnownIntervalID !== undefined) {
|
if (this.clientWellKnownIntervalID !== undefined) {
|
||||||
global.clearInterval(this.clientWellKnownIntervalID);
|
global.clearInterval(this.clientWellKnownIntervalID);
|
||||||
}
|
}
|
||||||
@ -6343,6 +6349,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return this.turnServersExpiry;
|
return this.turnServersExpiry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get pollingTurnServers(): boolean {
|
||||||
|
return this.checkTurnServersIntervalID !== null;
|
||||||
|
}
|
||||||
|
|
||||||
// XXX: Intended private, used in code.
|
// XXX: Intended private, used in code.
|
||||||
public async checkTurnServers(): Promise<boolean> {
|
public async checkTurnServers(): Promise<boolean> {
|
||||||
if (!this.canSupportVoip) {
|
if (!this.canSupportVoip) {
|
||||||
@ -6370,17 +6380,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// The TTL is in seconds but we work in ms
|
// The TTL is in seconds but we work in ms
|
||||||
this.turnServersExpiry = Date.now() + (res.ttl * 1000);
|
this.turnServersExpiry = Date.now() + (res.ttl * 1000);
|
||||||
credentialsGood = true;
|
credentialsGood = true;
|
||||||
|
this.emit(ClientEvent.TurnServers, this.turnServers);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Failed to get TURN URIs", 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) {
|
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");
|
logger.info("TURN access unavailable for this account: stopping credentials checks");
|
||||||
if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID);
|
if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID);
|
||||||
this.checkTurnServersIntervalID = null;
|
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;
|
return credentialsGood;
|
||||||
|
Reference in New Issue
Block a user