diff options
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/main/index.ts | 65 | ||||
| -rw-r--r-- | gui/src/main/windows-split-tunneling.ts | 976 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 29 | ||||
| -rw-r--r-- | gui/src/renderer/components/SplitTunnelingSettings.tsx | 56 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/actions.ts | 6 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 4 | ||||
| -rw-r--r-- | gui/src/shared/application-types.ts | 36 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 19 |
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>(), }, }; |
