Support authenticated media downloads in Desktop too (#1757)

* Support authenticated media downloads in Desktop too

We can't use service workers for a variety of reasons/errors, so we instead intercept HTTP(S) requests from the renderer process. With a bit of help from the IPC channels, we're able to emulate what the Element Web ServiceWorker does.

The IPC channel is considered "safe" for transmitting sensitive details like the user access token: if we can't trust the IPC, we can't trust much of anything. This is unlike the `postMessage` API in a web browser where browser extensions may be listening: we don't have extensions in this environment.

* Remove unused import

* Appease the linter
This commit is contained in:
Travis Ralston 2024-07-10 07:41:27 -06:00 committed by GitHub
parent 14a24be4ea
commit 8754fa5fa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 48 additions and 1 deletions

View File

@ -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. // Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
import "./squirrelhooks"; import "./squirrelhooks";
import { app, BrowserWindow, Menu, autoUpdater, protocol, dialog, Input, Event, session } from "electron"; import { app, BrowserWindow, Menu, autoUpdater, protocol, dialog, Input, Event, session, ipcMain } from "electron";
import * as Sentry from "@sentry/electron/main"; import * as Sentry from "@sentry/electron/main";
import AutoLaunch from "auto-launch"; import AutoLaunch from "auto-launch";
import path from "path"; import path from "path";
@ -549,6 +549,51 @@ app.on("ready", async () => {
global.mainWindow?.webContents.send("openDesktopCapturerSourcePicker"); global.mainWindow?.webContents.send("openDesktopCapturerSourcePicker");
setDisplayMediaCallback(callback); 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.
});
}); });
app.on("window-all-closed", () => { app.on("window-all-closed", () => {

View File

@ -35,6 +35,8 @@ const CHANNELS = [
"userDownloadCompleted", "userDownloadCompleted",
"userDownloadAction", "userDownloadAction",
"openDesktopCapturerSourcePicker", "openDesktopCapturerSourcePicker",
"userAccessToken",
"serverSupportedVersions",
]; ];
contextBridge.exposeInMainWorld("electron", { contextBridge.exposeInMainWorld("electron", {