summaryrefslogtreecommitdiffhomepage
path: root/gui/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/main')
-rw-r--r--gui/src/main/daemon-rpc.ts16
-rw-r--r--gui/src/main/gui-settings.ts2
-rw-r--r--gui/src/main/index.ts105
-rw-r--r--gui/src/main/notification-controller.ts3
-rw-r--r--gui/src/main/windows-split-tunneling.ts43
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();
}