Update makeTransport to fetch backend transports and validate all transports before response.

This commit is contained in:
Will Hunt
2025-12-11 13:54:10 +00:00
parent 9bd85c33e2
commit 8bc7b53534
2 changed files with 98 additions and 41 deletions

View File

@@ -32,6 +32,7 @@ describe("LocalTransport", () => {
memberships$: constant(new Epoch<CallMembership[]>([])), memberships$: constant(new Epoch<CallMembership[]>([])),
client: { client: {
getDomain: () => "", getDomain: () => "",
_unstable_getRTCTransports: async () => [],
// These won't be called in this error path but satisfy the type // These won't be called in this error path but satisfy the type
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
@@ -67,6 +68,7 @@ describe("LocalTransport", () => {
client: { client: {
// Use empty domain to skip .well-known and use config directly // Use empty domain to skip .well-known and use config directly
getDomain: () => "", getDomain: () => "",
_unstable_getRTCTransports: async () => [],
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
}, },
@@ -103,6 +105,7 @@ describe("LocalTransport", () => {
memberships$: constant(new Epoch<CallMembership[]>([])), memberships$: constant(new Epoch<CallMembership[]>([])),
client: { client: {
getDomain: () => "", getDomain: () => "",
_unstable_getRTCTransports: async () => [],
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
}, },
@@ -138,6 +141,7 @@ describe("LocalTransport", () => {
memberships$, memberships$,
client: { client: {
getDomain: () => "", getDomain: () => "",
_unstable_getRTCTransports: async () => [],
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
}, },

View File

@@ -8,11 +8,11 @@ Please see LICENSE in the repository root for full details.
import { import {
type CallMembership, type CallMembership,
isLivekitTransport, isLivekitTransport,
type LivekitTransportConfig,
type LivekitTransport, type LivekitTransport,
isLivekitTransportConfig, isLivekitTransportConfig,
type Transport,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { type MatrixClient } from "matrix-js-sdk"; import { MatrixError, type MatrixClient } from "matrix-js-sdk";
import { import {
combineLatest, combineLatest,
distinctUntilChanged, distinctUntilChanged,
@@ -45,7 +45,8 @@ const logger = rootLogger.getChild("[LocalTransport]");
interface Props { interface Props {
scope: ObservableScope; scope: ObservableScope;
memberships$: Behavior<Epoch<CallMembership[]>>; memberships$: Behavior<Epoch<CallMembership[]>>;
client: Pick<MatrixClient, "getDomain"> & OpenIDClientParts; client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports"> &
OpenIDClientParts;
roomId: string; roomId: string;
useOldestMember$: Behavior<boolean>; useOldestMember$: Behavior<boolean>;
} }
@@ -116,73 +117,125 @@ export const createLocalTransport$ = ({
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci"; const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
/** /**
* Determine the correct Transport for the current session, including
* validating auth against the service to ensure it's correct.
* Prefers in order:
* *
* @param client * 1. The transports returned via the homeserver.
* @param roomId * 2. The transports returned via .well-known.
* @returns * 3. The transport configured in Element Call's config.
*
* @param client The authenticated Matrix client for the current user
* @param roomId The ID of the room to be connected to.
* @param urlFromDevSettings Override URL provided by the user's local config.
* @returns A fully validated transport config.
* @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken
*/ */
async function makeTransport( async function makeTransport(
client: Pick<MatrixClient, "getDomain"> & OpenIDClientParts, client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports"> &
OpenIDClientParts,
roomId: string, roomId: string,
urlFromDevSettings: string | null, urlFromDevSettings: string | null,
): Promise<LivekitTransport> { ): Promise<LivekitTransport> {
let transport: LivekitTransport | undefined;
logger.trace("Searching for a preferred transport"); logger.trace("Searching for a preferred transport");
//TODO refactor this to use the jwt service returned alias.
const livekitAlias = roomId; const livekitAlias = roomId;
// DEVTOOL: Highest priority: Load from devtool setting // DEVTOOL: Highest priority: Load from devtool setting
if (urlFromDevSettings !== null) { if (urlFromDevSettings !== null) {
const transportFromStorage: LivekitTransport = { logger.info("Using LiveKit transport from dev tools: ", urlFromDevSettings);
// Validate that the SFU is up. Otherwise, we want to fail on this
// as we don't permit other SFUs.
await getSFUConfigWithOpenID(client, urlFromDevSettings, livekitAlias);
return {
type: "livekit", type: "livekit",
livekit_service_url: urlFromDevSettings, livekit_service_url: urlFromDevSettings,
livekit_alias: livekitAlias, livekit_alias: livekitAlias,
}; };
logger.info(
"Using LiveKit transport from dev tools: ",
transportFromStorage,
);
transport = transportFromStorage;
} }
// WELL_KNOWN: Prioritize the .well-known/matrix/client, if available, over the configured SFU async function getFirstUsableTransport(
transports: Transport[],
): Promise<LivekitTransport | null> {
for (const potentialTransport of transports) {
if (isLivekitTransportConfig(potentialTransport)) {
try {
await getSFUConfigWithOpenID(
client,
potentialTransport.livekit_service_url,
livekitAlias,
);
return {
...potentialTransport,
livekit_alias: livekitAlias,
};
} catch (ex) {
logger.debug(
`Could not use SFU service "${potentialTransport.livekit_service_url}" as SFU`,
ex,
);
}
}
}
return null;
}
// MSC4143: Attempt to fetch transports from backend.
if ("_unstable_getRTCTransports" in client) {
try {
const selectedTransport = await getFirstUsableTransport(
await client._unstable_getRTCTransports(),
);
if (selectedTransport) {
logger.info("Using backend-configured SFU", selectedTransport);
return selectedTransport;
}
} catch (ex) {
if (ex instanceof MatrixError && ex.httpStatus === 404) {
// Expected, this is an unstable endpoint and it's not required.
logger.debug("Backend does not provide any RTC transports", ex);
} else {
// We got an error that wasn't just missing support for the feature, so log it loudly.
logger.error(
"Unexpected error fetching RTC transports from backend",
ex,
);
}
}
}
// Legacy MSC4143 (to be removed) WELL_KNOWN: Prioritize the .well-known/matrix/client, if available.
const domain = client.getDomain(); const domain = client.getDomain();
if (domain && transport === undefined) { if (domain) {
// we use AutoDiscovery instead of relying on the MatrixClient having already // we use AutoDiscovery instead of relying on the MatrixClient having already
// been fully configured and started // been fully configured and started
const wellKnownFoci = (await AutoDiscovery.getRawClientConfig(domain))?.[ const wellKnownFoci = (await AutoDiscovery.getRawClientConfig(domain))?.[
FOCI_WK_KEY FOCI_WK_KEY
]; ];
if (Array.isArray(wellKnownFoci)) { const selectedTransport = Array.isArray(wellKnownFoci)
const wellKnownTransport: LivekitTransportConfig | undefined = ? await getFirstUsableTransport(wellKnownFoci)
wellKnownFoci.find((f) => f && isLivekitTransportConfig(f)); : null;
if (wellKnownTransport !== undefined) { if (selectedTransport) {
logger.info("Using LiveKit transport from .well-known: ", transport); logger.info("Using .well-known SFU", selectedTransport);
transport = { ...wellKnownTransport, livekit_alias: livekitAlias }; return selectedTransport;
}
} }
} }
// CONFIG: Least prioritized; Load from config file // CONFIG: Least prioritized; Load from config file
const urlFromConf = Config.get().livekit?.livekit_service_url; const urlFromConf = Config.get().livekit?.livekit_service_url;
if (urlFromConf && transport === undefined) { if (urlFromConf) {
const transportFromConf: LivekitTransport = { try {
type: "livekit", await getSFUConfigWithOpenID(client, urlFromConf, roomId);
livekit_service_url: urlFromConf, const selectedTransport: LivekitTransport = {
livekit_alias: livekitAlias, type: "livekit",
}; livekit_service_url: urlFromConf,
logger.info("Using LiveKit transport from config: ", transportFromConf); livekit_alias: livekitAlias,
transport = transportFromConf; };
logger.info("Using config SFU", selectedTransport);
return selectedTransport;
} catch (ex) {
logger.error("Failed to validate config SFU", ex);
}
} }
if (!transport) throw new MatrixRTCTransportMissingError(domain ?? ""); // this will call the jwt/sfu/get endpoint to pre create the livekit room. throw new MatrixRTCTransportMissingError(domain ?? "");
await getSFUConfigWithOpenID(
client,
transport.livekit_service_url,
transport.livekit_alias,
);
return transport;
} }