diff options
Diffstat (limited to 'gui/src/main')
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 16 | ||||
| -rw-r--r-- | gui/src/main/gui-settings.ts | 2 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 105 | ||||
| -rw-r--r-- | gui/src/main/notification-controller.ts | 3 | ||||
| -rw-r--r-- | gui/src/main/windows-split-tunneling.ts | 43 |
5 files changed, 144 insertions, 25 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index c740393e2a..10159ad49e 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -476,6 +476,18 @@ export class DaemonRpc { return response.toObject(); } + public async addSplitTunnelingApplication(path: string): Promise<void> { + await this.callString(this.client.addSplitTunnelApp, path); + } + + public async removeSplitTunnelingApplication(path: string): Promise<void> { + await this.callString(this.client.removeSplitTunnelApp, path); + } + + public async setSplitTunnelingState(enabled: boolean): Promise<void> { + await this.callBool(this.client.setSplitTunnelState, enabled); + } + private subscriptionId(): number { const current = this.nextSubscriptionId; this.nextSubscriptionId += 1; @@ -802,7 +814,7 @@ function convertFromTunnelStateErrorCause( return { reason: 'tunnel_parameter_error', details: parameterErrorMap[state.parameterError] }; } case grpcTypes.ErrorState.Cause.SPLIT_TUNNEL_ERROR: - return { reason: 'start_tunnel_error' }; + return { reason: 'split_tunnel_error' }; case grpcTypes.ErrorState.Cause.VPN_PERMISSION_DENIED: // VPN_PERMISSION_DENIED is only ever created on Android throw invalidErrorStateCause; @@ -868,12 +880,14 @@ function convertFromSettings(settings: grpcTypes.Settings): ISettings | undefine const relaySettings = convertFromRelaySettings(settings.getRelaySettings())!; const bridgeSettings = convertFromBridgeSettings(settingsObject.bridgeSettings!); const tunnelOptions = convertFromTunnelOptions(settingsObject.tunnelOptions!); + const splitTunnel = settingsObject.splitTunnel ?? { enableExclusions: false, appsList: [] }; return { ...settings.toObject(), bridgeState, relaySettings, bridgeSettings, tunnelOptions, + splitTunnel, }; } diff --git a/gui/src/main/gui-settings.ts b/gui/src/main/gui-settings.ts index 7708facc84..57dc754cd8 100644 --- a/gui/src/main/gui-settings.ts +++ b/gui/src/main/gui-settings.ts @@ -81,7 +81,7 @@ export default class GuiSettings { return this.stateValue.unpinnedWindow; } - public addBrowsedForSplitTunnelingapplications(newApp: string) { + public addBrowsedForSplitTunnelingApplications(newApp: string) { this.changeStateAndNotify({ ...this.stateValue, browsedForSplitTunnelingApplications: [...this.browsedForSplitTunnelingApplications, newApp], diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 1785794818..1c981adf11 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -16,6 +16,7 @@ import { sprintf } from 'sprintf-js'; import * as uuid from 'uuid'; import config from '../config.json'; import { hasExpired } from '../shared/account-expiry'; +import { IApplication } from '../shared/application-types'; import BridgeSettingsBuilder from '../shared/bridge-settings-builder'; import { AccountToken, @@ -75,8 +76,9 @@ import TrayIconController, { TrayIconType } from './tray-icon-controller'; import WindowController from './window-controller'; import { ITranslations } from '../shared/ipc-schema'; -// Only import when running app on Linux. +// Only import split tunneling library on correct OS. const linuxSplitTunneling = process.platform === 'linux' && require('./linux-split-tunneling'); +const windowsSplitTunneling = process.platform === 'win32' && require('./windows-split-tunneling'); const DAEMON_RPC_PATH = process.platform === 'win32' ? 'unix:////./pipe/Mullvad VPN' : 'unix:///var/run/mullvad-vpn'; @@ -108,6 +110,10 @@ class ApplicationMain { private tray?: Tray; private trayIconController?: TrayIconController; + // True while file pickers are displayed which is used to decide if the Browser window should be + // hidden when losing focus. + private browsingFiles = false; + private daemonRpc = new DaemonRpc(DAEMON_RPC_PATH); private daemonEventListener?: SubscriptionListener<DaemonEvent>; private reconnectBackoff = new ReconnectionBackoff(); @@ -123,6 +129,10 @@ class ApplicationMain { autoConnect: false, blockWhenDisconnected: false, showBetaReleases: false, + splitTunnel: { + enableExclusions: false, + appsList: [], + }, relaySettings: { normal: { location: 'any', @@ -218,6 +228,8 @@ class ApplicationMain { private rendererLog?: Logger; private translations: ITranslations = { locale: this.locale }; + private windowsSplitTunnelingApplications?: IApplication[]; + public run() { // Remove window animations to combat window flickering when opening window. Can be removed when // this issue has been resolved: https://github.com/electron/electron/issues/12130 @@ -722,6 +734,7 @@ class ApplicationMain { this.notificationController.notifyTunnelState( newState, this.settings.blockWhenDisconnected, + this.settings.splitTunnel.enableExclusions && this.settings.splitTunnel.appsList.length > 0, this.accountData?.expiry, ); @@ -754,6 +767,10 @@ class ApplicationMain { if (this.windowController) { IpcMainEventChannel.settings.notify(this.windowController.webContents, newSettings); + + if (windowsSplitTunneling) { + consumePromise(this.updateSplitTunnelingApplications(newSettings.splitTunnel.appsList)); + } } // since settings can have the relay constraints changed, the relay @@ -761,6 +778,20 @@ class ApplicationMain { this.setRelays(this.relays, newSettings.relaySettings, newSettings.bridgeState); } + private async updateSplitTunnelingApplications(appList: string[]): Promise<void> { + const { applications } = await windowsSplitTunneling.getApplications({ + applicationPaths: appList, + }); + this.windowsSplitTunnelingApplications = applications; + + if (this.windowController) { + IpcMainEventChannel.windowsSplitTunneling.notify( + this.windowController.webContents, + applications, + ); + } + } + private setLocation(newLocation: ILocation) { this.location = newLocation; @@ -1056,6 +1087,7 @@ class ApplicationMain { translations: this.translations, platform: process.platform, runningInDevelopment: process.env.NODE_ENV === 'development', + windowsSplitTunnelingApplications: this.windowsSplitTunnelingApplications, })); IpcMainEventChannel.settings.handleSetAllowLan((allowLan: boolean) => @@ -1155,18 +1187,63 @@ class ApplicationMain { }); IpcMainEventChannel.wireguardKeys.handleVerifyKey(() => this.daemonRpc.verifyWireguardKey()); - IpcMainEventChannel.splitTunneling.handleGetApplications(() => { + IpcMainEventChannel.linuxSplitTunneling.handleGetApplications(() => { if (linuxSplitTunneling) { return linuxSplitTunneling.getApplications(this.locale); } else { - throw Error('linuxSplitTunneling called without being imported'); + throw Error('linuxSplitTunneling.getApplications function called without being imported'); } }); - IpcMainEventChannel.splitTunneling.handleLaunchApplication((application) => { + IpcMainEventChannel.windowsSplitTunneling.handleGetApplications((updateCaches: boolean) => { + if (windowsSplitTunneling) { + return windowsSplitTunneling.getApplications({ + updateCaches, + }); + } else { + throw Error('windowsSplitTunneling.getApplications function called without being imported'); + } + }); + IpcMainEventChannel.linuxSplitTunneling.handleLaunchApplication((application) => { if (linuxSplitTunneling) { return linuxSplitTunneling.launchApplication(application); } else { - throw Error('linuxSplitTunneling called without being imported'); + throw Error('linuxSplitTunneling.launchApplication function called without being imported'); + } + }); + + IpcMainEventChannel.windowsSplitTunneling.handleSetState((enabled) => { + if (windowsSplitTunneling) { + return this.daemonRpc.setSplitTunnelingState(enabled); + } else { + throw Error('windowsSplitTunneling.setState function called without being imported'); + } + }); + IpcMainEventChannel.windowsSplitTunneling.handleAddApplication(async (application) => { + if (windowsSplitTunneling) { + // If the applications is a string (path) it's an application picked with the file picker + // that we want to add to the list of additional applications. + if (typeof application === 'string') { + this.guiSettings.addBrowsedForSplitTunnelingApplications(application); + const applicationPath = windowsSplitTunneling.addApplicationPathToCache(application); + await this.daemonRpc.addSplitTunnelingApplication(applicationPath); + } else { + await this.daemonRpc.addSplitTunnelingApplication(application.absolutepath); + } + } else { + throw Error( + 'windowsSplitTunneling.handleAddApplication function called without being imported', + ); + } + }); + IpcMainEventChannel.windowsSplitTunneling.handleRemoveApplication((application) => { + if (windowsSplitTunneling) { + return this.daemonRpc.removeSplitTunnelingApplication( + typeof application === 'string' ? application : application.absolutepath, + ); + } else { + throw Error( + 'windowsSplitTunneling.handleRemoveApplication function called without being imported', + ); } }); @@ -1228,7 +1305,21 @@ class ApplicationMain { await shell.openExternal(url); } }); - IpcMainEventChannel.app.handleShowOpenDialog((options) => dialog.showOpenDialog(options)); + IpcMainEventChannel.app.handleShowOpenDialog(async (options) => { + this.browsingFiles = true; + const response = await dialog.showOpenDialog({ + defaultPath: app.getPath('home'), + ...options, + }); + this.browsingFiles = false; + return response; + }); + + if (windowsSplitTunneling) { + this.guiSettings.browsedForSplitTunnelingApplications.forEach( + windowsSplitTunneling.addApplicationPathToCache, + ); + } } private async createNewAccount(): Promise<string> { @@ -1806,7 +1897,7 @@ class ApplicationMain { cursorPos.y >= trayBounds.y && cursorPos.x <= trayBounds.x + trayBounds.width && cursorPos.y <= trayBounds.y + trayBounds.height; - if (!isCursorInside) { + if (!isCursorInside && !this.browsingFiles) { windowController.hide(); } }); diff --git a/gui/src/main/notification-controller.ts b/gui/src/main/notification-controller.ts index 80eb14364a..ca0e12888f 100644 --- a/gui/src/main/notification-controller.ts +++ b/gui/src/main/notification-controller.ts @@ -51,6 +51,7 @@ export default class NotificationController { public notifyTunnelState( tunnelState: TunnelState, blockWhenDisconnected: boolean, + hasExcludedApps: boolean, accountExpiry?: string, ) { const notificationProviders: SystemNotificationProvider[] = [ @@ -58,7 +59,7 @@ export default class NotificationController { new ConnectedNotificationProvider(tunnelState), new ReconnectingNotificationProvider(tunnelState), new DisconnectedNotificationProvider({ tunnelState, blockWhenDisconnected }), - new ErrorNotificationProvider({ tunnelState, accountExpiry }), + new ErrorNotificationProvider({ tunnelState, accountExpiry, hasExcludedApps }), ]; const notificationProvider = notificationProviders.find((notification) => diff --git a/gui/src/main/windows-split-tunneling.ts b/gui/src/main/windows-split-tunneling.ts index c07f9f2f7c..88a32f9850 100644 --- a/gui/src/main/windows-split-tunneling.ts +++ b/gui/src/main/windows-split-tunneling.ts @@ -36,7 +36,13 @@ const APPLICATION_PATHS = [ // Some applications might be falsely filtered from the application list. This allow-list specifies // apps that are falsely filtered but should be included. -const APPLICATION_ALLOW_LIST = ['firefox.exe', 'chrome.exe']; +const APPLICATION_ALLOW_LIST = [ + 'firefox.exe', + 'chrome.exe', + 'msedge.exe', + 'brave.exe', + 'iexplore.exe', +]; // Cache of all previously scanned shortcuts. const shortcutCache: Record<string, ShortcutDetails> = {}; @@ -68,7 +74,10 @@ export async function getApplications(options: { .filter( (application) => options.applicationPaths === undefined || - options.applicationPaths.includes(application.absolutepath), + options.applicationPaths.find( + (applicationPath) => + applicationPath.toLowerCase() === application.absolutepath.toLowerCase(), + ) !== undefined, ) .sort((a, b) => a.name.localeCompare(b.name)); @@ -101,11 +110,11 @@ async function updateShortcutCache(): Promise<void> { const shortcuts: ShortcutDetails[] = []; for (const shortcut of resolvedLinks) { if ( - APPLICATION_ALLOW_LIST.includes(path.basename(shortcut.target)) || + APPLICATION_ALLOW_LIST.includes(path.basename(shortcut.target.toLowerCase())) || (await importsDll(shortcut.target, 'WS2_32.dll')) ) { shortcuts.push(shortcut); - shortcutCache[shortcut.target] = shortcut; + shortcutCache[shortcut.target.toLowerCase()] = shortcut; } } } @@ -115,11 +124,12 @@ async function updateApplicationCache(): Promise<void> { await Promise.all( shortcuts.map(async (shortcut) => { - if (applicationCache[shortcut.target] === undefined) { - applicationCache[shortcut.target] = await convertToSplitTunnelingApplication(shortcut); + const lowercaseTarget = shortcut.target.toLowerCase(); + if (applicationCache[lowercaseTarget] === undefined) { + applicationCache[lowercaseTarget] = await convertToSplitTunnelingApplication(shortcut); } - return applicationCache[shortcut.target]; + return applicationCache[lowercaseTarget]; }), ); } @@ -127,8 +137,10 @@ async function updateApplicationCache(): Promise<void> { // Add excluded apps that are missing from the shortcut cache to it function addApplicationToAdditionalShortcuts(applicationPath: string): void { if ( - shortcutCache[applicationPath] === undefined && - !additionalShortcuts.some((shortcut) => shortcut.target === applicationPath) + shortcutCache[applicationPath.toLowerCase()] === undefined && + !additionalShortcuts.some( + (shortcut) => shortcut.target.toLowerCase() === applicationPath.toLowerCase(), + ) ) { additionalShortcuts.push({ target: applicationPath, @@ -178,16 +190,17 @@ function resolveLinks(linkPaths: string[]): ShortcutDetails[] { // Removes all duplicate shortcuts. function removeDuplicates(shortcuts: ShortcutDetails[]): ShortcutDetails[] { const unique = shortcuts.reduce((shortcuts, shortcut) => { - if (shortcuts[shortcut.target]) { + const lowercaseTarget = shortcut.target.toLowerCase(); + if (shortcuts[lowercaseTarget]) { if ( - shortcuts[shortcut.target].args && - shortcuts[shortcut.target].args !== '' && + shortcuts[lowercaseTarget].args && + shortcuts[lowercaseTarget].args !== '' && (!shortcut.args || shortcut.args === '') ) { - shortcuts[shortcut.target] = shortcut; + shortcuts[lowercaseTarget] = shortcut; } } else { - shortcuts[shortcut.target] = shortcut; + shortcuts[lowercaseTarget] = shortcut; } return shortcuts; }, {} as Record<string, ShortcutDetails>); @@ -206,7 +219,7 @@ async function convertToSplitTunnelingApplication( } async function retrieveIcon(exe: string) { - const icon = await app.getFileIcon(exe); + const icon = await app.getFileIcon(exe, { size: 'large' }); return icon.toDataURL(); } |
