summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/index.ts65
-rw-r--r--gui/src/main/windows-split-tunneling.ts976
-rw-r--r--gui/src/renderer/app.tsx29
-rw-r--r--gui/src/renderer/components/SplitTunnelingSettings.tsx56
-rw-r--r--gui/src/renderer/redux/settings/actions.ts6
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts4
-rw-r--r--gui/src/shared/application-types.ts36
-rw-r--r--gui/src/shared/ipc-schema.ts19
8 files changed, 636 insertions, 555 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 4cdb9f5727..185497f0bc 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -6,7 +6,10 @@ import util from 'util';
import config from '../config.json';
import { hasExpired } from '../shared/account-expiry';
-import { IWindowsApplication } from '../shared/application-types';
+import {
+ ISplitTunnelingApplication,
+ ISplitTunnelingAppListRetriever,
+} from '../shared/application-types';
import {
AccessMethodSetting,
DaemonEvent,
@@ -67,7 +70,7 @@ const execAsync = util.promisify(exec);
// 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 splitTunneling: ISplitTunnelingAppListRetriever | undefined = importSplitTunneling();
const ALLOWED_PERMISSIONS = ['clipboard-sanitized-write'];
@@ -110,7 +113,7 @@ class ApplicationMain
private rendererLog?: Logger;
private translations: ITranslations = { locale: this.locale };
- private windowsSplitTunnelingApplications?: IWindowsApplication[];
+ private splitTunnelingApplications?: ISplitTunnelingApplication[];
private macOsScrollbarVisibility?: MacOsScrollbarVisibility;
@@ -723,9 +726,7 @@ class ApplicationMain
IpcMainEventChannel.settings.notify?.(newSettings);
- if (windowsSplitTunneling) {
- void this.updateSplitTunnelingApplications(newSettings.splitTunnel.appsList);
- }
+ void this.updateSplitTunnelingApplications(newSettings.splitTunnel.appsList);
}
private setRelayList(relayList: IRelayListWithEndpointData) {
@@ -734,12 +735,12 @@ class ApplicationMain
}
private async updateSplitTunnelingApplications(appList: string[]): Promise<void> {
- const { applications } = await windowsSplitTunneling.getApplications({
- applicationPaths: appList,
- });
- this.windowsSplitTunnelingApplications = applications;
+ if (splitTunneling) {
+ const { applications } = await splitTunneling.getMetadataForApplications(appList);
+ this.splitTunnelingApplications = applications;
- IpcMainEventChannel.windowsSplitTunneling.notify?.(applications);
+ IpcMainEventChannel.splitTunneling.notify?.(applications);
+ }
}
private registerIpcListeners() {
@@ -758,7 +759,7 @@ class ApplicationMain
upgradeVersion: this.version.upgradeVersion,
guiSettings: this.settings.gui.state,
translations: this.translations,
- windowsSplitTunnelingApplications: this.windowsSplitTunnelingApplications,
+ splitTunnelingApplications: this.splitTunnelingApplications,
macOsScrollbarVisibility: this.macOsScrollbarVisibility,
changelog: this.changelog ?? [],
forceShowChanges: CommandLineOptions.showChanges.match,
@@ -789,38 +790,38 @@ class ApplicationMain
IpcMainEventChannel.linuxSplitTunneling.handleGetApplications(() => {
return linuxSplitTunneling.getApplications(this.locale);
});
- IpcMainEventChannel.windowsSplitTunneling.handleGetApplications((updateCaches: boolean) => {
- return windowsSplitTunneling.getApplications({ updateCaches });
+ IpcMainEventChannel.splitTunneling.handleGetApplications((updateCaches: boolean) => {
+ return splitTunneling!.getApplications(updateCaches);
});
IpcMainEventChannel.linuxSplitTunneling.handleLaunchApplication((application) => {
return linuxSplitTunneling.launchApplication(application);
});
- IpcMainEventChannel.windowsSplitTunneling.handleSetState((enabled) => {
+ IpcMainEventChannel.splitTunneling.handleSetState((enabled) => {
return this.daemonRpc.setSplitTunnelingState(enabled);
});
- IpcMainEventChannel.windowsSplitTunneling.handleAddApplication(async (application) => {
+ IpcMainEventChannel.splitTunneling.handleAddApplication(async (application) => {
// 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.settings.gui.addBrowsedForSplitTunnelingApplications(application);
- const applicationPath = await windowsSplitTunneling.addApplicationPathToCache(application);
- await this.daemonRpc.addSplitTunnelingApplication(applicationPath);
+ const executablePath = await splitTunneling!.resolveExecutablePath(application);
+ await splitTunneling!.addApplicationPathToCache(application);
+ await this.daemonRpc.addSplitTunnelingApplication(executablePath);
} else {
await this.daemonRpc.addSplitTunnelingApplication(application.absolutepath);
}
});
- IpcMainEventChannel.windowsSplitTunneling.handleRemoveApplication((application) => {
+ IpcMainEventChannel.splitTunneling.handleRemoveApplication((application) => {
return this.daemonRpc.removeSplitTunnelingApplication(
typeof application === 'string' ? application : application.absolutepath,
);
});
- IpcMainEventChannel.windowsSplitTunneling.handleForgetManuallyAddedApplication(
- (application) => {
- this.settings.gui.deleteBrowsedForSplitTunnelingApplications(application.absolutepath);
- return windowsSplitTunneling.removeApplicationFromCache(application);
- },
- );
+ IpcMainEventChannel.splitTunneling.handleForgetManuallyAddedApplication((application) => {
+ this.settings.gui.deleteBrowsedForSplitTunnelingApplications(application.absolutepath);
+ splitTunneling!.removeApplicationFromCache(application);
+ return Promise.resolve();
+ });
IpcMainEventChannel.app.handleQuit(() => this.disconnectAndQuit());
IpcMainEventChannel.app.handleOpenUrl(async (url) => {
@@ -851,10 +852,10 @@ class ApplicationMain
this.settings.registerIpcListeners();
this.account.registerIpcListeners();
- if (windowsSplitTunneling) {
- this.settings.gui.browsedForSplitTunnelingApplications.forEach(
- windowsSplitTunneling.addApplicationPathToCache,
- );
+ if (splitTunneling) {
+ this.settings.gui.browsedForSplitTunnelingApplications.forEach((application) => {
+ void splitTunneling.addApplicationPathToCache(application);
+ });
}
}
@@ -1104,6 +1105,12 @@ class ApplicationMain
/* eslint-enable @typescript-eslint/member-ordering */
}
+function importSplitTunneling() {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { WindowsSplitTunnelingAppListRetriever } = require('./windows-split-tunneling');
+ return new WindowsSplitTunnelingAppListRetriever();
+}
+
if (CommandLineOptions.help.match) {
console.log('Mullvad VPN');
console.log('Graphical interface for managing the Mullvad VPN daemon');
diff --git a/gui/src/main/windows-split-tunneling.ts b/gui/src/main/windows-split-tunneling.ts
index 844b74b77b..c74623a1f7 100644
--- a/gui/src/main/windows-split-tunneling.ts
+++ b/gui/src/main/windows-split-tunneling.ts
@@ -2,7 +2,10 @@ import { app, shell } from 'electron';
import fs from 'fs';
import path from 'path';
-import { IWindowsApplication } from '../shared/application-types';
+import {
+ ISplitTunnelingApplication,
+ ISplitTunnelingAppListRetriever,
+} from '../shared/application-types';
import log from '../shared/logging';
import {
ArrayValue,
@@ -57,581 +60,616 @@ const APPLICATION_ALLOW_LIST = [
'iexplore.exe',
];
-// Cache of all previously scanned shortcuts.
-const shortcutCache: Record<string, ShortcutDetails> = {};
-// Cache of all previously scanned applications.
-const applicationCache: Record<string, IWindowsApplication> = {};
-// List of shortcuts that have been added manually by the user.
-let additionalShortcuts: ShortcutDetails[] = [];
+export class WindowsSplitTunnelingAppListRetriever implements ISplitTunnelingAppListRetriever {
+ // Cache of all previously scanned shortcuts.
+ private shortcutCache: Record<string, ShortcutDetails> = {};
+ // Cache of all previously scanned applications.
+ private applicationCache: Record<string, ISplitTunnelingApplication> = {};
+ // List of shortcuts that have been added manually by the user.
+ private additionalShortcuts: ShortcutDetails[] = [];
-// Finds applications by searching through the startmenu for shortcuts with and exe-file as target.
-// If applicationPaths has a value, the returned applications are only the ones corresponding to
-// those paths.
-export async function getApplications(options: {
- applicationPaths?: string[];
- updateCaches?: boolean;
-}): Promise<{ fromCache: boolean; applications: IWindowsApplication[] }> {
- const cacheIsEmpty = Object.keys(shortcutCache).length === 0;
+ // Finds applications by searching through the startmenu for shortcuts with and exe-file as
+ // target.
+ public async getApplications(
+ updateCaches = false,
+ ): Promise<{ fromCache: boolean; applications: ISplitTunnelingApplication[] }> {
+ const cacheIsEmpty = Object.keys(this.shortcutCache).length === 0;
- if (options.updateCaches || cacheIsEmpty) {
- await updateShortcutCache();
- }
+ const fromCache = !updateCaches && !cacheIsEmpty;
+ if (!fromCache) {
+ await this.updateShortcutCache();
+ }
- // Add excluded apps that are missing from the shortcut cache to it
- if (options.applicationPaths) {
- await Promise.all(options.applicationPaths.map(addApplicationToAdditionalShortcuts));
+ await this.updateApplicationCache();
+
+ return {
+ fromCache,
+ applications: Object.values(this.applicationCache),
+ };
}
- await updateApplicationCache();
- // If applicationPaths is supplied the returnvalue should only contain the applications
- // corresponding to those paths.
- const applications = Object.values(applicationCache)
- .filter(
+ public async getMetadataForApplications(
+ applicationPaths: string[],
+ ): Promise<{ fromCache: boolean; applications: ISplitTunnelingApplication[] }> {
+ // Add excluded apps that are missing from the shortcut cache to it
+ await Promise.all(
+ applicationPaths.map((applicationPath) =>
+ this.addApplicationToAdditionalShortcuts(applicationPath),
+ ),
+ );
+
+ const applications = await this.getApplications();
+ // If applicationPaths is supplied the returnvalue should only contain the applications
+ // corresponding to those paths.
+ applications.applications = applications.applications.filter(
(application) =>
- options.applicationPaths === undefined ||
- options.applicationPaths.find(
+ applicationPaths.find(
(applicationPath) =>
applicationPath.toLowerCase() === application.absolutepath.toLowerCase(),
) !== undefined,
- )
- .sort((a, b) => a.name.localeCompare(b.name));
-
- return {
- fromCache: !options.updateCaches && !cacheIsEmpty,
- applications,
- };
-}
+ );
-// Adds either a shortcut or an executable to the additionalShortcuts list
-export async function addApplicationPathToCache(applicationPath: string): Promise<string> {
- const parsedPath = path.parse(applicationPath);
- if (parsedPath.ext === '.lnk') {
- const shortcutDetiails = shell.readShortcutLink(path.resolve(applicationPath));
- additionalShortcuts.push({
- ...shortcutDetiails,
- name: path.parse(applicationPath).name,
- deletable: true,
- });
- return shortcutDetiails.target;
- } else {
- await addApplicationToAdditionalShortcuts(applicationPath);
- return applicationPath;
+ return applications;
}
-}
-export function removeApplicationFromCache(application: IWindowsApplication): void {
- additionalShortcuts = additionalShortcuts.filter(
- (shortcut) => shortcut.target !== application.absolutepath,
- );
- delete applicationCache[application.absolutepath.toLowerCase()];
-}
+ public resolveExecutablePath(providedPath: string): Promise<string> {
+ if (path.extname(providedPath) === '.lnk') {
+ return Promise.resolve(shell.readShortcutLink(path.resolve(providedPath)).target);
+ }
-// Reads the start-menu directories and adds all shortcuts, targeting applications using networking,
-// to the shortcuts cache. Whether or not an application use networking is determined by checking for
-// "WS2_32.dll" in it's imports.
-async function updateShortcutCache(): Promise<void> {
- const links = await Promise.all(APPLICATION_PATHS.map(findAllLinks));
- const resolvedLinks = removeDuplicates(resolveLinks(links.flat()));
+ return Promise.resolve(providedPath);
+ }
- const shortcuts: ShortcutDetails[] = [];
- for (const shortcut of resolvedLinks) {
- if (
- APPLICATION_ALLOW_LIST.includes(path.basename(shortcut.target.toLowerCase())) ||
- (await importsDll(shortcut.target, 'WS2_32.dll'))
- ) {
- shortcuts.push(shortcut);
- shortcutCache[shortcut.target.toLowerCase()] = shortcut;
+ // Adds either a shortcut or an executable to the additionalShortcuts list
+ public async addApplicationPathToCache(applicationPath: string): Promise<void> {
+ const parsedPath = path.parse(applicationPath);
+ if (parsedPath.ext === '.lnk') {
+ const shortcutDetiails = shell.readShortcutLink(path.resolve(applicationPath));
+ this.additionalShortcuts.push({
+ ...shortcutDetiails,
+ name: path.parse(applicationPath).name,
+ deletable: true,
+ });
+ } else {
+ await this.addApplicationToAdditionalShortcuts(applicationPath);
}
}
-}
-async function updateApplicationCache(): Promise<void> {
- const shortcuts = Object.values(shortcutCache).concat(additionalShortcuts);
+ public removeApplicationFromCache(application: ISplitTunnelingApplication): void {
+ this.additionalShortcuts = this.additionalShortcuts.filter(
+ (shortcut) => shortcut.target !== application.absolutepath,
+ );
+ delete this.applicationCache[application.absolutepath.toLowerCase()];
+ }
- await Promise.all(
- shortcuts.map(async (shortcut) => {
- const lowercaseTarget = shortcut.target.toLowerCase();
- if (applicationCache[lowercaseTarget] === undefined) {
- applicationCache[lowercaseTarget] = await convertToSplitTunnelingApplication(shortcut);
+ // Reads the start-menu directories and adds all shortcuts, targeting applications using networking,
+ // to the shortcuts cache. Whether or not an application use networking is determined by checking for
+ // "WS2_32.dll" in it's imports.
+ private async updateShortcutCache(): Promise<void> {
+ const links = await Promise.all(
+ APPLICATION_PATHS.map((applicationPath) => this.findAllLinks(applicationPath)),
+ );
+ const resolvedLinks = this.removeDuplicates(this.resolveLinks(links.flat()));
+
+ const shortcuts: ShortcutDetails[] = [];
+ for (const shortcut of resolvedLinks) {
+ if (
+ APPLICATION_ALLOW_LIST.includes(path.basename(shortcut.target.toLowerCase())) ||
+ (await this.importsDll(shortcut.target, 'WS2_32.dll'))
+ ) {
+ shortcuts.push(shortcut);
+ this.shortcutCache[shortcut.target.toLowerCase()] = shortcut;
}
+ }
+ }
- return applicationCache[lowercaseTarget];
- }),
- );
-}
+ private async updateApplicationCache(): Promise<void> {
+ const shortcuts = Object.values(this.shortcutCache).concat(this.additionalShortcuts);
-// Add excluded apps that are missing from the shortcut cache to it
-async function addApplicationToAdditionalShortcuts(applicationPath: string): Promise<void> {
- if (
- shortcutCache[applicationPath.toLowerCase()] === undefined &&
- !additionalShortcuts.some(
- (shortcut) => shortcut.target.toLowerCase() === applicationPath.toLowerCase(),
- )
- ) {
- additionalShortcuts.push({
- target: applicationPath,
- name: (await getProgramName(applicationPath)) ?? path.parse(applicationPath).name,
- deletable: true,
- });
+ await Promise.all(
+ shortcuts.map(async (shortcut) => {
+ const lowercaseTarget = shortcut.target.toLowerCase();
+ if (this.applicationCache[lowercaseTarget] === undefined) {
+ this.applicationCache[lowercaseTarget] = await this.convertToSplitTunnelingApplication(
+ shortcut,
+ );
+ }
+
+ return this.applicationCache[lowercaseTarget];
+ }),
+ );
}
-}
-// Fins all links in a directory.
-async function findAllLinks(path: string): Promise<string[]> {
- if (path.endsWith('.lnk')) {
- return [path];
- } else {
- const stat = await fs.promises.stat(path);
- if (stat.isDirectory()) {
- const contents = await fs.promises.readdir(path);
- const result = await Promise.all(contents.map((item) => findAllLinks(`${path}/${item}`)));
- return result.flat();
- } else {
- return [];
+ // Add excluded apps that are missing from the shortcut cache to it
+ private async addApplicationToAdditionalShortcuts(applicationPath: string): Promise<void> {
+ if (
+ this.shortcutCache[applicationPath.toLowerCase()] === undefined &&
+ !this.additionalShortcuts.some(
+ (shortcut) => shortcut.target.toLowerCase() === applicationPath.toLowerCase(),
+ )
+ ) {
+ this.additionalShortcuts.push({
+ target: applicationPath,
+ name: (await this.getProgramName(applicationPath)) ?? path.parse(applicationPath).name,
+ deletable: true,
+ });
}
}
-}
-function resolveLinks(linkPaths: string[]): ShortcutDetails[] {
- return linkPaths
- .map((link) => {
- try {
- return {
- ...shell.readShortcutLink(path.resolve(link)),
- name: path.parse(link).name,
- };
- } catch {
- return null;
+ // Fins all links in a directory.
+ private async findAllLinks(path: string): Promise<string[]> {
+ if (path.endsWith('.lnk')) {
+ return [path];
+ } else {
+ const stat = await fs.promises.stat(path);
+ if (stat.isDirectory()) {
+ const contents = await fs.promises.readdir(path);
+ const result = await Promise.all(
+ contents.map((item) => this.findAllLinks(`${path}/${item}`)),
+ );
+ return result.flat();
+ } else {
+ return [];
}
- })
- .filter(
- (shortcut): shortcut is ShortcutDetails =>
- shortcut !== null &&
- !shortcut.target.endsWith('Mullvad VPN.exe') &&
- shortcut.target.endsWith('.exe') &&
- !shortcut.target.toLowerCase().includes('install') && // Covers "uninstall" as well.
- !shortcut.name.toLowerCase().includes('install'),
- );
-}
+ }
+ }
-async function getProgramName(exePath: string): Promise<string | undefined> {
- try {
- return await getProductName(exePath);
- } catch {
- return undefined;
+ private resolveLinks(linkPaths: string[]): ShortcutDetails[] {
+ return linkPaths
+ .map((link) => {
+ try {
+ return {
+ ...shell.readShortcutLink(path.resolve(link)),
+ name: path.parse(link).name,
+ };
+ } catch {
+ return null;
+ }
+ })
+ .filter(
+ (shortcut): shortcut is ShortcutDetails =>
+ shortcut !== null &&
+ !shortcut.target.endsWith('Mullvad VPN.exe') &&
+ shortcut.target.endsWith('.exe') &&
+ !shortcut.target.toLowerCase().includes('install') && // Covers "uninstall" as well.
+ !shortcut.name.toLowerCase().includes('install'),
+ );
}
-}
-// Removes all duplicate shortcuts.
-function removeDuplicates(shortcuts: ShortcutDetails[]): ShortcutDetails[] {
- const unique = shortcuts.reduce((shortcuts, shortcut) => {
- const lowercaseTarget = shortcut.target.toLowerCase();
- if (shortcuts[lowercaseTarget]) {
- if (
- shortcuts[lowercaseTarget].args &&
- shortcuts[lowercaseTarget].args !== '' &&
- (!shortcut.args || shortcut.args === '')
- ) {
- shortcuts[lowercaseTarget] = shortcut;
- }
- } else {
- shortcuts[lowercaseTarget] = shortcut;
+ private async getProgramName(exePath: string): Promise<string | undefined> {
+ try {
+ return await this.getProductName(exePath);
+ } catch {
+ return undefined;
}
- return shortcuts;
- }, {} as Record<string, ShortcutDetails>);
+ }
- return Object.values(unique);
-}
+ // Removes all duplicate shortcuts.
+ private removeDuplicates(shortcuts: ShortcutDetails[]): ShortcutDetails[] {
+ const unique = shortcuts.reduce((shortcuts, shortcut) => {
+ const lowercaseTarget = shortcut.target.toLowerCase();
+ if (shortcuts[lowercaseTarget]) {
+ if (
+ shortcuts[lowercaseTarget].args &&
+ shortcuts[lowercaseTarget].args !== '' &&
+ (!shortcut.args || shortcut.args === '')
+ ) {
+ shortcuts[lowercaseTarget] = shortcut;
+ }
+ } else {
+ shortcuts[lowercaseTarget] = shortcut;
+ }
+ return shortcuts;
+ }, {} as Record<string, ShortcutDetails>);
-async function convertToSplitTunnelingApplication(
- shortcut: ShortcutDetails,
-): Promise<IWindowsApplication> {
- return {
- absolutepath: shortcut.target,
- name: shortcut.name,
- icon: await retrieveIcon(shortcut.target),
- deletable: shortcut.deletable,
- };
-}
+ return Object.values(unique);
+ }
-async function retrieveIcon(exe: string) {
- const icon = await app.getFileIcon(exe, { size: 'large' });
- return icon.toDataURL();
-}
+ private async convertToSplitTunnelingApplication(
+ shortcut: ShortcutDetails,
+ ): Promise<ISplitTunnelingApplication> {
+ return {
+ absolutepath: shortcut.target,
+ name: shortcut.name,
+ icon: await this.retrieveIcon(shortcut.target),
+ deletable: shortcut.deletable,
+ };
+ }
-// Checks if the application at the supplied path imports a specific dll.
-async function importsDll(path: string, dllName: string): Promise<boolean> {
- let fileHandle: fs.promises.FileHandle;
- try {
- fileHandle = await fs.promises.open(path, fs.constants.O_RDONLY);
- } catch (e) {
- return false;
+ private async retrieveIcon(exe: string) {
+ const icon = await app.getFileIcon(exe, { size: 'large' });
+ return icon.toDataURL();
}
- const imports = await getExeImports(fileHandle, path);
- await fileHandle.close();
- return imports.map((name) => name.toLowerCase()).includes(dllName.toLowerCase());
-}
+ // Checks if the application at the supplied path imports a specific dll.
+ private async importsDll(path: string, dllName: string): Promise<boolean> {
+ let fileHandle: fs.promises.FileHandle;
+ try {
+ fileHandle = await fs.promises.open(path, fs.constants.O_RDONLY);
+ } catch (e) {
+ return false;
+ }
-async function getExeImports(fileHandle: fs.promises.FileHandle, path: string): Promise<string[]> {
- try {
- const tableOffsetResult = await getTableOffset(fileHandle, IMAGE_DIRECTORY_ENTRY_IMPORT);
- if (tableOffsetResult) {
- const { offset: importTableOffset, rvaToOffset } = tableOffsetResult;
- const moduleNames = await getImportModuleNames(fileHandle, importTableOffset, rvaToOffset);
- return moduleNames;
- } else {
+ const imports = await this.getExeImports(fileHandle, path);
+ await fileHandle.close();
+ return imports.map((name) => name.toLowerCase()).includes(dllName.toLowerCase());
+ }
+
+ private async getExeImports(fileHandle: fs.promises.FileHandle, path: string): Promise<string[]> {
+ try {
+ const tableOffsetResult = await this.getTableOffset(fileHandle, IMAGE_DIRECTORY_ENTRY_IMPORT);
+ if (tableOffsetResult) {
+ const { offset: importTableOffset, rvaToOffset } = tableOffsetResult;
+ const moduleNames = await this.getImportModuleNames(
+ fileHandle,
+ importTableOffset,
+ rvaToOffset,
+ );
+ return moduleNames;
+ } else {
+ return [];
+ }
+ } catch (e) {
+ log.error(`Failed to read .exe import table for ${path}.`, e);
return [];
}
- } catch (e) {
- log.error(`Failed to read .exe import table for ${path}.`, e);
- return [];
}
-}
-async function readString(
- fileHandle: fs.promises.FileHandle,
- offset: number,
- encoding: 'ascii' | 'ucs2',
-): Promise<{ value: string; endOffset: number }> {
- const characterSize = getCharacterSize(encoding);
- const buffer = Buffer.alloc(characterSize);
- await fileHandle.read(buffer, 0, characterSize, offset);
+ private async readString(
+ fileHandle: fs.promises.FileHandle,
+ offset: number,
+ encoding: 'ascii' | 'ucs2',
+ ): Promise<{ value: string; endOffset: number }> {
+ const characterSize = this.getCharacterSize(encoding);
+ const buffer = Buffer.alloc(characterSize);
+ await fileHandle.read(buffer, 0, characterSize, offset);
- const nextOffset = offset + characterSize;
- if (buffer.every((value) => value === 0)) {
- return { value: '', endOffset: nextOffset };
- } else {
- const { value: nextValue, endOffset } = await readString(fileHandle, nextOffset, encoding);
- const value = buffer.toString(encoding) + nextValue;
- return { value, endOffset };
+ const nextOffset = offset + characterSize;
+ if (buffer.every((value) => value === 0)) {
+ return { value: '', endOffset: nextOffset };
+ } else {
+ const { value: nextValue, endOffset } = await this.readString(
+ fileHandle,
+ nextOffset,
+ encoding,
+ );
+ const value = buffer.toString(encoding) + nextValue;
+ return { value, endOffset };
+ }
}
-}
-function getCharacterSize(encoding: 'ascii' | 'ucs2'): number {
- switch (encoding) {
- case 'ascii':
- return 1;
- case 'ucs2':
- return 2;
+ private getCharacterSize(encoding: 'ascii' | 'ucs2'): number {
+ switch (encoding) {
+ case 'ascii':
+ return 1;
+ case 'ucs2':
+ return 2;
+ }
}
-}
-// Finds and returns the NT header.
-async function getNtHeader(
- fileHandle: fs.promises.FileHandle,
-): Promise<StructValue<ImageNtHeadersUnion>> {
- // Check whether or not the file follows the PE format.
- const dosHeader = await Value.fromFile(fileHandle, 0, DOS_HEADER);
- const eMagic = dosHeader.get<PrimitiveValue>('e_magic').value();
- if (eMagic !== 0x5a4d) {
- throw new Error('Not a PE file');
- }
+ // Finds and returns the NT header.
+ private async getNtHeader(
+ fileHandle: fs.promises.FileHandle,
+ ): Promise<StructValue<ImageNtHeadersUnion>> {
+ // Check whether or not the file follows the PE format.
+ const dosHeader = await Value.fromFile(fileHandle, 0, DOS_HEADER);
+ const eMagic = dosHeader.get<PrimitiveValue>('e_magic').value();
+ if (eMagic !== 0x5a4d) {
+ throw new Error('Not a PE file');
+ }
- const ntHeaderOffset = dosHeader.get<PrimitiveValue>('e_lfanew').value();
+ const ntHeaderOffset = dosHeader.get<PrimitiveValue>('e_lfanew').value();
- // Check if this is a 32- or 64-bit exe-file and return the correct datatype.
- const ntHeader32 = await Value.fromFile(fileHandle, ntHeaderOffset, IMAGE_NT_HEADERS);
- const signature = ntHeader32.get<PrimitiveValue>('Signature').buffer.toString('ascii');
- if (signature !== 'PE\0\0') {
- throw new Error('Not a PE file');
- }
+ // Check if this is a 32- or 64-bit exe-file and return the correct datatype.
+ const ntHeader32 = await Value.fromFile(fileHandle, ntHeaderOffset, IMAGE_NT_HEADERS);
+ const signature = ntHeader32.get<PrimitiveValue>('Signature').buffer.toString('ascii');
+ if (signature !== 'PE\0\0') {
+ throw new Error('Not a PE file');
+ }
- const magic = ntHeader32
- .get<StructValue<typeof IMAGE_OPTIONAL_HEADER32>>('OptionalHeader')
- .get<PrimitiveValue>('Magic')
- .value();
+ const magic = ntHeader32
+ .get<StructValue<typeof IMAGE_OPTIONAL_HEADER32>>('OptionalHeader')
+ .get<PrimitiveValue>('Magic')
+ .value();
- // magic is 0x20b for 64-bit executables.
- return magic === 0x20b
- ? Value.fromFile(fileHandle, ntHeaderOffset, IMAGE_NT_HEADERS64)
- : ntHeader32;
-}
+ // magic is 0x20b for 64-bit executables.
+ return magic === 0x20b
+ ? Value.fromFile(fileHandle, ntHeaderOffset, IMAGE_NT_HEADERS64)
+ : ntHeader32;
+ }
-// Reads the import table and returns a list of the imported DLLs.
-async function getImportModuleNames(
- fileHandle: fs.promises.FileHandle,
- importTableOffset: number,
- rvaToOffset: RvaToOffset,
-): Promise<string[]> {
- const moduleNames: string[] = [];
- const entrySize = Value.sizeOf(IMAGE_IMPORT_MODULE_DIRECTORY);
+ // Reads the import table and returns a list of the imported DLLs.
+ private async getImportModuleNames(
+ fileHandle: fs.promises.FileHandle,
+ importTableOffset: number,
+ rvaToOffset: RvaToOffset,
+ ): Promise<string[]> {
+ const moduleNames: string[] = [];
+ const entrySize = Value.sizeOf(IMAGE_IMPORT_MODULE_DIRECTORY);
- // eslint-disable-next-line no-constant-condition
- for (let i = 0; true; i++) {
- const importEntry = await Value.fromFile(
- fileHandle,
- importTableOffset + i * entrySize,
- IMAGE_IMPORT_MODULE_DIRECTORY,
- );
- const nameRva = importEntry.get('ModuleName').value();
+ // eslint-disable-next-line no-constant-condition
+ for (let i = 0; true; i++) {
+ const importEntry = await Value.fromFile(
+ fileHandle,
+ importTableOffset + i * entrySize,
+ IMAGE_IMPORT_MODULE_DIRECTORY,
+ );
+ const nameRva = importEntry.get('ModuleName').value();
- if (nameRva !== 0x0) {
- const offset = await rvaToOffset(nameRva);
+ if (nameRva !== 0x0) {
+ const offset = await rvaToOffset(nameRva);
- const { value: name } = await readString(fileHandle, offset, 'ascii');
- moduleNames.push(name);
- } else {
- return moduleNames;
+ const { value: name } = await this.readString(fileHandle, offset, 'ascii');
+ moduleNames.push(name);
+ } else {
+ return moduleNames;
+ }
}
}
-}
-
-async function getProductName(path: string): Promise<string | undefined> {
- let fileHandle: fs.promises.FileHandle;
- try {
- fileHandle = await fs.promises.open(path, fs.constants.O_RDONLY);
- } catch {
- return undefined;
- }
- try {
- const getTableOffsetResult = await getTableOffset(fileHandle, IMAGE_DIRECTORY_ENTRY_RESOURCE);
+ private async getProductName(path: string): Promise<string | undefined> {
+ let fileHandle: fs.promises.FileHandle;
+ try {
+ fileHandle = await fs.promises.open(path, fs.constants.O_RDONLY);
+ } catch {
+ return undefined;
+ }
- if (getTableOffsetResult) {
- const { offset: resourceTableOffset, rvaToOffset } = getTableOffsetResult;
- const leafOffsets = await getResourceTreeLeafOffsets(
+ try {
+ const getTableOffsetResult = await this.getTableOffset(
fileHandle,
- resourceTableOffset,
- resourceTableOffset,
- rvaToOffset,
- [[16], [1], [0, 1033]],
+ IMAGE_DIRECTORY_ENTRY_RESOURCE,
);
- const productName = await leafOffsets.reduce(async (alreadyFoundValue, leafOffset) => {
- const value = await alreadyFoundValue;
- if (value) {
- return value;
- } else {
- const strings = await getVsVersionInfoStrings(fileHandle, leafOffset);
- return strings.get('FileDescription') ?? strings.get('ProductName');
- }
- }, Promise.resolve() as Promise<string | undefined>);
+ if (getTableOffsetResult) {
+ const { offset: resourceTableOffset, rvaToOffset } = getTableOffsetResult;
+ const leafOffsets = await this.getResourceTreeLeafOffsets(
+ fileHandle,
+ resourceTableOffset,
+ resourceTableOffset,
+ rvaToOffset,
+ [[16], [1], [0, 1033]],
+ );
- return productName;
- } else {
+ const productName = await leafOffsets.reduce(async (alreadyFoundValue, leafOffset) => {
+ const value = await alreadyFoundValue;
+ if (value) {
+ return value;
+ } else {
+ const strings = await this.getVsVersionInfoStrings(fileHandle, leafOffset);
+ return strings.get('FileDescription') ?? strings.get('ProductName');
+ }
+ }, Promise.resolve() as Promise<string | undefined>);
+
+ return productName;
+ } else {
+ return undefined;
+ }
+ } catch {
return undefined;
+ } finally {
+ await fileHandle.close();
}
- } catch {
- return undefined;
- } finally {
- await fileHandle.close();
}
-}
-async function getTableOffset(
- fileHandle: fs.promises.FileHandle,
- tableIndex: number,
-): Promise<{ offset: number; rvaToOffset: RvaToOffset } | undefined> {
- const ntHeader = await getNtHeader(fileHandle);
- const fileHeader = ntHeader.get<StructValue<typeof IMAGE_FILE_HEADER>>('FileHeader');
- const optionalHeader = ntHeader.get<StructValue<ImageOptionalHeaderUnion>>('OptionalHeader');
+ private async getTableOffset(
+ fileHandle: fs.promises.FileHandle,
+ tableIndex: number,
+ ): Promise<{ offset: number; rvaToOffset: RvaToOffset } | undefined> {
+ const ntHeader = await this.getNtHeader(fileHandle);
+ const fileHeader = ntHeader.get<StructValue<typeof IMAGE_FILE_HEADER>>('FileHeader');
+ const optionalHeader = ntHeader.get<StructValue<ImageOptionalHeaderUnion>>('OptionalHeader');
+
+ const tableRva = optionalHeader
+ .get<ArrayValue<typeof IMAGE_DATA_DIRECTORY>>('DataDirectory')
+ .nth(tableIndex)
+ .get('VirtualAddress')
+ .value();
+
+ if (tableRva === 0x0) {
+ return undefined;
+ }
- const tableRva = optionalHeader
- .get<ArrayValue<typeof IMAGE_DATA_DIRECTORY>>('DataDirectory')
- .nth(tableIndex)
- .get('VirtualAddress')
- .value();
+ const numberOfSections = fileHeader.get<PrimitiveValue>('NumberOfSections').value();
+ const ntHeaderEndOffset =
+ ntHeader.offset +
+ ntHeader.get<PrimitiveValue<typeof DWORD>>('Signature').size +
+ fileHeader.size +
+ fileHeader.get<PrimitiveValue>('SizeOfOptionalHeader').value();
- if (tableRva === 0x0) {
- return undefined;
+ const rvaToOffset = (rva: number) =>
+ rvaToOffsetImpl(fileHandle, rva, numberOfSections, ntHeaderEndOffset);
+
+ const tableOffset = await rvaToOffset(tableRva);
+
+ return { offset: tableOffset, rvaToOffset };
}
- const numberOfSections = fileHeader.get<PrimitiveValue>('NumberOfSections').value();
- const ntHeaderEndOffset =
- ntHeader.offset +
- ntHeader.get<PrimitiveValue<typeof DWORD>>('Signature').size +
- fileHeader.size +
- fileHeader.get<PrimitiveValue>('SizeOfOptionalHeader').value();
+ // Searches the resource tree for the supplied paths and returns the leaves at the end of those
+ // paths.
+ private async getResourceTreeLeafOffsets(
+ fileHandle: fs.promises.FileHandle,
+ sectionOffset: number,
+ tableOffset: number,
+ rvaToOffset: (rva: number) => Promise<number>,
+ [ids, ...path]: number[][],
+ ): Promise<number[]> {
+ const table = await Value.fromFile(fileHandle, tableOffset, IMAGE_RESOURCE_DIRECTORY);
- const rvaToOffset = (rva: number) =>
- rvaToOffsetImpl(fileHandle, rva, numberOfSections, ntHeaderEndOffset);
+ const numberOfNameEntries = table.get('NumberOfNameEntries').value();
+ const numberOfIdEntries = table.get('NumberOfIdEntries').value();
- const tableOffset = await rvaToOffset(tableRva);
+ const leaves: number[] = [];
- return { offset: tableOffset, rvaToOffset };
-}
+ for (let i = numberOfNameEntries; i < numberOfNameEntries + numberOfIdEntries; i++) {
+ const offset =
+ tableOffset +
+ Value.sizeOf(IMAGE_RESOURCE_DIRECTORY) +
+ i * Value.sizeOf(IMAGE_RESOURCE_DIRECTORY_ID_ENTRY);
+ const entry = await Value.fromFile(fileHandle, offset, IMAGE_RESOURCE_DIRECTORY_ID_ENTRY);
+
+ const id = entry.get('Id').value();
+ if (!ids.includes(id)) {
+ continue;
+ }
-// Searches the resource tree for the supplied paths and returns the leaves at the end of those
-// paths.
-async function getResourceTreeLeafOffsets(
- fileHandle: fs.promises.FileHandle,
- sectionOffset: number,
- tableOffset: number,
- rvaToOffset: (rva: number) => Promise<number>,
- [ids, ...path]: number[][],
-): Promise<number[]> {
- const table = await Value.fromFile(fileHandle, tableOffset, IMAGE_RESOURCE_DIRECTORY);
+ let offsetToData = entry.get('OffsetToData').value();
+ // If the first bit is 1 then the offset points to another node, otherwise it point to a leaf.
+ const isLeaf = (offsetToData & 0x80000000) === 0;
- const numberOfNameEntries = table.get('NumberOfNameEntries').value();
- const numberOfIdEntries = table.get('NumberOfIdEntries').value();
+ if (isLeaf && path.length === 0) {
+ const leafDataOffset = await this.getResourceTreeLeafValueOffset(
+ fileHandle,
+ sectionOffset + offsetToData,
+ rvaToOffset,
+ );
- const leaves: number[] = [];
+ leaves.push(leafDataOffset);
+ } else if (!isLeaf) {
+ offsetToData &= 0x7fffffff;
- for (let i = numberOfNameEntries; i < numberOfNameEntries + numberOfIdEntries; i++) {
- const offset =
- tableOffset +
- Value.sizeOf(IMAGE_RESOURCE_DIRECTORY) +
- i * Value.sizeOf(IMAGE_RESOURCE_DIRECTORY_ID_ENTRY);
- const entry = await Value.fromFile(fileHandle, offset, IMAGE_RESOURCE_DIRECTORY_ID_ENTRY);
+ const subTreeLeaves = await this.getResourceTreeLeafOffsets(
+ fileHandle,
+ sectionOffset,
+ sectionOffset + offsetToData,
+ rvaToOffset,
+ path,
+ );
- const id = entry.get('Id').value();
- if (!ids.includes(id)) {
- continue;
+ leaves.push(...subTreeLeaves);
+ } else {
+ continue;
+ }
}
- let offsetToData = entry.get('OffsetToData').value();
- // If the first bit is 1 then the offset points to another node, otherwise it point to a leaf.
- const isLeaf = (offsetToData & 0x80000000) === 0;
+ return leaves;
+ }
- if (isLeaf && path.length === 0) {
- const leafDataOffset = await getResourceTreeLeafValueOffset(
+ // Finds the Strings structures within the VS_VERSIONINFO structure and returns the contents.
+ private async getVsVersionInfoStrings(
+ fileHandle: fs.promises.FileHandle,
+ offset: number,
+ ): Promise<Map<string, string>> {
+ try {
+ const stringFileInfoOffset = await this.getVsVersionInfoChildrenOffset(fileHandle, offset);
+
+ const stringTableOffset = await this.getChildrenOffset(
fileHandle,
- sectionOffset + offsetToData,
- rvaToOffset,
+ stringFileInfoOffset,
+ STRING_FILE_INFO,
+ (szKey) => szKey === 'StringFileInfo',
);
+ const stringTable = await Value.fromFile(fileHandle, stringTableOffset, STRING_TABLE);
+ const stringTableLength = stringTable.get<PrimitiveValue>('wLength').value();
- leaves.push(leafDataOffset);
- } else if (!isLeaf) {
- offsetToData &= 0x7fffffff;
+ const stringsOffset = await this.getChildrenOffset(
+ fileHandle,
+ stringTableOffset,
+ STRING_TABLE,
+ (szKey) => szKey.substring(4).toLowerCase() === '04b0',
+ );
- const subTreeLeaves = await getResourceTreeLeafOffsets(
+ const strings = await this.parseStrings(
fileHandle,
- sectionOffset,
- sectionOffset + offsetToData,
- rvaToOffset,
- path,
+ stringsOffset,
+ stringTableOffset + stringTableLength,
);
- leaves.push(...subTreeLeaves);
- } else {
- continue;
+ return strings;
+ } catch {
+ return new Map();
}
}
- return leaves;
-}
+ // Loops through the list of strings and returns a map with the contents.
+ private async parseStrings(
+ fileHandle: fs.promises.FileHandle,
+ stringsOffset: number,
+ stringTableEnd: number,
+ ): Promise<Map<string, string>> {
+ const strings = new Map<string, string>();
+
+ let currentStringOffset = stringsOffset;
+ while (currentStringOffset < stringTableEnd) {
+ const stringValue = await Value.fromFile(
+ fileHandle,
+ currentStringOffset,
+ STRING_TABLE_STRING,
+ );
+ const structSize = stringValue.get('wLength').value();
+ const valueSize = (stringValue.get('wValueLength').value() - 1) * 2;
-// Finds the Strings structures within the VS_VERSIONINFO structure and returns the contents.
-async function getVsVersionInfoStrings(
- fileHandle: fs.promises.FileHandle,
- offset: number,
-): Promise<Map<string, string>> {
- try {
- const stringFileInfoOffset = await getVsVersionInfoChildrenOffset(fileHandle, offset);
+ const szKeyOffset = currentStringOffset + stringValue.size;
+ const { value: szKey, endOffset } = await this.readString(fileHandle, szKeyOffset, 'ucs2');
- const stringTableOffset = await getChildrenOffset(
- fileHandle,
- stringFileInfoOffset,
- STRING_FILE_INFO,
- (szKey) => szKey === 'StringFileInfo',
- );
- const stringTable = await Value.fromFile(fileHandle, stringTableOffset, STRING_TABLE);
- const stringTableLength = stringTable.get<PrimitiveValue>('wLength').value();
+ const valueOffset = this.alignDword(endOffset);
+ // Some programs specify the value size in bytes instead of words resulting in reading double
+ // the length. To make sure we don't read beyond the end offset we calculate the max size to
+ // read. The last value is the null termination character.
+ const calculatedValueMaxSize = structSize - (valueOffset - currentStringOffset) - 2;
+ const valueReadSize = Math.min(valueSize, calculatedValueMaxSize);
- const stringsOffset = await getChildrenOffset(
- fileHandle,
- stringTableOffset,
- STRING_TABLE,
- (szKey) => szKey.substr(4).toLowerCase() === '04b0',
- );
+ const { buffer } = await fileHandle.read(
+ Buffer.alloc(valueReadSize),
+ 0,
+ valueReadSize,
+ valueOffset,
+ );
+ const value = buffer.toString('ucs2');
- const strings = await parseStrings(
- fileHandle,
- stringsOffset,
- stringTableOffset + stringTableLength,
- );
+ strings.set(szKey, value);
+ currentStringOffset += this.alignDword(stringValue.get<PrimitiveValue>('wLength').value());
+ }
return strings;
- } catch {
- return new Map();
}
-}
-
-// Loops through the list of strings and returns a map with the contents.
-async function parseStrings(
- fileHandle: fs.promises.FileHandle,
- stringsOffset: number,
- stringTableEnd: number,
-): Promise<Map<string, string>> {
- const strings = new Map<string, string>();
- let currentStringOffset = stringsOffset;
- while (currentStringOffset < stringTableEnd) {
- const stringValue = await Value.fromFile(fileHandle, currentStringOffset, STRING_TABLE_STRING);
- const structSize = stringValue.get('wLength').value();
- const valueSize = (stringValue.get('wValueLength').value() - 1) * 2;
+ private async getResourceTreeLeafValueOffset(
+ fileHandle: fs.promises.FileHandle,
+ offset: number,
+ rvaToOffset: (rva: number) => Promise<number>,
+ ): Promise<number> {
+ const leaf = await Value.fromFile(fileHandle, offset, IMAGE_RESOURCE_DIRECTORY_DATA_ENTRY);
+ const valueRva = leaf.get<PrimitiveValue>('DataRVA').value();
+ const valueOffset = await rvaToOffset(valueRva);
- const szKeyOffset = currentStringOffset + stringValue.size;
- const { value: szKey, endOffset } = await readString(fileHandle, szKeyOffset, 'ucs2');
-
- const valueOffset = alignDword(endOffset);
- // Some programs specify the value size in bytes instead of words resulting in reading double
- // the length. To make sure we don't read beyond the end offset we calculate the max size to
- // read. The last value is the null termination character.
- const calculatedValueMaxSize = structSize - (valueOffset - currentStringOffset) - 2;
- const valueReadSize = Math.min(valueSize, calculatedValueMaxSize);
+ return valueOffset;
+ }
- const { buffer } = await fileHandle.read(
- Buffer.alloc(valueReadSize),
- 0,
- valueReadSize,
- valueOffset,
+ // Finds the offset to the Children field in the VS_VERSIONINFO structure.
+ private async getVsVersionInfoChildrenOffset(fileHandle: fs.promises.FileHandle, offset: number) {
+ const valueValueOffset = await this.getChildrenOffset(
+ fileHandle,
+ offset,
+ VS_VERSIONINFO,
+ (szKey) => szKey === 'VS_VERSION_INFO',
);
- const value = buffer.toString('ucs2');
+ const versionInfo = await Value.fromFile(fileHandle, offset, VS_VERSIONINFO);
+ const versionInfoValueLength = versionInfo.get<PrimitiveValue>('wValueLength').value();
+ const valuePadding2Offset = valueValueOffset + versionInfoValueLength;
+ const valueChildrenOffset = this.alignDword(valuePadding2Offset);
- strings.set(szKey, value);
- currentStringOffset += alignDword(stringValue.get<PrimitiveValue>('wLength').value());
+ return valueChildrenOffset;
}
- return strings;
-}
-
-async function getResourceTreeLeafValueOffset(
- fileHandle: fs.promises.FileHandle,
- offset: number,
- rvaToOffset: (rva: number) => Promise<number>,
-): Promise<number> {
- const leaf = await Value.fromFile(fileHandle, offset, IMAGE_RESOURCE_DIRECTORY_DATA_ENTRY);
- const valueRva = leaf.get<PrimitiveValue>('DataRVA').value();
- const valueOffset = await rvaToOffset(valueRva);
-
- return valueOffset;
-}
-
-// Finds the offset to the Children field in the VS_VERSIONINFO structure.
-async function getVsVersionInfoChildrenOffset(fileHandle: fs.promises.FileHandle, offset: number) {
- const valueValueOffset = await getChildrenOffset(
- fileHandle,
- offset,
- VS_VERSIONINFO,
- (szKey) => szKey === 'VS_VERSION_INFO',
- );
- const versionInfo = await Value.fromFile(fileHandle, offset, VS_VERSIONINFO);
- const versionInfoValueLength = versionInfo.get<PrimitiveValue>('wValueLength').value();
- const valuePadding2Offset = valueValueOffset + versionInfoValueLength;
- const valueChildrenOffset = alignDword(valuePadding2Offset);
-
- return valueChildrenOffset;
-}
+ // Finds the offset to the Children field in any of the STRING_FILE_INFO, STRING_TABLE and
+ // STRING_TABLE_STRING structures.
+ private async getChildrenOffset(
+ fileHandle: fs.promises.FileHandle,
+ offset: number,
+ datatype: StructWrapper,
+ validateSzKey?: (szKey: string) => boolean,
+ ) {
+ const szKeyOffset = offset + Value.sizeOf(datatype);
+ const { value, endOffset } = await this.readString(fileHandle, szKeyOffset, 'ucs2');
+ if (validateSzKey && !validateSzKey(value)) {
+ throw new Error(`Invalid szKey "${value}"`);
+ }
-// Finds the offset to the Children field in any of the STRING_FILE_INFO, STRING_TABLE and
-// STRING_TABLE_STRING structures.
-async function getChildrenOffset(
- fileHandle: fs.promises.FileHandle,
- offset: number,
- datatype: StructWrapper,
- validateSzKey?: (szKey: string) => boolean,
-) {
- const szKeyOffset = offset + Value.sizeOf(datatype);
- const { value, endOffset } = await readString(fileHandle, szKeyOffset, 'ucs2');
- if (validateSzKey && !validateSzKey(value)) {
- throw new Error(`Invalid szKey "${value}"`);
+ return this.alignDword(endOffset);
}
- return alignDword(endOffset);
-}
-
-function alignDword(offset: number): number {
- return Math.ceil(offset / 4) * 4;
+ private alignDword(offset: number): number {
+ return Math.ceil(offset / 4) * 4;
+ }
}
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 92c4cb5698..95f7e3d0cf 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -4,7 +4,10 @@ import { bindActionCreators } from 'redux';
import { StyleSheetManager } from 'styled-components';
import { closeToExpiry, hasExpired } from '../shared/account-expiry';
-import { ILinuxSplitTunnelingApplication, IWindowsApplication } from '../shared/application-types';
+import {
+ ILinuxSplitTunnelingApplication,
+ ISplitTunnelingApplication,
+} from '../shared/application-types';
import {
AccessMethodSetting,
AccountToken,
@@ -185,7 +188,7 @@ export default class AppRenderer {
this.storeAutoStart(autoStart);
});
- IpcRendererEventChannel.windowsSplitTunneling.listen((applications: IWindowsApplication[]) => {
+ IpcRendererEventChannel.splitTunneling.listen((applications: ISplitTunnelingApplication[]) => {
this.reduxActions.settings.setSplitTunnelingApplications(applications);
});
@@ -258,9 +261,9 @@ export default class AppRenderer {
this.checkContentHeight(true);
});
- if (initialState.windowsSplitTunnelingApplications) {
+ if (initialState.splitTunnelingApplications) {
this.reduxActions.settings.setSplitTunnelingApplications(
- initialState.windowsSplitTunnelingApplications,
+ initialState.splitTunnelingApplications,
);
}
@@ -334,11 +337,11 @@ export default class AppRenderer {
public launchExcludedApplication = (application: ILinuxSplitTunnelingApplication | string) =>
IpcRendererEventChannel.linuxSplitTunneling.launchApplication(application);
public setSplitTunnelingState = (state: boolean) =>
- IpcRendererEventChannel.windowsSplitTunneling.setState(state);
- public addSplitTunnelingApplication = (application: string | IWindowsApplication) =>
- IpcRendererEventChannel.windowsSplitTunneling.addApplication(application);
- public forgetManuallyAddedSplitTunnelingApplication = (application: IWindowsApplication) =>
- IpcRendererEventChannel.windowsSplitTunneling.forgetManuallyAddedApplication(application);
+ IpcRendererEventChannel.splitTunneling.setState(state);
+ public addSplitTunnelingApplication = (application: string | ISplitTunnelingApplication) =>
+ IpcRendererEventChannel.splitTunneling.addApplication(application);
+ public forgetManuallyAddedSplitTunnelingApplication = (application: ISplitTunnelingApplication) =>
+ IpcRendererEventChannel.splitTunneling.forgetManuallyAddedApplication(application);
public setObfuscationSettings = (obfuscationSettings: ObfuscationSettings) =>
IpcRendererEventChannel.settings.setObfuscationSettings(obfuscationSettings);
public setDaitaSettings = (daitaSettings: IDaitaSettings) =>
@@ -513,12 +516,12 @@ export default class AppRenderer {
return IpcRendererEventChannel.autoStart.set(autoStart);
};
- public getWindowsSplitTunnelingApplications(updateCache = false) {
- return IpcRendererEventChannel.windowsSplitTunneling.getApplications(updateCache);
+ public getSplitTunnelingApplications(updateCache = false) {
+ return IpcRendererEventChannel.splitTunneling.getApplications(updateCache);
}
- public removeSplitTunnelingApplication(application: IWindowsApplication) {
- void IpcRendererEventChannel.windowsSplitTunneling.removeApplication(application);
+ public removeSplitTunnelingApplication(application: ISplitTunnelingApplication) {
+ void IpcRendererEventChannel.splitTunneling.removeApplication(application);
}
public async showLaunchDaemonSettings() {
diff --git a/gui/src/renderer/components/SplitTunnelingSettings.tsx b/gui/src/renderer/components/SplitTunnelingSettings.tsx
index e26b9ac089..ad7dbccfb1 100644
--- a/gui/src/renderer/components/SplitTunnelingSettings.tsx
+++ b/gui/src/renderer/components/SplitTunnelingSettings.tsx
@@ -6,7 +6,7 @@ import { colors, strings } from '../../config.json';
import {
IApplication,
ILinuxSplitTunnelingApplication,
- IWindowsApplication,
+ ISplitTunnelingApplication,
} from '../../shared/application-types';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
@@ -92,7 +92,7 @@ function PlatformSpecificSplitTunnelingSettings(props: IPlatformSplitTunnelingSe
case 'linux':
return <LinuxSplitTunnelingSettings {...props} />;
case 'win32':
- return <WindowsSplitTunnelingSettings {...props} />;
+ return <SplitTunnelingSettings {...props} />;
default:
throw new Error(`Split tunneling not implemented on ${window.env.platform}`);
}
@@ -300,12 +300,12 @@ function LinuxApplicationRow(props: ILinuxApplicationRowProps) {
);
}
-export function WindowsSplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsProps) {
+export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsProps) {
const {
addSplitTunnelingApplication,
removeSplitTunnelingApplication,
forgetManuallyAddedSplitTunnelingApplication,
- getWindowsSplitTunnelingApplications,
+ getSplitTunnelingApplications,
setSplitTunnelingState,
} = useAppContext();
const splitTunnelingEnabled = useSelector((state: IReduxState) => state.settings.splitTunneling);
@@ -314,13 +314,13 @@ export function WindowsSplitTunnelingSettings(props: IPlatformSplitTunnelingSett
);
const [searchTerm, setSearchTerm] = useState('');
- const [applications, setApplications] = useState<IWindowsApplication[]>();
+ const [applications, setApplications] = useState<ISplitTunnelingApplication[]>();
useAsyncEffect(async () => {
- const { fromCache, applications } = await getWindowsSplitTunnelingApplications();
+ const { fromCache, applications } = await getSplitTunnelingApplications();
setApplications(applications);
if (fromCache) {
- const { applications } = await getWindowsSplitTunnelingApplications(true);
+ const { applications } = await getSplitTunnelingApplications(true);
setApplications(applications);
}
}, []);
@@ -345,7 +345,7 @@ export function WindowsSplitTunnelingSettings(props: IPlatformSplitTunnelingSett
}, [applications, splitTunnelingApplications, searchTerm]);
const addApplication = useCallback(
- async (application: IWindowsApplication | string) => {
+ async (application: ISplitTunnelingApplication | string) => {
if (!splitTunnelingEnabled) {
await setSplitTunnelingState(true);
}
@@ -354,26 +354,26 @@ export function WindowsSplitTunnelingSettings(props: IPlatformSplitTunnelingSett
[addSplitTunnelingApplication, splitTunnelingEnabled, setSplitTunnelingState],
);
- const addApplicationAndUpdate = useCallback(
- async (application: IWindowsApplication | string) => {
+ const addBrowsedForApplication = useCallback(
+ async (application: string) => {
await addApplication(application);
- const { applications } = await getWindowsSplitTunnelingApplications();
+ const { applications } = await getSplitTunnelingApplications();
setApplications(applications);
},
- [addApplication, getWindowsSplitTunnelingApplications],
+ [addApplication, getSplitTunnelingApplications],
);
const forgetManuallyAddedApplicationAndUpdate = useCallback(
- async (application: IWindowsApplication) => {
+ async (application: ISplitTunnelingApplication) => {
await forgetManuallyAddedSplitTunnelingApplication(application);
- const { applications } = await getWindowsSplitTunnelingApplications();
+ const { applications } = await getSplitTunnelingApplications();
setApplications(applications);
},
- [forgetManuallyAddedSplitTunnelingApplication, getWindowsSplitTunnelingApplications],
+ [forgetManuallyAddedSplitTunnelingApplication, getSplitTunnelingApplications],
);
const removeApplication = useCallback(
- async (application: IWindowsApplication) => {
+ async (application: ISplitTunnelingApplication) => {
if (!splitTunnelingEnabled) {
await setSplitTunnelingState(true);
}
@@ -395,21 +395,17 @@ export function WindowsSplitTunnelingSettings(props: IPlatformSplitTunnelingSett
}, [filePickerCallback, props.scrollToTop]);
const excludedRowRenderer = useCallback(
- (application: IWindowsApplication) => (
- <WindowsApplicationRow application={application} onRemove={removeApplication} />
+ (application: ISplitTunnelingApplication) => (
+ <ApplicationRow application={application} onRemove={removeApplication} />
),
[removeApplication],
);
const includedRowRenderer = useCallback(
- (application: IWindowsApplication) => {
+ (application: ISplitTunnelingApplication) => {
const onForget = application.deletable ? forgetManuallyAddedApplicationAndUpdate : undefined;
return (
- <WindowsApplicationRow
- application={application}
- onAdd={addApplication}
- onDelete={onForget}
- />
+ <ApplicationRow application={application} onAdd={addApplication} onDelete={onForget} />
);
},
[addApplication, forgetManuallyAddedApplicationAndUpdate],
@@ -516,14 +512,14 @@ function applicationGetKey<T extends IApplication>(application: T): string {
return application.absolutepath;
}
-interface IWindowsApplicationRowProps {
- application: IWindowsApplication;
- onAdd?: (application: IWindowsApplication) => void;
- onRemove?: (application: IWindowsApplication) => void;
- onDelete?: (application: IWindowsApplication) => void;
+interface IApplicationRowProps {
+ application: ISplitTunnelingApplication;
+ onAdd?: (application: ISplitTunnelingApplication) => void;
+ onRemove?: (application: ISplitTunnelingApplication) => void;
+ onDelete?: (application: ISplitTunnelingApplication) => void;
}
-function WindowsApplicationRow(props: IWindowsApplicationRowProps) {
+function ApplicationRow(props: IApplicationRowProps) {
const onAdd = useCallback(() => {
props.onAdd?.(props.application);
}, [props.onAdd, props.application]);
diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts
index ba6e4ce5a8..d2a3fb1c4a 100644
--- a/gui/src/renderer/redux/settings/actions.ts
+++ b/gui/src/renderer/redux/settings/actions.ts
@@ -1,4 +1,4 @@
-import { IWindowsApplication } from '../../../shared/application-types';
+import { ISplitTunnelingApplication } from '../../../shared/application-types';
import {
AccessMethodSetting,
ApiAccessMethodSettings,
@@ -100,7 +100,7 @@ export interface IUpdateSplitTunnelingStateAction {
export interface ISetSplitTunnelingApplicationsAction {
type: 'SET_SPLIT_TUNNELING_APPLICATIONS';
- applications: IWindowsApplication[];
+ applications: ISplitTunnelingApplication[];
}
export interface ISetObfuscationSettings {
@@ -281,7 +281,7 @@ function updateSplitTunnelingState(enabled: boolean): IUpdateSplitTunnelingState
}
function setSplitTunnelingApplications(
- applications: IWindowsApplication[],
+ applications: ISplitTunnelingApplication[],
): ISetSplitTunnelingApplicationsAction {
return {
type: 'SET_SPLIT_TUNNELING_APPLICATIONS',
diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts
index 18873eea20..eae413fe22 100644
--- a/gui/src/renderer/redux/settings/reducers.ts
+++ b/gui/src/renderer/redux/settings/reducers.ts
@@ -1,5 +1,5 @@
import { getDefaultApiAccessMethods } from '../../../main/default-settings';
-import { IWindowsApplication } from '../../../shared/application-types';
+import { ISplitTunnelingApplication } from '../../../shared/application-types';
import {
AccessMethodSetting,
ApiAccessMethodSettings,
@@ -115,7 +115,7 @@ export interface ISettingsReduxState {
};
dns: IDnsOptions;
splitTunneling: boolean;
- splitTunnelingApplications: IWindowsApplication[];
+ splitTunnelingApplications: ISplitTunnelingApplication[];
obfuscationSettings: ObfuscationSettings;
customLists: CustomLists;
apiAccessMethods: ApiAccessMethodSettings;
diff --git a/gui/src/shared/application-types.ts b/gui/src/shared/application-types.ts
index 5f9fb80fd0..526d994d7b 100644
--- a/gui/src/shared/application-types.ts
+++ b/gui/src/shared/application-types.ts
@@ -6,7 +6,7 @@ export interface IApplication {
icon?: string;
}
-export interface IWindowsApplication extends IApplication {
+export interface ISplitTunnelingApplication extends IApplication {
deletable: boolean;
}
@@ -24,3 +24,37 @@ export interface ILinuxApplication extends IApplication {
export interface ILinuxSplitTunnelingApplication extends ILinuxApplication {
warning?: Warning;
}
+
+export interface ISplitTunnelingAppListRetriever {
+ /**
+ * Returns a list of all applications known to the app.
+ * @param updateCaches Specifies if the application list should be fetched again and merged into the existing cache.
+ */
+ getApplications(
+ updateCaches?: boolean,
+ ): Promise<{ fromCache: boolean; applications: ISplitTunnelingApplication[] }>;
+
+ /**
+ * Returns an object containing information about whether or not it was fetched from the cache,
+ * and a list of ISplitTunnelingApplication corresponding to the provided paths.
+ */
+ getMetadataForApplications(
+ applicationPaths: string[],
+ ): Promise<{ fromCache: boolean; applications: ISplitTunnelingApplication[] }>;
+
+ /**
+ * Resolves the actual executable path when an app is provided. On Windows this resolves links and
+ * on macOS this finds the executable when an application bundle is provided.
+ */
+ resolveExecutablePath(providedPath: string): Promise<string>;
+
+ /**
+ * Adds an application to the internal cache.
+ */
+ addApplicationPathToCache(applicationPath: string): Promise<void>;
+
+ /**
+ * Removes an application from the internal cache.
+ */
+ removeApplicationFromCache(application: ISplitTunnelingApplication): void;
+}
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index 561ec924a0..b7f298b2bc 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -1,6 +1,6 @@
import { GetTextTranslations } from 'gettext-parser';
-import { ILinuxSplitTunnelingApplication, IWindowsApplication } from './application-types';
+import { ILinuxSplitTunnelingApplication, ISplitTunnelingApplication } from './application-types';
import {
AccessMethodSetting,
AccountDataError,
@@ -71,7 +71,7 @@ export interface IAppStateSnapshot {
upgradeVersion: IAppVersionInfo;
guiSettings: IGuiSettingsState;
translations: ITranslations;
- windowsSplitTunnelingApplications?: IWindowsApplication[];
+ splitTunnelingApplications?: ISplitTunnelingApplication[];
macOsScrollbarVisibility?: MacOsScrollbarVisibility;
changelog: IChangelog;
forceShowChanges: boolean;
@@ -238,12 +238,15 @@ export const ipcSchema = {
getApplications: invoke<void, ILinuxSplitTunnelingApplication[]>(),
launchApplication: invoke<ILinuxSplitTunnelingApplication | string, LaunchApplicationResult>(),
},
- windowsSplitTunneling: {
- '': notifyRenderer<IWindowsApplication[]>(),
+ splitTunneling: {
+ '': notifyRenderer<ISplitTunnelingApplication[]>(),
setState: invoke<boolean, void>(),
- getApplications: invoke<boolean, { fromCache: boolean; applications: IWindowsApplication[] }>(),
- addApplication: invoke<IWindowsApplication | string, void>(),
- removeApplication: invoke<IWindowsApplication, void>(),
- forgetManuallyAddedApplication: invoke<IWindowsApplication, void>(),
+ getApplications: invoke<
+ boolean,
+ { fromCache: boolean; applications: ISplitTunnelingApplication[] }
+ >(),
+ addApplication: invoke<ISplitTunnelingApplication | string, void>(),
+ removeApplication: invoke<ISplitTunnelingApplication, void>(),
+ forgetManuallyAddedApplication: invoke<ISplitTunnelingApplication, void>(),
},
};