diff --git a/res/sw.js b/res/sw.js deleted file mode 100644 index 1fdf7324e17..00000000000 --- a/res/sw.js +++ /dev/null @@ -1 +0,0 @@ -self.addEventListener("fetch", () => {}); diff --git a/src/serviceworker/index.ts b/src/serviceworker/index.ts new file mode 100644 index 00000000000..7d7de1da4c4 --- /dev/null +++ b/src/serviceworker/index.ts @@ -0,0 +1,77 @@ +const serverSupportMap: { + [serverUrl: string]: { + supportsMSC3916: boolean, + cacheExpires: number, + }, +} = {}; + +const credentialStore: { + [serverUrl: string]: string, +} = {}; + +// We skipWaiting() to update the service worker more frequently, particularly in development environments. +// @ts-expect-error - service worker types are not available. See 'fetch' event handler. +skipWaiting(); + +self.addEventListener("message", (event) => { + if (event.data?.type !== "credentials") return; // ignore + credentialStore[event.data.homeserverUrl] = event.data.accessToken; + console.log(`[Service Worker] Updated access token for ${event.data.homeserverUrl} (accessToken? ${Boolean(event.data.accessToken)})`); +}); + +// @ts-expect-error - getting types to work for this is difficult, so we anticipate that "addEventListener" doesn't +// have a valid signature. +self.addEventListener("fetch", (event: FetchEvent) => { + // This is the authenticated media (MSC3916) check, proxying what was unauthenticated to the authenticated variants. + + if (event.request.method !== "GET") { + return; // not important to us + } + + // Note: ideally we'd keep the request headers and etc, but in practice we can't even see those details. + // See https://stackoverflow.com/a/59152482 + let url = event.request.url; + + // We only intercept v3 download and thumbnail requests as presumably everything else is deliberate. + // For example, `/_matrix/media/unstable` or `/_matrix/media/v3/preview_url` are something well within + // the control of the application, and appear to be choices made at a higher level than us. + if (url.includes("/_matrix/media/v3/download") || url.includes("/_matrix/media/v3/thumbnail")) { + // We need to call respondWith synchronously, otherwise we may never execute properly. This means + // later on we need to proxy the request through if it turns out the server doesn't support authentication. + event.respondWith((async (): Promise => { + // Figure out which homeserver we're communicating with + const csApi = url.substring(0, url.indexOf("/_matrix/media/v3")); + + // Locate our access token, and populate the fetchConfig with the authentication header. + const accessToken = credentialStore[csApi]; + let fetchConfig: {headers?: {[key: string]: string}} = {}; + if (accessToken) { + fetchConfig = { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }; + } + + // Update or populate the server support map using a (usually) authenticated `/versions` call. + if (!serverSupportMap[csApi] || serverSupportMap[csApi].cacheExpires <= (new Date()).getTime()) { + const versions = await (await fetch(`${csApi}/_matrix/client/versions`, fetchConfig)).json(); + serverSupportMap[csApi] = { + supportsMSC3916: Boolean(versions?.unstable_features?.["org.matrix.msc3916"]), + cacheExpires: (new Date()).getTime() + (2 * 60 * 60 * 1000), // 2 hours from now + }; + } + + // If we have server support (and a means of authentication), rewrite the URL to use MSC3916 endpoints. + if (serverSupportMap[csApi].supportsMSC3916 && accessToken) { + // Currently unstable only. + url = url.replace(/\/media\/v3\/(.*)\//, "/client/unstable/org.matrix.msc3916/media/$1/"); + } // else by default we make no changes + + // Add authentication and send the request. We add authentication even if MSC3916 endpoints aren't + // being used to ensure patches like this work: + // https://github.com/matrix-org/synapse/commit/2390b66bf0ec3ff5ffb0c7333f3c9b239eeb92bb + return fetch(url, fetchConfig); + })()); + } +}); diff --git a/src/vector/platform/WebPlatform.ts b/src/vector/platform/WebPlatform.ts index 39446158fda..1a734b3c431 100644 --- a/src/vector/platform/WebPlatform.ts +++ b/src/vector/platform/WebPlatform.ts @@ -46,7 +46,10 @@ export default class WebPlatform extends VectorBasePlatform { super(); // Register service worker if available on this platform if ("serviceWorker" in navigator) { - navigator.serviceWorker.register("sw.js"); + // sw.js is exported by webpack, sourced from `/src/serviceworker/index.ts` + navigator.serviceWorker.register("sw.js") + .then(r => r.update()) + .catch(e => console.error("Error registering/updating service worker:", e)); } } diff --git a/webpack.config.js b/webpack.config.js index 3b827c8d95d..9ea4d10317c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -153,6 +153,10 @@ module.exports = (env, argv) => { mobileguide: "./src/vector/mobile_guide/index.ts", jitsi: "./src/vector/jitsi/index.ts", usercontent: "./node_modules/matrix-react-sdk/src/usercontent/index.ts", + serviceworker: { + import: "./src/serviceworker/index.ts", + filename: "sw.js", // update WebPlatform if this changes + }, ...(useHMR ? {} : cssThemes), }, @@ -666,7 +670,7 @@ module.exports = (env, argv) => { // HtmlWebpackPlugin will screw up our formatting like the names // of the themes and which chunks we actually care about. inject: false, - excludeChunks: ["mobileguide", "usercontent", "jitsi"], + excludeChunks: ["mobileguide", "usercontent", "jitsi", "serviceworker"], minify: false, templateParameters: { og_image_url: ogImageUrl, @@ -739,7 +743,6 @@ module.exports = (env, argv) => { "res/jitsi_external_api.min.js", "res/jitsi_external_api.min.js.LICENSE.txt", "res/manifest.json", - "res/sw.js", "res/welcome.html", { from: "welcome/**", context: path.resolve(__dirname, "res") }, { from: "themes/**", context: path.resolve(__dirname, "res") },