diff --git a/apps/web/src/MatrixClientPeg.test.ts b/apps/web/src/MatrixClientPeg.test.ts index bceecdd0c58..4697a8aef82 100644 --- a/apps/web/src/MatrixClientPeg.test.ts +++ b/apps/web/src/MatrixClientPeg.test.ts @@ -15,6 +15,7 @@ import fetchMock from "@fetch-mock/vitest"; import { advanceDateAndTime, stubClient, createTestClient } from "test-utils"; import { type IMatrixClientPeg, MatrixClientPeg as peg } from "./MatrixClientPeg"; +import SdkConfig from "./SdkConfig"; vi.useFakeTimers(); @@ -116,5 +117,30 @@ describe("MatrixClientPeg", () => { await testPeg.start(); expect(mockInitRustCrypto).toHaveBeenCalledTimes(1); }); + + it("should poll the client well-known by default", async () => { + vi.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); + const startClient = vi.spyOn(testPeg.safeGet(), "startClient").mockResolvedValue(undefined); + + await testPeg.start(); + + const opts = startClient.mock.calls[0][0]; + expect(opts?.clientWellKnownPollPeriod).toBe(2 * 60 * 60); + }); + + it("should not poll the client well-known when enable_client_well_known_lookups is false", async () => { + const sdkConfigGet = SdkConfig.get; + vi.spyOn(SdkConfig, "get").mockImplementation((key?: any, altCaseName?: string): any => { + if (key === "enable_client_well_known_lookups") return false; + return sdkConfigGet(key, altCaseName); + }); + vi.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); + const startClient = vi.spyOn(testPeg.safeGet(), "startClient").mockResolvedValue(undefined); + + await testPeg.start(); + + const opts = startClient.mock.calls[0][0]; + expect(opts?.clientWellKnownPollPeriod).toBeUndefined(); + }); }); }); diff --git a/apps/web/src/MatrixClientPeg.ts b/apps/web/src/MatrixClientPeg.ts index 03d47b0a335..4d96a653b39 100644 --- a/apps/web/src/MatrixClientPeg.ts +++ b/apps/web/src/MatrixClientPeg.ts @@ -263,7 +263,11 @@ class MatrixClientPegClass implements IMatrixClientPeg { // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = PendingEventOrdering.Detached; opts.lazyLoadMembers = true; - opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours + // Poll the user's `/.well-known/matrix/client` for client config unless disabled by config. + // Leaving `clientWellKnownPollPeriod` unset stops the SDK fetching it. + if (SdkConfig.get("enable_client_well_known_lookups")) { + opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours + } opts.threadSupport = true; if (SettingsStore.getValue("feature_user_status")) { opts.unstableMSC4429SyncUserProfileFields = ["org.matrix.msc4426.status"]; diff --git a/apps/web/src/SdkConfig.ts b/apps/web/src/SdkConfig.ts index affa80c5bec..a52dd8eb66b 100644 --- a/apps/web/src/SdkConfig.ts +++ b/apps/web/src/SdkConfig.ts @@ -30,6 +30,7 @@ export const DEFAULTS = { integrations_rest_url: "https://scalar.vector.im/api", show_labs_settings: false, force_verification: false, + enable_client_well_known_lookups: true, jitsi: { preferred_domain: "meet.element.io", diff --git a/apps/web/src/stores/CallStore.ts b/apps/web/src/stores/CallStore.ts index 119a0f6d6e8..04ecf0aa4f6 100644 --- a/apps/web/src/stores/CallStore.ts +++ b/apps/web/src/stores/CallStore.ts @@ -16,6 +16,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import WidgetStore from "./WidgetStore"; import SettingsStore from "../settings/SettingsStore"; import { SettingLevel } from "../settings/SettingLevel"; +import SdkConfig from "../SdkConfig"; import { Call, CallEvent, ConnectionState } from "../models/Call"; export enum CallStoreEvent { @@ -71,10 +72,13 @@ export class CallStore extends AsyncStoreWithClient { } // See https://github.com/matrix-org/matrix-spec-proposals/blob/d61969a9a3696b6c54d7987b1643b5bc03670927/proposals/4143-matrix-rtc.md#discovery-of-foci-using-well-knownmatrixclient // This well-known option has since been removed from the spec but is still widely deployed. - await this.matrixClient.waitForClientWellKnown(); - const foci = this.matrixClient.getClientWellKnown()?.["org.matrix.msc4143.rtc_foci"]; - if (Array.isArray(foci)) { - foci.forEach((foci) => this.configuredMatrixRTCTransports.add(foci)); + // Reading it can be disabled via config; the modern endpoint above is unaffected. + if (SdkConfig.get("enable_client_well_known_lookups")) { + await this.matrixClient.waitForClientWellKnown(); + const foci = this.matrixClient.getClientWellKnown()?.["org.matrix.msc4143.rtc_foci"]; + if (Array.isArray(foci)) { + foci.forEach((foci) => this.configuredMatrixRTCTransports.add(foci)); + } } this.emit(CallStoreEvent.TransportsUpdated); } diff --git a/apps/web/test/unit-tests/stores/CallStore-test.ts b/apps/web/test/unit-tests/stores/CallStore-test.ts index 847c4b35929..2861dc7aa85 100644 --- a/apps/web/test/unit-tests/stores/CallStore-test.ts +++ b/apps/web/test/unit-tests/stores/CallStore-test.ts @@ -11,6 +11,7 @@ import { type MockedObject } from "jest-mock"; import { ElementCall } from "../../../src/models/Call"; import { CallStore } from "../../../src/stores/CallStore"; +import SdkConfig from "../../../src/SdkConfig"; import { setUpClientRoomAndStores, cleanUpClientRoomAndStores, @@ -66,4 +67,20 @@ describe("CallStore", () => { { type: "type-d", other_data: "baz" }, ]); }); + it("does not fall back to client well-known when enable_client_well_known_lookups is false", async () => { + const sdkConfigGet = SdkConfig.get; + jest.spyOn(SdkConfig, "get").mockImplementation((key?: any, altCaseName?: string): any => { + if (key === "enable_client_well_known_lookups") return false; + return sdkConfigGet(key, altCaseName); + }); + client._unstable_getRTCTransports.mockResolvedValue([{ type: "type-a", some_data: "value" }]); + client.getClientWellKnown.mockReturnValue({ + "org.matrix.msc4143.rtc_foci": [{ type: "type-c", other_data: "bar" }], + }); + await setupAsyncStoreWithClient(CallStore.instance, client); + // Only the modern endpoint contributes; the legacy well-known fallback is skipped entirely. + expect(CallStore.instance.getConfiguredRTCTransports()).toEqual([{ type: "type-a", some_data: "value" }]); + expect(client.waitForClientWellKnown).not.toHaveBeenCalled(); + expect(client.getClientWellKnown).not.toHaveBeenCalled(); + }); }); diff --git a/docs/config.md b/docs/config.md index 5a51b446ab2..7b83e649d6a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -57,6 +57,12 @@ If both `default_server_config` and `default_server_name` are used, Element will information using `.well-known`, and if that fails, take `default_server_config` as the homeserver connection information. +1. `enable_client_well_known_lookups`: Controls whether Element makes runtime requests to the logged-in user's + [`/.well-known/matrix/...`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient) + endpoints (for example, polling for client configuration after login). Set it to `false` to stop Element making these + requests, so it only contacts the homeserver base URL; the `well_known` object returned inline in the `/login` response + is unaffected. Defaults to `true`. + ## Labs flags Labs flags are optional, typically beta or in-development, features that can be turned on or off. The full range of diff --git a/packages/shared-types/lib/config.json.d.ts b/packages/shared-types/lib/config.json.d.ts index 283f031be0e..a05dd19a9c7 100644 --- a/packages/shared-types/lib/config.json.d.ts +++ b/packages/shared-types/lib/config.json.d.ts @@ -37,6 +37,14 @@ export interface WebConfigJson { disable_login_language_selector?: boolean; disable_3pid_login?: boolean; + /** + * Whether the app may make runtime requests to the user's `/.well-known/matrix/...` + * endpoints (such as the post-login client well-known poll). Set to false to disable these, so the app + * only contacts the homeserver base URL. Does not affect the `well_known` returned inline in the + * `/login` response. Defaults to true. + */ + enable_client_well_known_lookups?: boolean; + brand?: string; branding?: { welcome_background_url?: string | string[]; // chosen at random if array