Support build-time specified protocol scheme for oidc callback (#2285)
This commit is contained in:
committed by
GitHub
parent
468d2249d1
commit
ec4c610158
@@ -1,6 +1,6 @@
|
|||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import { Configuration as BaseConfiguration } from "electron-builder";
|
import { type Configuration as BaseConfiguration, type Protocol } from "electron-builder";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This script has different outputs depending on your os platform.
|
* This script has different outputs depending on your os platform.
|
||||||
@@ -16,9 +16,13 @@ import { Configuration as BaseConfiguration } from "electron-builder";
|
|||||||
* Passes $ED_DEBIAN_CHANGELOG to build.deb.fpm if specified
|
* Passes $ED_DEBIAN_CHANGELOG to build.deb.fpm if specified
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const DEFAULT_APP_ID = "im.riot.app";
|
||||||
const NIGHTLY_APP_ID = "im.riot.nightly";
|
const NIGHTLY_APP_ID = "im.riot.nightly";
|
||||||
const NIGHTLY_DEB_NAME = "element-nightly";
|
const NIGHTLY_DEB_NAME = "element-nightly";
|
||||||
|
|
||||||
|
const DEFAULT_PROTOCOL_SCHEME = "io.element.desktop";
|
||||||
|
const NIGHTLY_PROTOCOL_SCHEME = "io.element.nightly";
|
||||||
|
|
||||||
interface Pkg {
|
interface Pkg {
|
||||||
name: string;
|
name: string;
|
||||||
productName: string;
|
productName: string;
|
||||||
@@ -33,7 +37,11 @@ type Writable<T> = NonNullable<
|
|||||||
const pkg: Pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
const pkg: Pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
||||||
|
|
||||||
interface Configuration extends BaseConfiguration {
|
interface Configuration extends BaseConfiguration {
|
||||||
extraMetadata: Partial<Pick<Pkg, "version">> & Omit<Pkg, "version">;
|
extraMetadata: Partial<Pick<Pkg, "version">> &
|
||||||
|
Omit<Pkg, "version"> & {
|
||||||
|
electron_appId: string;
|
||||||
|
electron_protocol: string;
|
||||||
|
};
|
||||||
linux: BaseConfiguration["linux"];
|
linux: BaseConfiguration["linux"];
|
||||||
win: BaseConfiguration["win"];
|
win: BaseConfiguration["win"];
|
||||||
mac: BaseConfiguration["mac"];
|
mac: BaseConfiguration["mac"];
|
||||||
@@ -50,7 +58,7 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
|||||||
// Make all fuses required to ensure they are all explicitly specified
|
// Make all fuses required to ensure they are all explicitly specified
|
||||||
electronFuses: Required<Configuration["electronFuses"]>;
|
electronFuses: Required<Configuration["electronFuses"]>;
|
||||||
} = {
|
} = {
|
||||||
appId: "im.riot.app",
|
appId: DEFAULT_APP_ID,
|
||||||
asarUnpack: "**/*.node",
|
asarUnpack: "**/*.node",
|
||||||
electronFuses: {
|
electronFuses: {
|
||||||
enableCookieEncryption: true,
|
enableCookieEncryption: true,
|
||||||
@@ -85,6 +93,8 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
|||||||
name: pkg.name,
|
name: pkg.name,
|
||||||
productName: pkg.productName,
|
productName: pkg.productName,
|
||||||
description: pkg.description,
|
description: pkg.description,
|
||||||
|
electron_appId: DEFAULT_APP_ID,
|
||||||
|
electron_protocol: DEFAULT_PROTOCOL_SCHEME,
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
target: ["tar.gz", "deb"],
|
target: ["tar.gz", "deb"],
|
||||||
@@ -142,12 +152,10 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
|||||||
directories: {
|
directories: {
|
||||||
output: "dist",
|
output: "dist",
|
||||||
},
|
},
|
||||||
protocols: [
|
protocols: {
|
||||||
{
|
name: "element",
|
||||||
name: "element",
|
schemes: [DEFAULT_PROTOCOL_SCHEME, "element"],
|
||||||
schemes: ["io.element.desktop", "element"],
|
},
|
||||||
},
|
|
||||||
],
|
|
||||||
nativeRebuilder: "sequential",
|
nativeRebuilder: "sequential",
|
||||||
nodeGypRebuild: false,
|
nodeGypRebuild: false,
|
||||||
npmRebuild: true,
|
npmRebuild: true,
|
||||||
@@ -170,11 +178,12 @@ if (process.env.ED_SIGNTOOL_SUBJECT_NAME && process.env.ED_SIGNTOOL_THUMBPRINT)
|
|||||||
if (process.env.ED_NIGHTLY) {
|
if (process.env.ED_NIGHTLY) {
|
||||||
config.deb.fpm = []; // Clear the fpm as the breaks deb fields don't apply to nightly
|
config.deb.fpm = []; // Clear the fpm as the breaks deb fields don't apply to nightly
|
||||||
|
|
||||||
config.appId = NIGHTLY_APP_ID;
|
config.appId = config.extraMetadata.electron_appId = NIGHTLY_APP_ID;
|
||||||
config.extraMetadata.productName += " Nightly";
|
config.extraMetadata.productName += " Nightly";
|
||||||
config.extraMetadata.name += "-nightly";
|
config.extraMetadata.name += "-nightly";
|
||||||
config.extraMetadata.description += " (nightly unstable build)";
|
config.extraMetadata.description += " (nightly unstable build)";
|
||||||
config.deb.fpm.push("--name", NIGHTLY_DEB_NAME);
|
config.deb.fpm.push("--name", NIGHTLY_DEB_NAME);
|
||||||
|
(config.protocols as Protocol).schemes[0] = config.extraMetadata.electron_protocol = NIGHTLY_PROTOCOL_SCHEME;
|
||||||
|
|
||||||
let version = process.env.ED_NIGHTLY;
|
let version = process.env.ED_NIGHTLY;
|
||||||
if (os.platform() === "win32") {
|
if (os.platform() === "win32") {
|
||||||
|
|||||||
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
@@ -13,11 +13,13 @@ import { type AppLocalization } from "../language-helper.js";
|
|||||||
// global type extensions need to use var for whatever reason
|
// global type extensions need to use var for whatever reason
|
||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
declare global {
|
declare global {
|
||||||
|
type IConfigOptions = Record<string, any>;
|
||||||
|
|
||||||
var mainWindow: BrowserWindow | null;
|
var mainWindow: BrowserWindow | null;
|
||||||
var appQuitting: boolean;
|
var appQuitting: boolean;
|
||||||
var appLocalization: AppLocalization;
|
var appLocalization: AppLocalization;
|
||||||
var launcher: AutoLaunch;
|
var launcher: AutoLaunch;
|
||||||
var vectorConfig: Record<string, any>;
|
var vectorConfig: IConfigOptions;
|
||||||
var trayConfig: {
|
var trayConfig: {
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
icon_path: string;
|
icon_path: string;
|
||||||
|
|||||||
26
src/build-config.ts
Normal file
26
src/build-config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path, { dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import { type JsonObject, loadJsonFile } from "./utils.js";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
interface BuildConfig {
|
||||||
|
appId: string;
|
||||||
|
protocol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readBuildConfig(): BuildConfig {
|
||||||
|
const packageJson = loadJsonFile(path.join(__dirname, "..", "package.json")) as JsonObject;
|
||||||
|
return {
|
||||||
|
appId: (packageJson["electron_appId"] as string) || "im.riot.app",
|
||||||
|
protocol: (packageJson["electron_protocol"] as string) || "io.element.desktop",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
// 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.js";
|
import "./squirrelhooks.js";
|
||||||
import { app, BrowserWindow, Menu, autoUpdater, protocol, dialog, type Input, type Event, session } from "electron";
|
import { app, BrowserWindow, Menu, autoUpdater, dialog, type Input, type Event, session, protocol } from "electron";
|
||||||
// eslint-disable-next-line n/file-extension-in-import
|
// eslint-disable-next-line n/file-extension-in-import
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import AutoLaunch from "auto-launch";
|
import AutoLaunch from "auto-launch";
|
||||||
@@ -28,12 +28,13 @@ import Store from "./store.js";
|
|||||||
import { buildMenuTemplate } from "./vectormenu.js";
|
import { buildMenuTemplate } from "./vectormenu.js";
|
||||||
import webContentsHandler from "./webcontents-handler.js";
|
import webContentsHandler from "./webcontents-handler.js";
|
||||||
import * as updater from "./updater.js";
|
import * as updater from "./updater.js";
|
||||||
import { getProfileFromDeeplink, protocolInit } from "./protocol.js";
|
import ProtocolHandler from "./protocol.js";
|
||||||
import { _t, AppLocalization } from "./language-helper.js";
|
import { _t, AppLocalization } from "./language-helper.js";
|
||||||
import { setDisplayMediaCallback } from "./displayMediaCallback.js";
|
import { setDisplayMediaCallback } from "./displayMediaCallback.js";
|
||||||
import { setupMacosTitleBar } from "./macos-titlebar.js";
|
import { setupMacosTitleBar } from "./macos-titlebar.js";
|
||||||
import { type Json, loadJsonFile } from "./utils.js";
|
import { type Json, loadJsonFile } from "./utils.js";
|
||||||
import { setupMediaAuth } from "./media-auth.js";
|
import { setupMediaAuth } from "./media-auth.js";
|
||||||
|
import { readBuildConfig } from "./build-config.js";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -72,10 +73,13 @@ function isRealUserDataDir(d: string): boolean {
|
|||||||
return fs.existsSync(path.join(d, "IndexedDB"));
|
return fs.existsSync(path.join(d, "IndexedDB"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildConfig = readBuildConfig();
|
||||||
|
const protocolHandler = new ProtocolHandler(buildConfig.protocol);
|
||||||
|
|
||||||
// check if we are passed a profile in the SSO callback url
|
// check if we are passed a profile in the SSO callback url
|
||||||
let userDataPath: string;
|
let userDataPath: string;
|
||||||
|
|
||||||
const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]);
|
const userDataPathInProtocol = protocolHandler.getProfileFromDeeplink(argv["_"]);
|
||||||
if (userDataPathInProtocol) {
|
if (userDataPathInProtocol) {
|
||||||
userDataPath = userDataPathInProtocol;
|
userDataPath = userDataPathInProtocol;
|
||||||
} else if (argv["profile-dir"]) {
|
} else if (argv["profile-dir"]) {
|
||||||
@@ -311,7 +315,7 @@ if (!gotLock) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do this after we know we are the primary instance of the app
|
// do this after we know we are the primary instance of the app
|
||||||
protocolInit();
|
protocolHandler.initialise(userDataPath);
|
||||||
|
|
||||||
// Register the scheme the app is served from as 'standard'
|
// Register the scheme the app is served from as 'standard'
|
||||||
// which allows things like relative URLs and IndexedDB to
|
// which allows things like relative URLs and IndexedDB to
|
||||||
@@ -616,4 +620,4 @@ app.on("second-instance", (ev, commandLine, workingDirectory) => {
|
|||||||
// It must also match the ID found in 'electron-builder'
|
// It must also match the ID found in 'electron-builder'
|
||||||
// in order to get the title and icon to show up correctly.
|
// in order to get the title and icon to show up correctly.
|
||||||
// Ref: https://stackoverflow.com/a/77314604/3525780
|
// Ref: https://stackoverflow.com/a/77314604/3525780
|
||||||
app.setAppUserModelId("im.riot.app");
|
app.setAppUserModelId(buildConfig.appId);
|
||||||
|
|||||||
11
src/ipc.ts
11
src/ipc.ts
@@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import { app, autoUpdater, desktopCapturer, ipcMain, powerSaveBlocker, TouchBar, nativeImage } from "electron";
|
import { app, autoUpdater, desktopCapturer, ipcMain, powerSaveBlocker, TouchBar, nativeImage } from "electron";
|
||||||
|
|
||||||
import IpcMainEvent = Electron.IpcMainEvent;
|
import IpcMainEvent = Electron.IpcMainEvent;
|
||||||
import { recordSSOSession } from "./protocol.js";
|
|
||||||
import { randomArray } from "./utils.js";
|
import { randomArray } from "./utils.js";
|
||||||
import { Settings } from "./settings.js";
|
import { Settings } from "./settings.js";
|
||||||
import { getDisplayMediaCallback, setDisplayMediaCallback } from "./displayMediaCallback.js";
|
import { getDisplayMediaCallback, setDisplayMediaCallback } from "./displayMediaCallback.js";
|
||||||
@@ -96,9 +95,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
global.mainWindow.focus();
|
global.mainWindow.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "getConfig":
|
|
||||||
ret = global.vectorConfig;
|
|
||||||
break;
|
|
||||||
case "navigateBack":
|
case "navigateBack":
|
||||||
if (global.mainWindow.webContents.canGoBack()) {
|
if (global.mainWindow.webContents.canGoBack()) {
|
||||||
global.mainWindow.webContents.goBack();
|
global.mainWindow.webContents.goBack();
|
||||||
@@ -135,10 +132,6 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages;
|
ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "startSSOFlow":
|
|
||||||
recordSSOSession(args[0]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "getPickleKey":
|
case "getPickleKey":
|
||||||
try {
|
try {
|
||||||
ret = await store.getSecret(`${args[0]}|${args[1]}`);
|
ret = await store.getSecret(`${args[0]}|${args[1]}`);
|
||||||
@@ -248,3 +241,5 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
reply: ret,
|
reply: ret,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("getConfig", () => global.vectorConfig);
|
||||||
|
|||||||
@@ -49,4 +49,16 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
}
|
}
|
||||||
ipcRenderer.send(channel, ...args);
|
ipcRenderer.send(channel, ...args);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async initialise(): Promise<{
|
||||||
|
protocol: string;
|
||||||
|
sessionId: string;
|
||||||
|
config: IConfigOptions;
|
||||||
|
}> {
|
||||||
|
const [{ protocol, sessionId }, config] = await Promise.all([
|
||||||
|
ipcRenderer.invoke("getProtocol"),
|
||||||
|
ipcRenderer.invoke("getConfig"),
|
||||||
|
]);
|
||||||
|
return { protocol, sessionId, config };
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
199
src/protocol.ts
199
src/protocol.ts
@@ -6,119 +6,136 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from "electron";
|
import { app, ipcMain } from "electron";
|
||||||
import { URL } from "node:url";
|
import { URL } from "node:url";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
const LEGACY_PROTOCOL = "element";
|
const LEGACY_PROTOCOL = "element";
|
||||||
const PROTOCOL = "io.element.desktop";
|
|
||||||
const SEARCH_PARAM = "element-desktop-ssoid";
|
const SEARCH_PARAM = "element-desktop-ssoid";
|
||||||
const STORE_FILE_NAME = "sso-sessions.json";
|
const STORE_FILE_NAME = "sso-sessions.json";
|
||||||
|
|
||||||
// we getPath userData before electron-main changes it, so this is the default value
|
// we getPath userData before electron-main changes it, so this is the default value
|
||||||
const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME);
|
const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME);
|
||||||
|
|
||||||
function processUrl(url: string): void {
|
export default class ProtocolHandler {
|
||||||
if (!global.mainWindow) return;
|
private readonly store: Record<string, string> = {};
|
||||||
|
private readonly sessionId: string;
|
||||||
|
|
||||||
const parsed = new URL(url);
|
public constructor(private readonly protocol: string) {
|
||||||
// sanity check: we only register for the one protocol, so we shouldn't
|
// get all args except `hidden` as it'd mean the app would not get focused
|
||||||
// be getting anything else unless the user is forcing a URL to open
|
// XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking
|
||||||
// with the Element app.
|
// --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url
|
||||||
if (parsed.protocol !== `${PROTOCOL}:` && parsed.protocol !== `${LEGACY_PROTOCOL}:`) {
|
const args = process.argv.slice(1).filter((arg) => arg !== "--hidden" && arg !== "-hidden");
|
||||||
console.log("Ignoring unexpected protocol: ", parsed.protocol);
|
if (app.isPackaged) {
|
||||||
return;
|
app.setAsDefaultProtocolClient(this.protocol, process.execPath, args);
|
||||||
|
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, args);
|
||||||
|
} else if (process.platform === "win32") {
|
||||||
|
// on Mac/Linux this would just cause the electron binary to open
|
||||||
|
// special handler for running without being packaged, e.g `electron .` by passing our app path to electron
|
||||||
|
app.setAsDefaultProtocolClient(this.protocol, process.execPath, [app.getAppPath(), ...args]);
|
||||||
|
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, [app.getAppPath(), ...args]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
// Protocol handler for macos
|
||||||
|
app.on("open-url", (ev, url) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.processUrl(url);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Protocol handler for win32/Linux
|
||||||
|
app.on("second-instance", (ev, commandLine) => {
|
||||||
|
const url = commandLine[commandLine.length - 1];
|
||||||
|
if (!url.startsWith(`${this.protocol}:/`) && !url.startsWith(`${LEGACY_PROTOCOL}://`)) return;
|
||||||
|
this.processUrl(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store = this.readStore();
|
||||||
|
this.sessionId = randomUUID();
|
||||||
|
|
||||||
|
ipcMain.handle("getProtocol", this.onGetProtocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlToLoad = new URL("vector://vector/webapp/");
|
private readonly onGetProtocol = (): { protocol: string; sessionId: string } => {
|
||||||
// ignore anything other than the search (used for SSO login redirect)
|
return {
|
||||||
// and the hash (for general element deep links)
|
protocol: this.protocol,
|
||||||
// There's no reason to allow anything else, particularly other paths,
|
sessionId: this.sessionId,
|
||||||
// since this would allow things like the internal jitsi wrapper to
|
};
|
||||||
// be loaded, which would get the app stuck on that page and generally
|
};
|
||||||
// be a bit strange and confusing.
|
|
||||||
urlToLoad.search = parsed.search;
|
|
||||||
urlToLoad.hash = parsed.hash;
|
|
||||||
|
|
||||||
console.log("Opening URL: ", urlToLoad.href);
|
private processUrl(url: string): void {
|
||||||
void global.mainWindow.loadURL(urlToLoad.href);
|
if (!global.mainWindow) return;
|
||||||
}
|
|
||||||
|
|
||||||
function readStore(): Record<string, string> {
|
const parsed = new URL(url);
|
||||||
try {
|
// sanity check: we only register for the one protocol, so we shouldn't
|
||||||
const s = fs.readFileSync(storePath, { encoding: "utf8" });
|
// be getting anything else unless the user is forcing a URL to open
|
||||||
const o = JSON.parse(s);
|
// with the Element app.
|
||||||
return typeof o === "object" ? o : {};
|
if (parsed.protocol !== `${this.protocol}:` && parsed.protocol !== `${LEGACY_PROTOCOL}:`) {
|
||||||
} catch {
|
console.log("Ignoring unexpected protocol: ", parsed.protocol);
|
||||||
return {};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlToLoad = new URL("vector://vector/webapp/");
|
||||||
|
// ignore anything other than the search (used for SSO login redirect)
|
||||||
|
// and the hash (for general element deep links)
|
||||||
|
// There's no reason to allow anything else, particularly other paths,
|
||||||
|
// since this would allow things like the internal jitsi wrapper to
|
||||||
|
// be loaded, which would get the app stuck on that page and generally
|
||||||
|
// be a bit strange and confusing.
|
||||||
|
urlToLoad.search = parsed.search;
|
||||||
|
urlToLoad.hash = parsed.hash;
|
||||||
|
|
||||||
|
console.log("Opening URL: ", urlToLoad.href);
|
||||||
|
void global.mainWindow.loadURL(urlToLoad.href);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function writeStore(data: Record<string, string>): void {
|
private readStore(): Record<string, string> {
|
||||||
fs.writeFileSync(storePath, JSON.stringify(data));
|
try {
|
||||||
}
|
const s = fs.readFileSync(storePath, { encoding: "utf8" });
|
||||||
|
const o = JSON.parse(s);
|
||||||
export function recordSSOSession(sessionID: string): void {
|
return typeof o === "object" ? o : {};
|
||||||
const userDataPath = app.getPath("userData");
|
} catch {
|
||||||
const store = readStore();
|
return {};
|
||||||
for (const key in store) {
|
|
||||||
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
|
|
||||||
if (store[key] === userDataPath) {
|
|
||||||
delete store[key];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
store[sessionID] = userDataPath;
|
|
||||||
writeStore(store);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProfileFromDeeplink(args: string[]): string | undefined {
|
private writeStore(): void {
|
||||||
// check if we are passed a profile in the SSO callback url
|
fs.writeFileSync(storePath, JSON.stringify(this.store));
|
||||||
const deeplinkUrl = args.find((arg) => arg.startsWith(`${PROTOCOL}://`) || arg.startsWith(`${LEGACY_PROTOCOL}://`));
|
}
|
||||||
if (deeplinkUrl?.includes(SEARCH_PARAM)) {
|
|
||||||
const parsedUrl = new URL(deeplinkUrl);
|
public initialise(userDataPath: string): void {
|
||||||
if (parsedUrl.protocol === `${PROTOCOL}:` || parsedUrl.protocol === `${LEGACY_PROTOCOL}:`) {
|
for (const key in this.store) {
|
||||||
const store = readStore();
|
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
|
||||||
let ssoID = parsedUrl.searchParams.get(SEARCH_PARAM);
|
if (this.store[key] === userDataPath) {
|
||||||
if (!ssoID) {
|
delete this.store[key];
|
||||||
// In OIDC, we must shuttle the value in the `state` param rather than `element-desktop-ssoid`
|
break;
|
||||||
// We encode it as a suffix like `:element-desktop-ssoid:XXYYZZ`
|
}
|
||||||
ssoID = parsedUrl.searchParams.get("state")!.split(`:${SEARCH_PARAM}:`)[1];
|
}
|
||||||
|
this.store[this.sessionId] = userDataPath;
|
||||||
|
this.writeStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getProfileFromDeeplink(args: string[]): string | undefined {
|
||||||
|
// check if we are passed a profile in the SSO callback url
|
||||||
|
const deeplinkUrl = args.find(
|
||||||
|
(arg) => arg.startsWith(`${this.protocol}://`) || arg.startsWith(`${LEGACY_PROTOCOL}://`),
|
||||||
|
);
|
||||||
|
if (deeplinkUrl?.includes(SEARCH_PARAM)) {
|
||||||
|
const parsedUrl = new URL(deeplinkUrl);
|
||||||
|
if (parsedUrl.protocol === `${this.protocol}:` || parsedUrl.protocol === `${LEGACY_PROTOCOL}:`) {
|
||||||
|
const store = this.readStore();
|
||||||
|
let sessionId = parsedUrl.searchParams.get(SEARCH_PARAM);
|
||||||
|
if (!sessionId) {
|
||||||
|
// In OIDC, we must shuttle the value in the `state` param rather than `element-desktop-ssoid`
|
||||||
|
// We encode it as a suffix like `:element-desktop-ssoid:XXYYZZ`
|
||||||
|
sessionId = parsedUrl.searchParams.get("state")!.split(`:${SEARCH_PARAM}:`)[1];
|
||||||
|
}
|
||||||
|
console.log("Forwarding to profile: ", store[sessionId]);
|
||||||
|
return store[sessionId];
|
||||||
}
|
}
|
||||||
console.log("Forwarding to profile: ", store[ssoID]);
|
|
||||||
return store[ssoID];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function protocolInit(): void {
|
|
||||||
// get all args except `hidden` as it'd mean the app would not get focused
|
|
||||||
// XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking
|
|
||||||
// --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url
|
|
||||||
const args = process.argv.slice(1).filter((arg) => arg !== "--hidden" && arg !== "-hidden");
|
|
||||||
if (app.isPackaged) {
|
|
||||||
app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, args);
|
|
||||||
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, args);
|
|
||||||
} else if (process.platform === "win32") {
|
|
||||||
// on Mac/Linux this would just cause the electron binary to open
|
|
||||||
// special handler for running without being packaged, e.g `electron .` by passing our app path to electron
|
|
||||||
app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [app.getAppPath(), ...args]);
|
|
||||||
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, [app.getAppPath(), ...args]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
|
||||||
// Protocol handler for macos
|
|
||||||
app.on("open-url", function (ev, url) {
|
|
||||||
ev.preventDefault();
|
|
||||||
processUrl(url);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Protocol handler for win32/Linux
|
|
||||||
app.on("second-instance", (ev, commandLine) => {
|
|
||||||
const url = commandLine[commandLine.length - 1];
|
|
||||||
if (!url.startsWith(`${PROTOCOL}:/`) && !url.startsWith(`${LEGACY_PROTOCOL}://`)) return;
|
|
||||||
processUrl(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function randomArray(size: number): Promise<string> {
|
|||||||
|
|
||||||
type JsonValue = null | string | number;
|
type JsonValue = null | string | number;
|
||||||
type JsonArray = Array<JsonValue | JsonObject | JsonArray>;
|
type JsonArray = Array<JsonValue | JsonObject | JsonArray>;
|
||||||
interface JsonObject {
|
export interface JsonObject {
|
||||||
[key: string]: JsonObject | JsonArray | JsonValue;
|
[key: string]: JsonObject | JsonArray | JsonValue;
|
||||||
}
|
}
|
||||||
export type Json = JsonArray | JsonObject;
|
export type Json = JsonArray | JsonObject;
|
||||||
|
|||||||
Reference in New Issue
Block a user