Skip to content

Commit

Permalink
Implement MSC3846: Allowing widgets to access TURN servers
Browse files Browse the repository at this point in the history
  • Loading branch information
robintown committed Jul 15, 2022
1 parent af36adc commit 4b97173
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 2 deletions.
76 changes: 76 additions & 0 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ import { SimpleObservable } from "./util/SimpleObservable";
import { IOpenIDCredentialsActionRequestData } from "./interfaces/OpenIDCredentialsAction";
import { INavigateActionRequest } from "./interfaces/NavigateAction";
import { IReadEventFromWidgetActionRequest, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
import {
ITurnServer,
IWatchTurnServersRequest,
IUnwatchTurnServersRequest,
IUpdateTurnServersRequestData,
} from "./interfaces/TurnServerActions";
import { Symbols } from "./Symbols";

/**
Expand Down Expand Up @@ -100,6 +106,7 @@ export class ClientWidgetApi extends EventEmitter {
private allowedCapabilities = new Set<Capability>();
private allowedEvents: WidgetEventCapability[] = [];
private isStopped = false;
private turnServers: AsyncGenerator<ITurnServer> | null = null;

/**
* Creates a new client widget API. This will instantiate the transport
Expand Down Expand Up @@ -492,6 +499,71 @@ export class ClientWidgetApi extends EventEmitter {
}
}

private async pollTurnServers(turnServers: AsyncGenerator<ITurnServer>, initialServer: ITurnServer) {
try {
await this.transport.send<IUpdateTurnServersRequestData>(
WidgetApiToWidgetAction.UpdateTurnServers,
initialServer as IUpdateTurnServersRequestData, // it's compatible, but missing the index signature
);

// Pick the generator up where we left off
for await (const server of turnServers) {
await this.transport.send<IUpdateTurnServersRequestData>(
WidgetApiToWidgetAction.UpdateTurnServers,
server as IUpdateTurnServersRequestData, // it's compatible, but missing the index signature
);
}
} catch (e) {
console.error("error polling for TURN servers", e);
}
}

private async handleWatchTurnServers(request: IWatchTurnServersRequest): Promise<void> {
if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) {
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Missing capability"},
});
} else if (this.turnServers) {
// We're already polling, so this is a no-op
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
} else {
try {
const turnServers = this.driver.getTurnServers();

// Peek at the first result, so we can at least verify that the
// client isn't banned from getting TURN servers entirely
const { done, value } = await turnServers.next();
if (done) throw new Error("Client refuses to provide any TURN servers");
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});

// Start the poll loop, sending the widget the initial result
this.pollTurnServers(turnServers, value);
this.turnServers = turnServers;
} catch (e) {
console.error("error getting first TURN server results", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "TURN servers not available"},
});
}
}
}

private async handleUnwatchTurnServers(request: IUnwatchTurnServersRequest): Promise<void> {
if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) {
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Missing capability"},
});
} else if (!this.turnServers) {
// We weren't polling anyways, so this is a no-op
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
} else {
// Stop the generator, allowing it to clean up
await this.turnServers.return(undefined);
this.turnServers = null;
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
}
}

private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
if (this.isStopped) return;
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {
Expand All @@ -517,6 +589,10 @@ export class ClientWidgetApi extends EventEmitter {
return this.handleCapabilitiesRenegotiate(<IRenegotiateCapabilitiesActionRequest>ev.detail);
case WidgetApiFromWidgetAction.MSC2876ReadEvents:
return this.handleReadEvents(<IReadEventFromWidgetActionRequest>ev.detail);
case WidgetApiFromWidgetAction.WatchTurnServers:
return this.handleWatchTurnServers(<IWatchTurnServersRequest>ev.detail);
case WidgetApiFromWidgetAction.UnwatchTurnServers:
return this.handleUnwatchTurnServers(<IUnwatchTurnServersRequest>ev.detail);
default:
return this.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
error: {
Expand Down
50 changes: 50 additions & 0 deletions src/WidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { EventEmitter } from "events";
import { Capability } from "./interfaces/Capabilities";
import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./interfaces/IWidgetApiRequest";
import { IWidgetApiAcknowledgeResponseData } from "./interfaces/IWidgetApiResponse";
import { WidgetApiDirection } from "./interfaces/WidgetApiDirection";
import {
ISupportedVersionsActionRequest,
Expand Down Expand Up @@ -61,6 +62,7 @@ import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapab
import { INavigateActionRequestData } from "./interfaces/NavigateAction";
import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
import { IRoomEvent } from "./interfaces/IRoomEvent";
import { ITurnServer, IUpdateTurnServersRequest } from "./interfaces/TurnServerActions";
import { Symbols } from "./Symbols";

/**
Expand Down Expand Up @@ -88,6 +90,7 @@ export class WidgetApi extends EventEmitter {
private requestedCapabilities: Capability[] = [];
private approvedCapabilities: Capability[];
private cachedClientVersions: ApiVersion[];
private turnServerWatchers = 0;

/**
* Creates a new API handler for the given widget.
Expand Down Expand Up @@ -487,6 +490,53 @@ export class WidgetApi extends EventEmitter {
).then();
}

/**
* Starts watching for TURN servers, yielding an initial set of credentials as soon as possible,
* and thereafter yielding new credentials whenever the previous ones expire.
* @yields {ITurnServer} The TURN server URIs and credentials currently available to the widget.
*/
public async* getTurnServers(): AsyncGenerator<ITurnServer> {
let setTurnServer: (server: ITurnServer) => void;

const onUpdateTurnServers = async (ev: CustomEvent<IUpdateTurnServersRequest>) => {
ev.preventDefault();
setTurnServer(ev.detail.data);
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
};

// Start listening for updates before we even start watching, to catch
// TURN data that is sent immediately
this.on(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers);

// Only send the 'watch' action if we aren't already watching
if (this.turnServerWatchers === 0) {
try {
await this.transport.send<IWidgetApiRequestEmptyData>(WidgetApiFromWidgetAction.WatchTurnServers, {});
} catch (e) {
this.off(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers);
throw e;
}
}
this.turnServerWatchers++;

try {
// Watch for new data indefinitely (until this generator's return method is called)
while (true) {
yield await new Promise<ITurnServer>(resolve => setTurnServer = resolve);
}
} finally {
// The loop was broken by the caller - clean up
this.off(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers);

// Since sending the 'unwatch' action will end updates for all other
// consumers, only send it if we're the only consumer remaining
this.turnServerWatchers--;
if (this.turnServerWatchers === 0) {
await this.transport.send<IWidgetApiRequestEmptyData>(WidgetApiFromWidgetAction.UnwatchTurnServers, {});
}
}
}

/**
* Starts the communication channel. This should be done early to ensure
* that messages are not missed. Communication can only be stopped by the client.
Expand Down
12 changes: 11 additions & 1 deletion src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { Capability, IOpenIDCredentials, OpenIDRequestState, SimpleObservable, IRoomEvent } from "..";
import { Capability, IOpenIDCredentials, OpenIDRequestState, SimpleObservable, IRoomEvent, ITurnServer } from "..";

export interface ISendEventDetails {
roomId: string;
Expand Down Expand Up @@ -169,4 +169,14 @@ export abstract class WidgetDriver {
public navigate(uri: string): Promise<void> {
throw new Error("Navigation is not implemented");
}

/**
* Polls for TURN server data, yielding an initial set of credentials as soon as possible, and
* thereafter yielding new credentials whenever the previous ones expire. The widget API will
* have already verified that the widget has permission to access TURN servers.
* @yields {ITurnServer} The TURN server URIs and credentials currently available to the client.
*/
public getTurnServers(): AsyncGenerator<ITurnServer> {
throw new Error("TURN server support is not implemented");
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export * from "./interfaces/SendToDeviceAction";
export * from "./interfaces/ReadEventAction";
export * from "./interfaces/IRoomEvent";
export * from "./interfaces/NavigateAction";
export * from "./interfaces/TurnServerActions";

// Complex models
export * from "./models/WidgetEventCapability";
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/ApiVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum UnstableApiVersion {
MSC2974 = "org.matrix.msc2974",
MSC2876 = "org.matrix.msc2876",
MSC3819 = "org.matrix.msc3819",
MSC3846 = "town.robin.msc3846",
}

export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string;
Expand All @@ -41,4 +42,5 @@ export const CurrentApiVersions: ApiVersion[] = [
UnstableApiVersion.MSC2974,
UnstableApiVersion.MSC2876,
UnstableApiVersion.MSC3819,
UnstableApiVersion.MSC3846,
];
1 change: 1 addition & 0 deletions src/interfaces/Capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export enum MatrixCapabilities {
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC2931Navigate = "org.matrix.msc2931.navigate",
MSC3846TurnServers = "town.robin.msc3846.turn_servers",
}

export type Capability = MatrixCapabilities | string;
Expand Down
55 changes: 55 additions & 0 deletions src/interfaces/TurnServerActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { IWidgetApiRequest, IWidgetApiRequestData, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest";
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction";
import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponse } from "./IWidgetApiResponse";

export interface ITurnServer {
uris: string[];
username: string;
password: string;
}

export interface IWatchTurnServersRequest extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.WatchTurnServers;
data: IWidgetApiRequestEmptyData;
}

export interface IWatchTurnServersResponse extends IWidgetApiResponse {
response: IWidgetApiAcknowledgeResponseData;
}

export interface IUnwatchTurnServersRequest extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.UnwatchTurnServers;
data: IWidgetApiRequestEmptyData;
}

export interface IUnwatchTurnServersResponse extends IWidgetApiResponse {
response: IWidgetApiAcknowledgeResponseData;
}

export interface IUpdateTurnServersRequestData extends IWidgetApiRequestData, ITurnServer {
}

export interface IUpdateTurnServersRequest extends IWidgetApiRequest {
action: WidgetApiToWidgetAction.UpdateTurnServers;
data: IUpdateTurnServersRequestData;
}

export interface IUpdateTurnServersResponse extends IWidgetApiResponse {
response: IWidgetApiAcknowledgeResponseData;
}
3 changes: 3 additions & 0 deletions src/interfaces/WidgetApiAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum WidgetApiToWidgetAction {
ButtonClicked = "button_clicked",
SendEvent = "send_event",
SendToDevice = "send_to_device",
UpdateTurnServers = "update_turn_servers",
}

export enum WidgetApiFromWidgetAction {
Expand All @@ -39,6 +40,8 @@ export enum WidgetApiFromWidgetAction {
SetModalButtonEnabled = "set_button_enabled",
SendEvent = "send_event",
SendToDevice = "send_to_device",
WatchTurnServers = "watch_turn_servers",
UnwatchTurnServers = "unwatch_turn_servers",

/**
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
Expand Down
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"sourceMap": true,
"outDir": "./lib",
"declaration": true,
"types": []
"types": [],
"lib": [
"es2020",
"dom"
]
},
"include": [
"./src/**/*.ts"
Expand Down

0 comments on commit 4b97173

Please sign in to comment.