mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-02 04:05:56 +00:00
274 lines
8.1 KiB
TypeScript
274 lines
8.1 KiB
TypeScript
/*
|
|
Copyright 2023, 2024 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import {
|
|
retryNetworkOperation,
|
|
type IOpenIDToken,
|
|
type MatrixClient,
|
|
} from "matrix-js-sdk";
|
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
|
|
|
import { FailToGetOpenIdToken } from "../utils/errors";
|
|
import { doNetworkOperationWithRetry } from "../utils/matrix";
|
|
import { Config } from "../config/Config";
|
|
|
|
/**
|
|
* Configuration and access tokens provided by the SFU on successful authentication.
|
|
*/
|
|
export interface SFUConfig {
|
|
url: string;
|
|
jwt: string;
|
|
livekitAlias: string;
|
|
// NOTE: Currently unused.
|
|
livekitIdentity: string;
|
|
}
|
|
|
|
/**
|
|
* Decoded details from the JWT.
|
|
*/
|
|
interface SFUJWTPayload {
|
|
/**
|
|
* Expiration time for the JWT.
|
|
* Note: This value is in seconds since Unix epoch.
|
|
*/
|
|
exp: number;
|
|
/**
|
|
* Name of the instance which authored the JWT
|
|
*/
|
|
iss: string;
|
|
/**
|
|
* Time at which the JWT can start to be used.
|
|
* Note: This value is in seconds since Unix epoch.
|
|
*/
|
|
nbf: number;
|
|
/**
|
|
* Subject. The Livekit alias in this context.
|
|
*/
|
|
sub: string;
|
|
/**
|
|
* The set of permissions for the user.
|
|
*/
|
|
video: {
|
|
canPublish: boolean;
|
|
canSubscribe: boolean;
|
|
room: string;
|
|
roomJoin: boolean;
|
|
};
|
|
}
|
|
|
|
// The bits we need from MatrixClient
|
|
export type OpenIDClientParts = Pick<
|
|
MatrixClient,
|
|
"getOpenIdToken" | "getDeviceId"
|
|
>;
|
|
/**
|
|
* Gets a bearer token from the homeserver and then use it to authenticate
|
|
* to the matrix RTC backend in order to get acces to the SFU.
|
|
* It has built-in retry for calls to the homeserver with a backoff policy.
|
|
* @param client The Matrix client
|
|
* @param membership Our own membership identity parts used to send to jwt service.
|
|
* @param serviceUrl The URL of the livekit SFU service
|
|
* @param forceOldJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination
|
|
* instead of a hash.
|
|
* This function by default uses whatever is possible with the current jwt service installed next to the SFU.
|
|
* For remote connections this does not matter, since we will not publish there we can rely on the newest option.
|
|
* For our own connection we can only use the hashed version if we also send the new matrix2.0 sticky events.
|
|
* @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases.
|
|
* @param delayEndpointBaseUrl The URL of the matrix homeserver.
|
|
* @param delayId The delay id used for the jwt service to manage.
|
|
* @param logger optional logger.
|
|
* @returns Object containing the token information
|
|
* @throws FailToGetOpenIdToken
|
|
*/
|
|
export async function getSFUConfigWithOpenID(
|
|
client: OpenIDClientParts,
|
|
membership: CallMembershipIdentityParts,
|
|
serviceUrl: string,
|
|
forceOldJwtEndpoint: boolean,
|
|
roomId: string,
|
|
delayEndpointBaseUrl?: string,
|
|
delayId?: string,
|
|
logger?: Logger,
|
|
): Promise<SFUConfig> {
|
|
let openIdToken: IOpenIDToken;
|
|
try {
|
|
openIdToken = await doNetworkOperationWithRetry(async () =>
|
|
client.getOpenIdToken(),
|
|
);
|
|
} catch (error) {
|
|
throw new FailToGetOpenIdToken(
|
|
error instanceof Error ? error : new Error("Unknown error"),
|
|
);
|
|
}
|
|
logger?.debug("Got openID token", openIdToken);
|
|
|
|
logger?.info(`Trying to get JWT for focus ${serviceUrl}...`);
|
|
|
|
let sfuConfig: { url: string; jwt: string } | undefined;
|
|
|
|
// If forceOldJwtEndpoint is set we indicate that we do not want to try the new endpoint,
|
|
// since we are not sending the new matrix2.0 sticky events (no hashed identity in the event)
|
|
if (forceOldJwtEndpoint === false) {
|
|
try {
|
|
sfuConfig = await getLiveKitJWTWithDelayDelegation(
|
|
membership,
|
|
serviceUrl,
|
|
roomId,
|
|
openIdToken,
|
|
delayEndpointBaseUrl,
|
|
delayId,
|
|
);
|
|
logger?.info(`Got JWT from call's active focus URL.`);
|
|
} catch (e) {
|
|
if (e instanceof NotSupportedError) {
|
|
logger?.warn(
|
|
`Failed fetching jwt with matrix 2.0 endpoint (retry with legacy) Not supported`,
|
|
e,
|
|
);
|
|
sfuConfig = undefined;
|
|
} else {
|
|
logger?.warn(
|
|
`Failed fetching jwt with matrix 2.0 endpoint other issues -> not going to try with legacy endpoint`,
|
|
e,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// DEPRECATED
|
|
// Either forceOldJwtEndpoint = true or getLiveKitJWTWithDelayDelegation throws -> reset sfuConfig = undefined
|
|
if (sfuConfig === undefined) {
|
|
sfuConfig = await getLiveKitJWT(
|
|
membership.deviceId,
|
|
serviceUrl,
|
|
roomId,
|
|
openIdToken,
|
|
);
|
|
|
|
logger?.info(`Got JWT from call's active focus URL.`);
|
|
}
|
|
|
|
if (!sfuConfig) {
|
|
throw new Error("No `sfuConfig` after trying with old and new endpoints");
|
|
}
|
|
|
|
// Pull the details from the JWT
|
|
const [, payloadStr] = sfuConfig.jwt.split(".");
|
|
// TODO: Prefer Uint8Array.fromBase64 when widely available
|
|
const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload;
|
|
return {
|
|
jwt: sfuConfig.jwt,
|
|
url: sfuConfig.url,
|
|
livekitAlias: payload.video.room,
|
|
// NOTE: Currently unused.
|
|
// Probably also not helpful since we now compute the backendIdentity on joining the call so we can use it for the encryption manager.
|
|
// The only reason for us to know it locally is to connect the right users with the lk world. (and to set our own keys)
|
|
livekitIdentity: payload.sub,
|
|
};
|
|
}
|
|
const RETRIES = 4;
|
|
async function getLiveKitJWT(
|
|
deviceId: string,
|
|
livekitServiceURL: string,
|
|
matrixRoomId: string,
|
|
openIDToken: IOpenIDToken,
|
|
): Promise<{ url: string; jwt: string }> {
|
|
let res: Response | undefined;
|
|
await retryNetworkOperation(RETRIES, async () => {
|
|
res = await fetch(livekitServiceURL + "/sfu/get", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
// This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used.
|
|
room: matrixRoomId,
|
|
openid_token: openIDToken,
|
|
device_id: deviceId,
|
|
}),
|
|
});
|
|
});
|
|
if (!res) {
|
|
throw new Error(
|
|
`Network error while connecting to jwt service after ${RETRIES} retries`,
|
|
);
|
|
}
|
|
if (!res.ok) {
|
|
throw new Error("SFU Config fetch failed with status code " + res.status);
|
|
}
|
|
return await res.json();
|
|
}
|
|
|
|
class NotSupportedError extends Error {
|
|
public constructor(message: string) {
|
|
super(message);
|
|
this.name = "NotSupported";
|
|
}
|
|
}
|
|
|
|
export async function getLiveKitJWTWithDelayDelegation(
|
|
membership: CallMembershipIdentityParts,
|
|
livekitServiceURL: string,
|
|
matrixRoomId: string,
|
|
openIDToken: IOpenIDToken,
|
|
delayEndpointBaseUrl?: string,
|
|
delayId?: string,
|
|
): Promise<{ url: string; jwt: string }> {
|
|
const { userId, deviceId, memberId } = membership;
|
|
|
|
const body = {
|
|
room_id: matrixRoomId,
|
|
slot_id: "m.call#ROOM",
|
|
openid_token: openIDToken,
|
|
member: {
|
|
id: memberId,
|
|
claimed_user_id: userId,
|
|
claimed_device_id: deviceId,
|
|
},
|
|
};
|
|
|
|
let bodyDalayParts = {};
|
|
// Also check for empty string
|
|
if (delayId && delayEndpointBaseUrl) {
|
|
const delayTimeoutMs =
|
|
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms ?? 1000;
|
|
bodyDalayParts = {
|
|
delay_id: delayId,
|
|
delay_timeout: delayTimeoutMs,
|
|
delay_cs_api_url: delayEndpointBaseUrl,
|
|
};
|
|
}
|
|
|
|
let res: Response | undefined;
|
|
|
|
await retryNetworkOperation(RETRIES, async () => {
|
|
res = await fetch(livekitServiceURL + "/get_token", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ ...body, ...bodyDalayParts }),
|
|
});
|
|
});
|
|
|
|
if (!res) {
|
|
throw new Error(
|
|
`Network error while connecting to jwt service after ${RETRIES} retries`,
|
|
);
|
|
}
|
|
if (!res.ok) {
|
|
const msg = "SFU Config fetch failed with status code " + res.status;
|
|
if (res.status === 404) {
|
|
throw new NotSupportedError(msg);
|
|
} else {
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
return await res.json();
|
|
}
|