2023-07-05 13:12:37 +01:00
|
|
|
/*
|
2024-09-06 10:22:13 +02:00
|
|
|
Copyright 2023, 2024 New Vector Ltd.
|
2023-07-05 13:12:37 +01:00
|
|
|
|
2025-02-18 17:59:58 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
2024-09-06 10:22:13 +02:00
|
|
|
Please see LICENSE in the repository root for full details.
|
2023-07-05 13:12:37 +01:00
|
|
|
*/
|
|
|
|
|
|
2025-03-13 13:58:43 +01:00
|
|
|
import { type IOpenIDToken, type MatrixClient } from "matrix-js-sdk";
|
2025-12-17 09:53:49 +01:00
|
|
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
2025-12-29 17:38:54 +01:00
|
|
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
2023-07-05 13:12:37 +01:00
|
|
|
|
2025-03-21 15:07:15 -04:00
|
|
|
import { FailToGetOpenIdToken } from "../utils/errors";
|
|
|
|
|
import { doNetworkOperationWithRetry } from "../utils/matrix";
|
2025-12-17 09:53:49 +01:00
|
|
|
import { Config } from "../config/Config";
|
2023-07-12 17:57:54 +01:00
|
|
|
|
2025-12-29 17:45:41 +00:00
|
|
|
/**
|
|
|
|
|
* Configuration and access tokens provided by the SFU on successful authentication.
|
|
|
|
|
*/
|
2023-07-05 13:12:37 +01:00
|
|
|
export interface SFUConfig {
|
|
|
|
|
url: string;
|
|
|
|
|
jwt: string;
|
2025-12-29 17:45:41 +00:00
|
|
|
livekitAlias: string;
|
|
|
|
|
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;
|
|
|
|
|
};
|
2023-07-05 13:12:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The bits we need from MatrixClient
|
|
|
|
|
export type OpenIDClientParts = Pick<
|
|
|
|
|
MatrixClient,
|
|
|
|
|
"getOpenIdToken" | "getDeviceId"
|
|
|
|
|
>;
|
2025-11-20 14:42:12 +01:00
|
|
|
/**
|
2025-11-21 13:04:28 +01:00
|
|
|
* 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.
|
2025-12-30 17:02:44 +01:00
|
|
|
* @param client The Matrix client
|
2025-12-29 17:38:54 +01:00
|
|
|
* @param membership
|
2025-12-30 17:02:44 +01:00
|
|
|
* @param serviceUrl The URL of the livekit SFU service
|
2025-12-29 17:38:54 +01:00
|
|
|
* @param forceOldEndpoint 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.
|
2026-01-05 21:08:21 +01:00
|
|
|
* @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.
|
2025-12-29 17:38:54 +01:00
|
|
|
* @param delayEndpointBaseUrl
|
|
|
|
|
* @param delayId
|
|
|
|
|
* @param logger
|
2025-11-21 13:04:28 +01:00
|
|
|
* @returns Object containing the token information
|
2025-11-20 14:42:12 +01:00
|
|
|
* @throws FailToGetOpenIdToken
|
|
|
|
|
*/
|
2023-07-05 13:12:37 +01:00
|
|
|
export async function getSFUConfigWithOpenID(
|
|
|
|
|
client: OpenIDClientParts,
|
2025-12-17 09:53:49 +01:00
|
|
|
membership: CallMembershipIdentityParts,
|
2025-08-27 14:01:01 +02:00
|
|
|
serviceUrl: string,
|
2025-12-29 17:38:54 +01:00
|
|
|
forceOldJwtEndpoint: boolean,
|
2026-01-05 21:08:21 +01:00
|
|
|
roomId: string,
|
2025-12-17 09:53:49 +01:00
|
|
|
delayEndpointBaseUrl?: string,
|
|
|
|
|
delayId?: string,
|
2025-12-29 17:38:54 +01:00
|
|
|
logger?: Logger,
|
2025-08-27 14:01:01 +02:00
|
|
|
): Promise<SFUConfig> {
|
2025-03-11 09:07:19 +01:00
|
|
|
let openIdToken: IOpenIDToken;
|
|
|
|
|
try {
|
|
|
|
|
openIdToken = await doNetworkOperationWithRetry(async () =>
|
|
|
|
|
client.getOpenIdToken(),
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
throw new FailToGetOpenIdToken(
|
|
|
|
|
error instanceof Error ? error : new Error("Unknown error"),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-29 17:38:54 +01:00
|
|
|
logger?.debug("Got openID token", openIdToken);
|
2023-07-05 13:12:37 +01:00
|
|
|
|
2025-12-29 17:38:54 +01:00
|
|
|
logger?.info(`Trying to get JWT for focus ${serviceUrl}...`);
|
2025-12-17 09:53:49 +01:00
|
|
|
const args: [CallMembershipIdentityParts, string, string, IOpenIDToken] = [
|
|
|
|
|
membership,
|
2025-08-27 14:01:01 +02:00
|
|
|
serviceUrl,
|
2026-01-05 21:08:21 +01:00
|
|
|
roomId,
|
2025-08-27 14:01:01 +02:00
|
|
|
openIdToken,
|
2025-12-17 09:53:49 +01:00
|
|
|
];
|
2026-01-05 21:08:21 +01:00
|
|
|
|
|
|
|
|
let sfuConfig: { url: string; jwt: string };
|
2025-12-29 17:38:54 +01:00
|
|
|
try {
|
|
|
|
|
// we do not want to try the old endpoint, since we are not sending the new matrix2.0 sticky events (no hashed identity in the event)
|
|
|
|
|
if (forceOldJwtEndpoint) throw new Error("Force old jwt endpoint");
|
|
|
|
|
if (!delayId)
|
|
|
|
|
throw new Error("No delayId, Won't try matrix 2.0 jwt endpoint.");
|
|
|
|
|
|
2026-01-05 21:08:21 +01:00
|
|
|
sfuConfig = await getLiveKitJWTWithDelayDelegation(
|
2025-12-17 09:53:49 +01:00
|
|
|
...args,
|
|
|
|
|
delayEndpointBaseUrl,
|
|
|
|
|
delayId,
|
|
|
|
|
);
|
2025-12-29 17:38:54 +01:00
|
|
|
logger?.info(`Got JWT from call's active focus URL.`);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger?.warn(
|
|
|
|
|
`Failed fetching jwt with matrix 2.0 endpoint (retry with legacy)`,
|
|
|
|
|
e,
|
|
|
|
|
);
|
2026-01-05 21:08:21 +01:00
|
|
|
sfuConfig = await getLiveKitJWT(...args);
|
2025-12-29 17:38:54 +01:00
|
|
|
logger?.info(`Got JWT from call's active focus URL.`);
|
2026-01-05 21:08:21 +01:00
|
|
|
} // Pull the details from the JWT
|
2025-12-29 17:45:41 +00:00
|
|
|
const [, payloadStr] = sfuConfig.jwt.split(".");
|
2026-01-05 21:08:21 +01:00
|
|
|
|
2025-12-29 17:45:41 +00:00
|
|
|
const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload;
|
|
|
|
|
return {
|
|
|
|
|
jwt: sfuConfig.jwt,
|
|
|
|
|
url: sfuConfig.url,
|
|
|
|
|
livekitAlias: payload.video.room,
|
|
|
|
|
// NOTE: Currently unused.
|
|
|
|
|
livekitIdentity: payload.sub,
|
|
|
|
|
};
|
2023-07-12 17:57:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getLiveKitJWT(
|
2025-12-17 09:53:49 +01:00
|
|
|
membership: CallMembershipIdentityParts,
|
2023-07-12 17:57:54 +01:00
|
|
|
livekitServiceURL: string,
|
2026-01-05 21:08:21 +01:00
|
|
|
matrixRoomId: string,
|
2023-10-11 10:42:04 -04:00
|
|
|
openIDToken: IOpenIDToken,
|
2025-12-29 17:45:41 +00:00
|
|
|
): Promise<{ url: string; jwt: string }> {
|
2023-07-12 17:57:54 +01:00
|
|
|
try {
|
|
|
|
|
const res = await fetch(livekitServiceURL + "/sfu/get", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
2026-01-05 21:08:21 +01:00
|
|
|
// This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used.
|
|
|
|
|
room: matrixRoomId,
|
2023-07-12 17:57:54 +01:00
|
|
|
openid_token: openIDToken,
|
2025-12-17 09:53:49 +01:00
|
|
|
device_id: membership.deviceId,
|
2023-07-12 17:57:54 +01:00
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error("SFU Config fetch failed with status code " + res.status);
|
|
|
|
|
}
|
|
|
|
|
return await res.json();
|
|
|
|
|
} catch (e) {
|
2025-12-29 17:45:41 +00:00
|
|
|
throw new Error("SFU Config fetch failed with exception", { cause: e });
|
2023-07-05 13:12:37 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 09:53:49 +01:00
|
|
|
|
|
|
|
|
export async function getLiveKitJWTWithDelayDelegation(
|
|
|
|
|
membership: CallMembershipIdentityParts,
|
|
|
|
|
livekitServiceURL: string,
|
2026-01-05 21:08:21 +01:00
|
|
|
matrixRoomId: string,
|
2025-12-17 09:53:49 +01:00
|
|
|
openIDToken: IOpenIDToken,
|
|
|
|
|
delayEndpointBaseUrl?: string,
|
|
|
|
|
delayId?: string,
|
2026-01-05 21:08:21 +01:00
|
|
|
): Promise<{ url: string; jwt: string }> {
|
2025-12-17 09:53:49 +01:00
|
|
|
const { userId, deviceId, memberId } = membership;
|
|
|
|
|
|
|
|
|
|
const body = {
|
2026-01-05 21:08:21 +01:00
|
|
|
room_id: matrixRoomId,
|
2025-12-17 09:53:49 +01:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(livekitServiceURL + "/get_token", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ ...body, ...bodyDalayParts }),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error("SFU Config fetch failed with status code " + res.status);
|
|
|
|
|
}
|
|
|
|
|
return await res.json();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new Error("SFU Config fetch failed with exception " + e);
|
|
|
|
|
}
|
|
|
|
|
}
|