diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index f9619285c0..62f1fb385e 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -35,7 +35,7 @@ const tray = require('./tray'); const vectorMenu = require('./vectormenu'); const webContentsHandler = require('./webcontents-handler'); const updater = require('./updater'); -const protocolInit = require('./protocol'); +const {getProfileFromDeeplink, protocolInit, recordSSOSession} = require('./protocol'); const windowStateKeeper = require('electron-window-state'); const Store = require('electron-store'); @@ -68,7 +68,11 @@ if (argv["help"]) { app.exit(); } -if (argv['profile-dir']) { +// check if we are passed a profile in the SSO callback url +const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]); +if (userDataPathInProtocol) { + app.setPath('userData', userDataPathInProtocol); +} else if (argv['profile-dir']) { app.setPath('userData', argv['profile-dir']); } else if (argv['profile']) { app.setPath('userData', `${app.getPath('userData')}-${argv['profile']}`); @@ -243,6 +247,9 @@ ipcMain.on('ipcCall', async function(ev, payload) { mainWindow.webContents.goForward(); } break; + case 'startSSOFlow': + recordSSOSession(args[0]); + break; default: mainWindow.webContents.send('ipcReply', { diff --git a/electron_app/src/protocol.js b/electron_app/src/protocol.js index 153ff64084..48247fef98 100644 --- a/electron_app/src/protocol.js +++ b/electron_app/src/protocol.js @@ -14,40 +14,91 @@ See the License for the specific language governing permissions and limitations under the License. */ -const {app} = require('electron'); +const {app} = require("electron"); +const path = require("path"); +const fs = require("fs"); + +const PROTOCOL = "riot://"; +const SEARCH_PARAM = "riot-desktop-ssoid"; +const STORE_FILE_NAME = "sso-sessions.json"; + +// 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 processUrl = (url) => { if (!global.mainWindow) return; console.log("Handling link: ", url); - global.mainWindow.loadURL(url.replace("riot://", "vector://")); + global.mainWindow.loadURL(url.replace(PROTOCOL, "vector://")); }; -module.exports = () => { - // 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 and --profile passing won't work on Mac/Linux - const args = process.argv.slice(1).filter(arg => arg !== "--hidden" && arg !== "-hidden"); - if (app.isPackaged) { - app.setAsDefaultProtocolClient('riot', 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('riot', 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("riot://")) return; - processUrl(url); - }); +const readStore = () => { + try { + const s = fs.readFileSync(storePath, { encoding: "utf8" }); + const o = JSON.parse(s); + return typeof o === "object" ? o : {}; + } catch (e) { + return {}; } }; +const writeStore = (data) => { + fs.writeFileSync(storePath, JSON.stringify(data)); +}; + +module.exports = { + recordSSOSession: (sessionID) => { + const userDataPath = app.getPath('userData'); + const store = readStore(); + 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); + }, + getProfileFromDeeplink: (args) => { + // check if we are passed a profile in the SSO callback url + const deeplinkUrl = args.find(arg => arg.startsWith('riot://')); + if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) { + const parsedUrl = new URL(deeplinkUrl); + if (parsedUrl.protocol === 'riot:') { + const ssoID = parsedUrl.searchParams.get(SEARCH_PARAM); + const store = readStore(); + console.log("Forwarding to profile: ", store[ssoID]); + return store[ssoID]; + } + } + }, + protocolInit: () => { + // 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('riot', 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('riot', 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)) return; + processUrl(url); + }); + } + }, +}; + diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js index 172718c825..aee9ee4930 100644 --- a/src/vector/platform/ElectronPlatform.js +++ b/src/vector/platform/ElectronPlatform.js @@ -32,6 +32,7 @@ import Spinner from "matrix-react-sdk/src/components/views/elements/Spinner"; import {Categories, Modifiers, registerShortcut} from "matrix-react-sdk/src/accessibility/KeyboardShortcuts"; import {Key} from "matrix-react-sdk/src/Keyboard"; import React from "react"; +import {randomString} from "matrix-js-sdk/src/randomstring"; const ipcRenderer = window.ipcRenderer; const isMac = navigator.platform.toUpperCase().includes('MAC'); @@ -250,6 +251,10 @@ export default class ElectronPlatform extends VectorBasePlatform { description: _td("Previous/next recently visited room or community"), }); } + + // this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile + this.ssoID = randomString(32); + this._ipcCall("startSSOFlow", this.ssoID); } async getConfig(): Promise<{}> { @@ -446,6 +451,7 @@ export default class ElectronPlatform extends VectorBasePlatform { getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { const url = super.getSSOCallbackUrl(hsUrl, isUrl); url.protocol = "riot"; + url.searchParams.set("riot-desktop-ssoid", this.ssoID); return url; }