From 6fca6800852321d45ed042d34d4db98c78b2b9dd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Aug 2024 09:44:18 +0100 Subject: [PATCH] Refactor media auth redirects to not kick in if the user is not logged in (#1817) --- src/electron-main.ts | 48 ++------------------------ src/media-auth.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 45 deletions(-) create mode 100644 src/media-auth.ts diff --git a/src/electron-main.ts b/src/electron-main.ts index b5a7edd..450bc61 100644 --- a/src/electron-main.ts +++ b/src/electron-main.ts @@ -19,7 +19,7 @@ limitations under the License. // Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc. import "./squirrelhooks"; -import { app, BrowserWindow, Menu, autoUpdater, protocol, dialog, Input, Event, session, ipcMain } from "electron"; +import { app, BrowserWindow, Menu, autoUpdater, protocol, dialog, Input, Event, session } from "electron"; import * as Sentry from "@sentry/electron/main"; import AutoLaunch from "auto-launch"; import path from "path"; @@ -42,6 +42,7 @@ import { _t, AppLocalization } from "./language-helper"; import { setDisplayMediaCallback } from "./displayMediaCallback"; import { setupMacosTitleBar } from "./macos-titlebar"; import { loadJsonFile } from "./utils"; +import { setupMediaAuth } from "./media-auth"; const argv = minimist(process.argv, { alias: { help: "h" }, @@ -550,50 +551,7 @@ app.on("ready", async () => { setDisplayMediaCallback(callback); }); - session.defaultSession.webRequest.onBeforeRequest((req, callback) => { - // This handler emulates the element-web service worker, where URLs are rewritten late in the request - // for backwards compatibility. As authenticated media becomes more prevalent, this should be replaced - // by the app using authenticated URLs from the outset. - let url = req.url; - if (!url.includes("/_matrix/media/v3/download") && !url.includes("/_matrix/media/v3/thumbnail")) { - return callback({}); // not a URL we care about - } - - // Check for feature support from the server. This requires asking the renderer process for supported - // versions. - ipcMain.once("serverSupportedVersions", (_, versionsResponse) => { - if (versionsResponse?.versions?.includes("v1.11")) { - url = url.replace(/\/media\/v3\/(.*)\//, "/client/v1/media/$1/"); - return callback({ redirectURL: url }); - } else { - return callback({}); // no support == no modification - } - }); - global.mainWindow!.webContents.send("serverSupportedVersions"); // ping now that the listener exists - - // we don't invoke callback() in this function - see the ipcMain.once above for callback usage. - }); - - session.defaultSession.webRequest.onBeforeSendHeaders((req, callback) => { - if (!req.url.includes("/_matrix/client/v1/media")) { - return callback({}); // invoke unmodified - } - - // Only add authorization header to authenticated media URLs. This emulates the service worker - // behaviour in element-web. - - // We need to get the access token from the renderer process to do that, though. - ipcMain.once("userAccessToken", (_, accessToken) => { - // `accessToken` can be falsy, but if we're trying to download media without authentication - // then we should expect failure anyway. - const headers = { ...req.requestHeaders }; - headers["Authorization"] = `Bearer ${accessToken}`; - return callback({ requestHeaders: headers }); - }); - global.mainWindow!.webContents.send("userAccessToken"); - - // we don't invoke callback() in this function - see the ipcMain.once above for callback usage. - }); + setupMediaAuth(global.mainWindow); }); app.on("window-all-closed", () => { diff --git a/src/media-auth.ts b/src/media-auth.ts new file mode 100644 index 0000000..de6601c --- /dev/null +++ b/src/media-auth.ts @@ -0,0 +1,80 @@ +/* +Copyright 2024 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { BrowserWindow, ipcMain, session } from "electron"; + +/** + * Check for feature support from the server. + * This requires asking the renderer process for supported versions. + */ +async function getSupportedVersions(window: BrowserWindow): Promise { + return new Promise((resolve) => { + ipcMain.once("serverSupportedVersions", (_, versionsResponse) => { + resolve(versionsResponse?.versions || []); + }); + window.webContents.send("serverSupportedVersions"); // ping now that the listener exists + }); +} + +/** + * Get the access token for the user. + * This requires asking the renderer process for the access token. + */ +async function getAccessToken(window: BrowserWindow): Promise { + return new Promise((resolve) => { + ipcMain.once("userAccessToken", (_, accessToken) => { + resolve(accessToken); + }); + window.webContents.send("userAccessToken"); // ping now that the listener exists + }); +} + +export function setupMediaAuth(window: BrowserWindow): void { + session.defaultSession.webRequest.onBeforeRequest(async (req, callback) => { + // This handler emulates the element-web service worker, where URLs are rewritten late in the request + // for backwards compatibility. As authenticated media becomes more prevalent, this should be replaced + // by the app using authenticated URLs from the outset. + let url = req.url; + if (!url.includes("/_matrix/media/v3/download") && !url.includes("/_matrix/media/v3/thumbnail")) { + return callback({}); // not a URL we care about + } + + const supportedVersions = await getSupportedVersions(window); + // We have to check that the access token is truthy otherwise we'd be intercepting pre-login media request too, + // e.g. those required for SSO button icons. + const accessToken = await getAccessToken(window); + if (supportedVersions.includes("v1.11") && accessToken) { + url = url.replace(/\/media\/v3\/(.*)\//, "/client/v1/media/$1/"); + return callback({ redirectURL: url }); + } else { + return callback({}); // no support == no modification + } + }); + + session.defaultSession.webRequest.onBeforeSendHeaders(async (req, callback) => { + if (!req.url.includes("/_matrix/client/v1/media")) { + return callback({}); // invoke unmodified + } + + // Only add authorization header to authenticated media URLs. This emulates the service worker + // behaviour in element-web. + const accessToken = await getAccessToken(window); + // `accessToken` can be falsy, but if we're trying to download media without authentication + // then we should expect failure anyway. + const headers = { ...req.requestHeaders, Authorization: `Bearer ${accessToken}` }; + return callback({ requestHeaders: headers }); + }); +}