summaryrefslogtreecommitdiffhomepage
path: root/gui/src/main/linux-split-tunneling.ts
blob: 8103fc4d0e589a1d3af5f5e0208d4ff5ba86b2b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import argvSplit from 'argv-split';
import child_process from 'child_process';
import linuxAppList, { AppData } from 'linux-app-list';
import path from 'path';
import { pascalCaseToCamelCase } from './transform-object-keys';
import { ILinuxSplitTunnelingApplication } from '../shared/application-types';
import { findIconPath, getImageDataUrl, shouldShowApplication } from './linux-desktop-entry';

const PROBLEMATIC_APPLICATIONS = {
  launchingInExistingProcess: [
    'brave-browser-stable',
    'chromium-browser',
    'firefox',
    'firefox-esr',
    'google-chrome-stable',
    'mate-terminal',
    'opera',
    'xfce4-terminal',
  ],
  launchingElsewhere: ['gnome-terminal'],
};

export function launchApplication(app: ILinuxSplitTunnelingApplication | string) {
  const excludeArguments = typeof app === 'string' ? [app] : formatExec(app.exec);
  child_process.spawn('mullvad-exclude', excludeArguments, { detached: true });
}

// Removes placeholder arguments and separates command into list of strings
function formatExec(exec: string) {
  return argvSplit(exec).filter((argument: string) => !/%[cdDfFikmnNuUv]/.test(argument));
}

// TODO: Switch to asyncronous reading of .desktop files
export function getApplications(locale: string): Promise<ILinuxSplitTunnelingApplication[]> {
  const appList = linuxAppList();
  const applications = appList
    .list()
    .map((filename) => {
      const applications = localizeNameAndIcon(appList.data(filename), locale);
      return pascalCaseToCamelCase<ILinuxSplitTunnelingApplication>(applications);
    })
    .filter(shouldShowApplication)
    .map(addApplicationWarnings)
    .sort((a, b) => a.name.localeCompare(b.name))
    .map(replaceIconNameWithDataUrl);

  return Promise.all(applications);
}

async function replaceIconNameWithDataUrl(
  app: ILinuxSplitTunnelingApplication,
): Promise<ILinuxSplitTunnelingApplication> {
  try {
    // Either the app has no icon or it's already an absolute path.
    if (app.icon === undefined || path.isAbsolute(app.icon)) {
      return app;
    }

    const iconPath = await findIconPath(app.icon);
    if (iconPath === undefined) {
      return app;
    }

    return { ...app, icon: await getImageDataUrl(iconPath) };
  } catch (e) {
    return app;
  }
}

function addApplicationWarnings(
  application: ILinuxSplitTunnelingApplication,
): ILinuxSplitTunnelingApplication {
  const binaryBasename = path.basename(application.exec!.split(' ')[0]);
  if (PROBLEMATIC_APPLICATIONS.launchingInExistingProcess.includes(binaryBasename)) {
    return {
      ...application,
      warning: 'launches-in-existing-process',
    };
  } else if (PROBLEMATIC_APPLICATIONS.launchingElsewhere.includes(binaryBasename)) {
    return {
      ...application,
      warning: 'launches-elsewhere',
    };
  } else {
    return application;
  }
}

function localizeNameAndIcon(application: AppData, locale: string) {
  if (application.lang) {
    // linux-app-list prefixes and suffixes the locale keys with a space for some reason.
    const lang = Object.fromEntries(
      Object.entries(application.lang).map(([locale, value]) => [locale.trim(), value]),
    )[locale];

    application.Name = lang?.Name ?? application.Name;
    application.Icon = lang?.Icon ?? application.Icon;
  }

  return application;
}