diff --git a/README.md b/README.md index d2fe695f6..4db69e914 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,49 @@ $PROFILE` in which case it becomes `Element-$PROFILE`, or it is using one of the above created by a pre-1.7 install, in which case it will be `Riot` or `Riot-$PROFILE`. +See https://github.com/vector-im/element-web/blob/develop/docs/config.md + +Electron Config +=============== + +Electron config is stored in the same folder as the [user specified config](#user-specified-configjson) +in `electron-config.json`. +This is for configuring Electron options as opposed to Matrix/Element options from `config.json`. + +This has the following config options: + +* `warnBeforeExit`: boolean, optional (default: `true`) + * `true`: Element will display a confirmation box before exiting. +* `minimizeToTray`: boolean, optional (default: `true`) + * `true`: Element will enable the tray. +* `spellCheckerEnabled`: boolean, optional (default: `true`) + * `true`: Element spell checking is enabled. +* `autoHideMenuBar`: boolean, optional (default: `true`) + * `true`: Element will automatically hide the menu bar. +* `locale`: string[], optional + * A list of locales for Element to enable. +* `proxy`: object, optional (default: `null`) + * Proxy configuration for Electron to use. [More info](https://www.electronjs.org/docs/api/session#sessetproxyconfig). + * `mode`: string, optional (default: `null`) + * Proxy mode. If it's unspecified, it will be automatically determined based on other specified options. + * `direct`: All connections go direct without a proxy involved. + * `auto_detect`: The proxy configuration is determined by a PAC script that will be downloaded from `wpad/wpad.dat`. + * `pac_script`: The proxy configuration is determined by a PAC script specified in `pacScript`. + This is the default mode if `pacScript` is specified. + * `fixed_servers`: The proxy configuration is specified in `proxyRules`. + This is the default mode if `proxyRules` is specified and `pacScript` is not specified. + * `system`: The proxy configuration is taken from the operating system. + * `pacScript`: string, optional (default: null) + * The URL for a PAC file. + * This supports local files through the `file:` URI scheme, + e.g. `file:///home/$USER/proxy.pac` or `file://c:/Users/Username/Documents/proxy.pac`, + as well as `data:`, `http:`, and `https:` URI schemes. + * A `data:` URI should follow the format `data:application/x-javascript-config;base64,$BASE64_PAC_CONTENT`; + * `proxyRules`: string, optional (default: `null`) + * Rules indicating which proxies to use. + * `proxyBypassRules`: string, optional (default: `null`) + * Rules indication which URLs should bypass the proxy settings. + Translations ========================== diff --git a/package.json b/package.json index 33df4ceef..3a375757c 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "auto-launch": "^5.0.5", "counterpart": "^0.18.6", "electron-store": "^6.0.1", + "electron-updater": "^4.3.9", "electron-window-state": "^5.0.3", "minimist": "^1.2.3", "png-to-ico": "^2.1.1", diff --git a/src/electron-main.ts b/src/electron-main.ts index 33f6a3f36..6ce3ab83b 100644 --- a/src/electron-main.ts +++ b/src/electron-main.ts @@ -19,7 +19,8 @@ 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, ipcMain, powerSaveBlocker, BrowserWindow, Menu, autoUpdater, protocol, dialog } from "electron"; +import { app, ipcMain, powerSaveBlocker, BrowserWindow, Menu, protocol, dialog } from "electron"; +import { autoUpdater } from "electron-updater"; import AutoLaunch from "auto-launch"; import path from "path"; import windowStateKeeper from 'electron-window-state'; @@ -35,6 +36,7 @@ import webContentsHandler from './webcontents-handler'; import * as updater from './updater'; import { getProfileFromDeeplink, protocolInit, recordSSOSession } from './protocol'; import { _t, AppLocalization } from './language-helper'; +import { ProxyConfig, Proxy } from './proxy-helper'; const argv = minimist(process.argv, { alias: { help: "h" }, @@ -81,6 +83,7 @@ let iconPath; let trayConfig; let launcher; let appLocalization; +let proxy; if (argv["help"]) { console.log("Options:"); @@ -261,6 +264,7 @@ const store = new Store<{ spellCheckerEnabled?: boolean; autoHideMenuBar?: boolean; locale?: string | string[]; + proxy?: ProxyConfig; }>({ name: "electron-config" }); let eventIndex = null; @@ -938,6 +942,17 @@ app.on('ready', async () => { webgl: false, }, }); + + proxy = new Proxy({ + store: store, + sessions: [ + mainWindow.webContents.session, // apply proxy to main window + autoUpdater.netSession, // apply proxy to autoUpdater + ], + }); + + await proxy.applyProxy(); // wait for proxy settings to be applied + mainWindow.loadURL('vector://vector/webapp/'); // Handle spellchecker @@ -1018,6 +1033,7 @@ function beforeQuit() { if (mainWindow) { mainWindow.webContents.send('before-quit'); } + if (proxy) proxy.close(); } app.on('before-quit', beforeQuit); diff --git a/src/proxy-helper.ts b/src/proxy-helper.ts new file mode 100644 index 000000000..7121e5833 --- /dev/null +++ b/src/proxy-helper.ts @@ -0,0 +1,109 @@ +/* +Copyright 2021 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 { Session } from 'electron'; +import fs from 'fs'; +import type Store from 'electron-store'; + +export interface ProxyConfig { + mode?: "direct" | "auto_detect" | "pac_script" | "fixed_servers" | "system"; + pacScript?: string; + proxyRules?: string; + proxyBypassRules?: string; +} + +type TypedStore = Store<{ proxy?: ProxyConfig }>; + +export class Proxy { + public ready: Promise; + private static readonly STORE_KEY = "proxy"; + + private readonly store: TypedStore; + private readonly sessions: Array; + private proxy: ProxyConfig; + private pacWatcher: fs.FSWatcher; + + constructor( { store, sessions = [] }: { store: TypedStore, sessions: Session[] }) { + this.store = store; + this.sessions = sessions; + + if (this.store.has(Proxy.STORE_KEY)) { + console.log("Setting up proxy."); + this.setProxy(this.store.get(Proxy.STORE_KEY)); + } + } + + public setProxy(proxy: ProxyConfig): void { + this.proxy = proxy; + this.store.set(Proxy.STORE_KEY, this.proxy); + if (this.proxy.pacScript) { + // Add custom handling for the file: URI handler as chromium does not support it + // https://bugs.chromium.org/p/chromium/issues/detail?id=839566#c40 + const pacURL = new URL(this.proxy.pacScript); + if (pacURL.protocol === 'file:') { + this.setProxyFromPACFile(pacURL.pathname); + this.watchProxyPACFile(pacURL.pathname); + } + } + } + + public async applyProxy(): Promise { + // Apply the proxy config to the sessions + if (!this.proxy) return; + + return Promise.allSettled( + this.sessions.map((session) => + session.closeAllConnections() // Ensure all in-progress connections are closed + .then(() => session.setProxy(this.proxy)) // Set the proxy settings + .then(() => session.forceReloadProxyConfig()), // Ensure the updated config has been reloaded + )); + } + + private setProxyFromPACFile(pacFile: fs.PathLike): void { + // Convert PAC file path into a base64 data: URI + if (!this.proxy) return; + + const pacBuf = fs.readFileSync(pacFile); + this.proxy.pacScript = `data:application/x-javascript-config;base64,${pacBuf.toString('base64')}`; + } + + private watchProxyPACFile(pacFile: fs.PathLike): void { + // Watch the PAC file for changes and reapply config if a change is detected + if (this.pacWatcher) return; + + this.pacWatcher = fs.watch(pacFile, async (event) => { + console.log("Started watching PAC file."); + }); + + this.pacWatcher.on('change', (eventType: string) => { + console.log("PAC file changed, updating proxy settings."); + this.setProxyFromPACFile(pacFile); + this.applyProxy(); + }); + + this.pacWatcher.on('close', () => { + console.log("Stopped watching PAC file."); + }); + } + + public close(): void { + // Cleanup the fs watcher + if (!this.pacWatcher) return; + + this.pacWatcher.close(); + this.pacWatcher = null; + } +} diff --git a/src/updater.ts b/src/updater.ts index f41e0cf9e..8294eeaeb 100644 --- a/src/updater.ts +++ b/src/updater.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { app, autoUpdater, ipcMain } from "electron"; +import { app, ipcMain } from "electron"; +import { autoUpdater } from "electron-updater"; const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000; const INITIAL_UPDATE_DELAY_MS = 30 * 1000; diff --git a/yarn.lock b/yarn.lock index fc81e6ab3..a01856da9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -765,6 +765,11 @@ "@types/node" "*" xmlbuilder ">=11.0.1" +"@types/semver@^7.3.5": + version "7.3.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.8.tgz#508a27995498d7586dcecd77c25e289bfaf90c59" + integrity sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now== + "@types/verror@^1.10.3": version "1.10.5" resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.5.tgz#2a1413aded46e67a1fe2386800e291123ed75eb1" @@ -1947,6 +1952,20 @@ electron-store@^6.0.1: conf "^7.1.2" type-fest "^0.16.0" +electron-updater@^4.3.9: + version "4.3.9" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.3.9.tgz#247c660bafad7c07935e1b81acd3e9a5fd733154" + integrity sha512-LCNfedSwZfS4Hza+pDyPR05LqHtGorCStaBgVpRnfKxOlZcvpYEX0AbMeH5XUtbtGRoH2V8osbbf2qKPNb7AsA== + dependencies: + "@types/semver" "^7.3.5" + builder-util-runtime "8.7.5" + fs-extra "^10.0.0" + js-yaml "^4.1.0" + lazy-val "^1.0.4" + lodash.escaperegexp "^4.1.2" + lodash.isequal "^4.5.0" + semver "^7.3.5" + electron-window-state@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/electron-window-state/-/electron-window-state-5.0.3.tgz#4f36d09e3f953d87aff103bf010f460056050aa8" @@ -3133,11 +3152,21 @@ lodash.difference@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= + lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"