diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-03-14 13:59:05 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-03-14 13:59:05 +0100 |
| commit | 4ab205bd9add69264ccdfaaf8cf068515ceddb77 (patch) | |
| tree | 302357507a7e51ece04f563c5f7018d64adaccac /gui/src/main | |
| parent | 6459ae7beefcc5f13eb54254dfe402dd807c62fe (diff) | |
| parent | 55aa3418f8b7ec6f473fd22819f7e54cb432d097 (diff) | |
| download | mullvadvpn-4ab205bd9add69264ccdfaaf8cf068515ceddb77.tar.xz mullvadvpn-4ab205bd9add69264ccdfaaf8cf068515ceddb77.zip | |
Merge branch 'device-api-electron'
Diffstat (limited to 'gui/src/main')
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 149 | ||||
| -rw-r--r-- | gui/src/main/errors.ts | 6 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 201 |
3 files changed, 177 insertions, 179 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 6638799e2f..2497bceaeb 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -36,8 +36,6 @@ import { TunnelType, IProxyEndpoint, ProxyType, - KeygenEvent, - IWireguardPublicKey, ISettings, ConnectionConfig, DaemonEvent, @@ -48,12 +46,16 @@ import { VoucherResponse, TunnelProtocol, IDnsOptions, + IDeviceConfig, + IDevice, + IDeviceRemoval, + IDeviceEvent, } from '../shared/daemon-rpc-types'; import log from '../shared/logging'; import { ManagementServiceClient } from './management_interface/management_interface_grpc_pb'; import * as grpcTypes from './management_interface/management_interface_pb'; -import { CommunicationError, InvalidAccountError } from './errors'; +import { CommunicationError, InvalidAccountError, TooManyDevicesError } from './errors'; const NETWORK_CALL_TIMEOUT = 10000; const CHANNEL_STATE_TIMEOUT = 1000 * 60 * 60; @@ -257,8 +259,24 @@ export class DaemonRpc { return response.getValue(); } - public async setAccount(accountToken?: AccountToken): Promise<void> { - await this.callString(this.client.setAccount, accountToken); + public async loginAccount(accountToken: AccountToken): Promise<void> { + try { + await this.callString(this.client.loginAccount, accountToken); + } catch (e) { + const error = e as grpc.ServiceError; + switch (error.code) { + case grpc.status.RESOURCE_EXHAUSTED: + throw new TooManyDevicesError(); + case grpc.status.UNAUTHENTICATED: + throw new InvalidAccountError(); + default: + throw new CommunicationError(); + } + } + } + + public async logoutAccount(): Promise<void> { + await this.callEmpty(this.client.logoutAccount); } // TODO: Custom tunnel configurations are not supported by the GUI. @@ -438,19 +456,6 @@ export class DaemonRpc { return response.getValue(); } - public async generateWireguardKey(): Promise<KeygenEvent> { - const response = await this.callEmpty<grpcTypes.KeygenEvent>(this.client.generateWireguardKey); - return convertFromKeygenEvent(response); - } - - public async getWireguardKey(): Promise<IWireguardPublicKey> { - const response = await this.callEmpty<grpcTypes.PublicKey>(this.client.getWireguardKey); - return { - created: response.getCreated()!.toDate().toISOString(), - key: convertFromWireguardKey(response.getKey()), - }; - } - public async setDnsOptions(dns: IDnsOptions): Promise<void> { const dnsOptions = new grpcTypes.DnsOptions(); @@ -473,11 +478,6 @@ export class DaemonRpc { await this.call<grpcTypes.DnsOptions, Empty>(this.client.setDnsOptions, dnsOptions); } - public async verifyWireguardKey(): Promise<boolean> { - const response = await this.callEmpty<BoolValue>(this.client.verifyWireguardKey); - return response.getValue(); - } - public async getVersionInfo(): Promise<IAppVersionInfo> { const response = await this.callEmpty<grpcTypes.AppVersionInfo>(this.client.getVersionInfo); return response.toObject(); @@ -499,6 +499,37 @@ export class DaemonRpc { await this.callEmpty(this.client.checkVolumes); } + public async getDevice(): Promise<IDeviceConfig | undefined> { + try { + const response = await this.callEmpty<grpcTypes.DeviceConfig>(this.client.getDevice); + return convertFromDeviceConfig(response); + } catch (e) { + const error = e as grpc.ServiceError; + if (error.code === grpc.status.NOT_FOUND) { + return undefined; + } else { + throw error; + } + } + } + + public async listDevices(accountToken: AccountToken): Promise<Array<IDevice>> { + const response = await this.callString<grpcTypes.DeviceList>( + this.client.listDevices, + accountToken, + ); + + return response.getDevicesList().map(convertFromDevice); + } + + public async removeDevice(deviceRemoval: IDeviceRemoval): Promise<void> { + const grpcDeviceRemoval = new grpcTypes.DeviceRemoval(); + grpcDeviceRemoval.setAccountToken(deviceRemoval.accountToken); + grpcDeviceRemoval.setDeviceId(deviceRemoval.deviceId); + + await this.call<grpcTypes.DeviceRemoval, Empty>(this.client.removeDevice, grpcDeviceRemoval); + } + private subscriptionId(): number { const current = this.nextSubscriptionId; this.nextSubscriptionId += 1; @@ -1123,36 +1154,26 @@ function convertFromDaemonEvent(data: grpcTypes.DaemonEvent): DaemonEvent { }; } - const keygenEvent = data.getKeyEvent(); - if (keygenEvent !== undefined) { - return { - wireguardKey: convertFromKeygenEvent(keygenEvent), - }; + const deviceConfig = data.getDevice(); + if (deviceConfig !== undefined) { + return { device: convertFromDeviceEvent(deviceConfig) }; } - return { - appVersionInfo: data.getVersionInfo()!.toObject(), - }; -} + const deviceRemoval = data.getRemoveDevice(); + if (deviceRemoval !== undefined) { + return { deviceRemoval: convertFromDeviceRemoval(deviceRemoval) }; + } -function convertFromKeygenEvent(data: grpcTypes.KeygenEvent): KeygenEvent { - switch (data.getEvent()) { - case grpcTypes.KeygenEvent.KeygenEvent.TOO_MANY_KEYS: - return 'too_many_keys'; - case grpcTypes.KeygenEvent.KeygenEvent.NEW_KEY: { - const newKey = data.getNewKey(); - return newKey - ? { - newKey: { - created: newKey.getCreated()!.toDate().toISOString(), - key: convertFromWireguardKey(newKey.getKey()), - }, - } - : 'generation_failure'; - } - case grpcTypes.KeygenEvent.KeygenEvent.GENERATION_FAILURE: - return 'generation_failure'; + const versionInfo = data.getVersionInfo(); + if (versionInfo !== undefined) { + return { appVersionInfo: versionInfo.toObject() }; } + + // Handle unknown daemon events + const keys = Object.entries(data.toObject()) + .filter(([, value]) => value !== undefined) + .map(([key]) => key); + throw new Error(`Unknown daemon event received containing ${keys}`); } function convertFromOpenVpnConstraints( @@ -1350,6 +1371,36 @@ function convertToTransportProtocol(protocol: RelayProtocol): grpcTypes.Transpor } } +function convertFromDeviceEvent(deviceEvent: grpcTypes.DeviceEvent): IDeviceEvent { + return { + deviceConfig: convertFromDeviceConfig(deviceEvent.getDevice()), + remote: deviceEvent.getRemote(), + }; +} + +function convertFromDeviceConfig(deviceConfig?: grpcTypes.DeviceConfig): IDeviceConfig | undefined { + const device = deviceConfig?.getDevice(); + return ( + deviceConfig && { + accountToken: deviceConfig.getAccountToken(), + device: device ? convertFromDevice(device) : undefined, + } + ); +} + +function convertFromDeviceRemoval(deviceRemoval: grpcTypes.RemoveDeviceEvent): Array<IDevice> { + return deviceRemoval.getNewDeviceListList().map(convertFromDevice); +} + +function convertFromDevice(device: grpcTypes.Device): IDevice { + const asObject = device.toObject(); + + return { + ...asObject, + ports: asObject.portsList.map((port) => port.id), + }; +} + function ensureExists<T>(value: T | undefined, errorMessage: string): T { if (value) { return value; diff --git a/gui/src/main/errors.ts b/gui/src/main/errors.ts index 261ee7a164..b7cc82c365 100644 --- a/gui/src/main/errors.ts +++ b/gui/src/main/errors.ts @@ -15,3 +15,9 @@ export class CommunicationError extends Error { super('api.mullvad.net is blocked, please check your firewall'); } } + +export class TooManyDevicesError extends Error { + constructor() { + super('Too many devices'); + } +} diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 8cebf9511b..c53456cc65 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -27,15 +27,16 @@ import { DaemonEvent, IAccountData, IAppVersionInfo, + IDeviceConfig, + IDeviceRemoval, IDnsOptions, IRelayList, ISettings, - IWireguardPublicKey, - KeygenEvent, liftConstraint, RelaySettings, RelaySettingsUpdate, TunnelState, + IDeviceEvent, } from '../shared/daemon-rpc-types'; import { messages, relayLocations } from '../shared/gettext'; import { SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state'; @@ -88,8 +89,6 @@ const windowsSplitTunneling = process.platform === 'win32' && require('./windows const DAEMON_RPC_PATH = process.platform === 'win32' ? 'unix:////./pipe/Mullvad VPN' : 'unix:///var/run/mullvad-vpn'; -const AUTO_CONNECT_FALLBACK_DELAY = 6000; - const GUI_VERSION = app.getVersion().replace('.0', ''); /// Mirrors the beta check regex in the daemon. Matches only well formed beta versions const IS_BETA = /^(\d{4})\.(\d+)-beta(\d+)$/; @@ -108,8 +107,6 @@ enum AppQuitStage { ready, } -type AccountVerification = { status: 'verified' } | { status: 'deferred'; error: Error }; - class ApplicationMain { private notificationController = new NotificationController({ openApp: () => this.windowController?.show(), @@ -145,7 +142,6 @@ class ApplicationMain { private tunnelStateFallbackScheduler = new Scheduler(); private settings: ISettings = { - accountToken: undefined, allowLan: false, autoConnect: false, blockWhenDisconnected: false, @@ -201,6 +197,7 @@ class ApplicationMain { }, }, }; + private deviceConfig?: IDeviceConfig; private guiSettings = new GuiSettings(); private tunnelStateExpectation?: Expectation; @@ -221,8 +218,6 @@ class ApplicationMain { // The UI locale which is set once from onReady handler private locale = 'en'; - private wireguardPublicKey?: IWireguardPublicKey; - private accountExpiryNotificationScheduler = new Scheduler(); private accountDataCache = new AccountDataCache( @@ -240,9 +235,6 @@ class ApplicationMain { }, ); - private autoConnectOnWireguardKeyEvent = false; - private autoConnectFallbackScheduler = new Scheduler(); - private blurNavigationResetScheduler = new Scheduler(); private backgroundThrottleScheduler = new Scheduler(); @@ -648,6 +640,16 @@ class ApplicationMain { return this.handleBootstrapError(error); } + // fetch device + try { + this.setDeviceConfig({ deviceConfig: await this.daemonRpc.getDevice() }); + } catch (e) { + const error = e as Error; + log.error(`Failed to fetch device: ${error.message}`); + + return this.handleBootstrapError(error); + } + // fetch settings try { this.setSettings(await this.daemonRpc.getSettings()); @@ -705,7 +707,7 @@ class ApplicationMain { } // show window when account is not set - if (!this.settings.accountToken) { + if (!this.deviceConfig) { this.windowController?.show(); } }; @@ -722,7 +724,6 @@ class ApplicationMain { this.daemonEventListener = undefined; this.tunnelStateFallback = undefined; - this.autoConnectFallbackScheduler.cancel(); if (wasConnected) { this.connectedToDaemon = false; @@ -775,15 +776,21 @@ class ApplicationMain { this.settings.relaySettings, this.settings.bridgeState, ); - } else if ('wireguardKey' in daemonEvent) { - this.handleWireguardKeygenEvent(daemonEvent.wireguardKey); } else if ('appVersionInfo' in daemonEvent) { this.setLatestVersion(daemonEvent.appVersionInfo); + } else if ('device' in daemonEvent) { + this.setDeviceConfig(daemonEvent.device); + } else if ('deviceRemoval' in daemonEvent) { + if (this.windowController) { + IpcMainEventChannel.account.notifyDevices( + this.windowController.webContents, + daemonEvent.deviceRemoval, + ); + } } }, (error: Error) => { log.error(`Cannot deserialize the daemon event: ${error.message}`); - log.error(error.stack); }, ); @@ -794,7 +801,11 @@ class ApplicationMain { private connectTunnel = async (): Promise<void> => { if ( - connectEnabled(this.connectedToDaemon, this.settings.accountToken, this.tunnelState.state) + connectEnabled( + this.connectedToDaemon, + this.deviceConfig?.accountToken, + this.tunnelState.state, + ) ) { this.setOptimisticTunnelState('connecting'); await this.daemonRpc.connectTunnel(); @@ -803,7 +814,11 @@ class ApplicationMain { private reconnectTunnel = async (): Promise<void> => { if ( - reconnectEnabled(this.connectedToDaemon, this.settings.accountToken, this.tunnelState.state) + reconnectEnabled( + this.connectedToDaemon, + this.deviceConfig?.accountToken, + this.tunnelState.state, + ) ) { this.setOptimisticTunnelState('connecting'); await this.daemonRpc.reconnectTunnel(); @@ -825,37 +840,6 @@ class ApplicationMain { } } - private setWireguardKey(wireguardKey?: IWireguardPublicKey) { - this.wireguardPublicKey = wireguardKey; - if (this.windowController) { - IpcMainEventChannel.wireguardKeys.notifyPublicKey( - this.windowController.webContents, - wireguardKey, - ); - } - - if (wireguardKey) { - this.wireguardKeygenEventAutoConnect(); - } - } - - private handleWireguardKeygenEvent(event: KeygenEvent) { - switch (event) { - case 'too_many_keys': - case 'generation_failure': - this.wireguardPublicKey = undefined; - break; - default: - this.wireguardPublicKey = event.newKey; - } - - if (this.windowController) { - IpcMainEventChannel.wireguardKeys.notifyKeygenEvent(this.windowController.webContents, event); - } - - this.wireguardKeygenEventAutoConnect(); - } - // This function sets a new tunnel state as an assumed next state and saves the current state as // fallback. The fallback is used if the assumed next state isn't reached. private setOptimisticTunnelState(state: 'connecting' | 'disconnecting') { @@ -924,14 +908,6 @@ class ApplicationMain { this.updateTrayIcon(this.tunnelState, newSettings.blockWhenDisconnected); - // make sure to invalidate the account data cache when account tokens change - this.updateAccountDataOnAccountChange(oldSettings.accountToken, newSettings.accountToken); - - if (oldSettings.accountToken !== newSettings.accountToken) { - void this.updateAccountHistory(); - void this.fetchWireguardKey(); - } - if (oldSettings.showBetaReleases !== newSettings.showBetaReleases) { this.setLatestVersion(this.upgradeVersion); } @@ -1135,6 +1111,23 @@ class ApplicationMain { } } + private setDeviceConfig(deviceEvent: IDeviceEvent) { + const oldDeviceConfig = this.deviceConfig; + this.deviceConfig = deviceEvent.deviceConfig; + + // make sure to invalidate the account data cache when account tokens change + this.updateAccountDataOnAccountChange( + oldDeviceConfig?.accountToken, + deviceEvent.deviceConfig?.accountToken, + ); + + void this.updateAccountHistory(); + + if (this.windowController) { + IpcMainEventChannel.account.notifyDevice(this.windowController.webContents, deviceEvent); + } + } + private trayIconType(tunnelState: TunnelState, blockWhenDisconnected: boolean): TrayIconType { switch (tunnelState.state) { case 'connected': @@ -1216,6 +1209,7 @@ class ApplicationMain { accountHistory: this.accountHistory, tunnelState: this.tunnelState, settings: this.settings, + deviceConfig: this.deviceConfig, relayListPair: { relays: this.processRelaysForPresentation(this.relays, this.settings.relaySettings), bridges: this.processBridgesForPresentation(this.relays, this.settings.bridgeState), @@ -1223,7 +1217,6 @@ class ApplicationMain { currentVersion: this.currentVersion, upgradeVersion: this.upgradeVersion, guiSettings: this.guiSettings.state, - wireguardPublicKey: this.wireguardPublicKey, translations: this.translations, windowsSplitTunnelingApplications: this.windowsSplitTunnelingApplications, macOsScrollbarVisibility: this.macOsScrollbarVisibility, @@ -1306,7 +1299,7 @@ class ApplicationMain { IpcMainEventChannel.account.handleLogout(() => this.logout()); IpcMainEventChannel.account.handleGetWwwAuthToken(() => this.daemonRpc.getWwwAuthToken()); IpcMainEventChannel.account.handleSubmitVoucher(async (voucherCode: string) => { - const currentAccountToken = this.settings.accountToken; + const currentAccountToken = this.deviceConfig?.accountToken; const response = await this.daemonRpc.submitVoucher(voucherCode); if (currentAccountToken) { @@ -1317,20 +1310,22 @@ class ApplicationMain { }); IpcMainEventChannel.account.handleUpdateData(() => this.updateAccountData()); + IpcMainEventChannel.account.handleGetDevice(async () => { + const deviceConfig = await this.daemonRpc.getDevice(); + return deviceConfig?.device; + }); + IpcMainEventChannel.account.handleListDevices((accountToken: AccountToken) => { + return this.daemonRpc.listDevices(accountToken); + }); + IpcMainEventChannel.account.handleRemoveDevice((deviceRemoval: IDeviceRemoval) => { + return this.daemonRpc.removeDevice(deviceRemoval); + }); + IpcMainEventChannel.accountHistory.handleClear(async () => { await this.daemonRpc.clearAccountHistory(); void this.updateAccountHistory(); }); - IpcMainEventChannel.wireguardKeys.handleGenerateKey(async () => { - try { - return await this.daemonRpc.generateWireguardKey(); - } catch { - return 'generation_failure'; - } - }); - IpcMainEventChannel.wireguardKeys.handleVerifyKey(() => this.daemonRpc.verifyWireguardKey()); - IpcMainEventChannel.linuxSplitTunneling.handleGetApplications(() => { if (linuxSplitTunneling) { return linuxSplitTunneling.getApplications(this.locale); @@ -1484,28 +1479,11 @@ class ApplicationMain { private async login(accountToken: AccountToken): Promise<void> { try { - const verification = await this.verifyAccount(accountToken); - - if (verification.status === 'deferred') { - log.warn(`Failed to get account data, logging in anyway: ${verification.error.message}`); - } - - this.autoConnectOnWireguardKeyEvent = this.guiSettings.autoConnect; - await this.daemonRpc.setAccount(accountToken); - - // Fallback if daemon doesn't send event. - if (this.autoConnectOnWireguardKeyEvent) { - this.autoConnectFallbackScheduler.schedule( - () => this.wireguardKeygenEventAutoConnect(), - AUTO_CONNECT_FALLBACK_DELAY, - ); - } + await this.daemonRpc.loginAccount(accountToken); } catch (e) { const error = e as Error; log.error(`Failed to login: ${error.message}`); - this.autoConnectOnWireguardKeyEvent = false; - if (error instanceof InvalidAccountError) { throw Error(messages.gettext('Invalid account number')); } else { @@ -1514,21 +1492,10 @@ class ApplicationMain { } } - private wireguardKeygenEventAutoConnect() { - if (this.autoConnectOnWireguardKeyEvent) { - this.autoConnectOnWireguardKeyEvent = false; - this.autoConnectFallbackScheduler.cancel(); - void this.autoConnect(); - } - } - private async autoConnect() { if (process.env.NODE_ENV === 'development') { log.info('Skip autoconnect in development'); - } else if ( - this.settings.accountToken && - (!this.accountData || !hasExpired(this.accountData.expiry)) - ) { + } else if (this.deviceConfig && (!this.accountData || !hasExpired(this.accountData.expiry))) { if (this.guiSettings.autoConnect) { try { log.info('Autoconnect the tunnel'); @@ -1548,9 +1515,8 @@ class ApplicationMain { private async logout(): Promise<void> { try { - await this.daemonRpc.setAccount(); + await this.daemonRpc.logoutAccount(); - this.autoConnectFallbackScheduler.cancel(); this.accountExpiryNotificationScheduler.cancel(); } catch (e) { const error = e as Error; @@ -1560,22 +1526,6 @@ class ApplicationMain { } } - private verifyAccount(accountToken: AccountToken): Promise<AccountVerification> { - return new Promise((resolve, reject) => { - this.accountDataCache.invalidate(); - this.accountDataCache.fetch(accountToken, { - onFinish: () => resolve({ status: 'verified' }), - onError: (error) => { - if (error instanceof InvalidAccountError) { - reject(error); - } else { - resolve({ status: 'deferred', error }); - } - }, - }); - }); - } - private updateAccountDataOnAccountChange(oldAccount?: string, newAccount?: string) { if (oldAccount && !newAccount) { this.accountDataCache.invalidate(); @@ -1589,8 +1539,8 @@ class ApplicationMain { } private updateAccountData() { - if (this.connectedToDaemon && this.settings.accountToken) { - this.accountDataCache.fetch(this.settings.accountToken); + if (this.connectedToDaemon && this.deviceConfig) { + this.accountDataCache.fetch(this.deviceConfig.accountToken); } } @@ -1641,15 +1591,6 @@ class ApplicationMain { } } - private async fetchWireguardKey(): Promise<void> { - try { - this.setWireguardKey(await this.daemonRpc.getWireguardKey()); - } catch (e) { - const error = e as Error; - log.error(`Failed to fetch wireguard key: ${error.message}`); - } - } - private updateDaemonsAutoConnect() { const daemonAutoConnect = this.guiSettings.autoConnect && getOpenAtLogin(); if (daemonAutoConnect !== this.settings.autoConnect) { @@ -1984,7 +1925,7 @@ class ApplicationMain { this.tray?.on('right-click', () => this.trayIconController?.popUpContextMenu( this.connectedToDaemon, - this.settings.accountToken, + this.deviceConfig?.accountToken, this.tunnelState, ), ); @@ -2022,7 +1963,7 @@ class ApplicationMain { private setTrayContextMenu() { this.trayIconController?.setContextMenu( this.connectedToDaemon, - this.settings.accountToken, + this.deviceConfig?.accountToken, this.tunnelState, ); } |
