diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-11-03 19:38:17 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-03-14 13:58:44 +0100 |
| commit | d5f21653bec09d342112d66c5d20eca38e16b49e (patch) | |
| tree | 9d6b8baacd70c2f84909e45fc7ea9976e9314e4d | |
| parent | 46fa74ad03658ad0afa9dc984565b7f10106af94 (diff) | |
| download | mullvadvpn-d5f21653bec09d342112d66c5d20eca38e16b49e.tar.xz mullvadvpn-d5f21653bec09d342112d66c5d20eca38e16b49e.zip | |
Add rpc and ipc calls for listing and removing devices
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 54 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 44 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 32 | ||||
| -rw-r--r-- | gui/src/renderer/redux/account/actions.ts | 14 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 17 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 10 |
6 files changed, 126 insertions, 45 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index eab5559ad3..f17505bd80 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -46,7 +46,9 @@ import { VoucherResponse, TunnelProtocol, IDnsOptions, - IDeviceConfig, + DeviceConfig, + IDevice, + IDeviceRemoval, } from '../shared/daemon-rpc-types'; import log from '../shared/logging'; @@ -484,6 +486,37 @@ export class DaemonRpc { await this.callEmpty(this.client.checkVolumes); } + public async getDevice(): Promise<DeviceConfig> { + 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.toObject().devicesList; + } + + 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; @@ -1110,7 +1143,7 @@ function convertFromDaemonEvent(data: grpcTypes.DaemonEvent): DaemonEvent { const deviceConfig = data.getDevice(); if (deviceConfig !== undefined) { - return { deviceConfig: convertFromDeviceConfig(deviceConfig) }; + return { deviceConfig: convertFromDeviceEvent(deviceConfig) }; } return { @@ -1313,12 +1346,17 @@ function convertToTransportProtocol(protocol: RelayProtocol): grpcTypes.Transpor } } -function convertFromDeviceConfig(deviceEvent: grpcTypes.DeviceEvent): IDeviceConfig { - const deviceConfig = deviceEvent.getDevice(); - return { - accountToken: deviceConfig?.getAccountToken(), - device: deviceConfig?.getDevice()?.toObject(), - }; +function convertFromDeviceEvent(deviceEvent: grpcTypes.DeviceEvent): DeviceConfig { + return convertFromDeviceConfig(deviceEvent.getDevice()); +} + +function convertFromDeviceConfig(deviceConfig?: grpcTypes.DeviceConfig): DeviceConfig { + return ( + deviceConfig && { + accountToken: deviceConfig.getAccountToken(), + device: deviceConfig.getDevice()?.toObject(), + } + ); } function ensureExists<T>(value: T | undefined, errorMessage: string): T { diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 07afda535c..f8a9b2497f 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -27,7 +27,8 @@ import { DaemonEvent, IAccountData, IAppVersionInfo, - IDeviceConfig, + DeviceConfig, + IDeviceRemoval, IDnsOptions, IRelayList, ISettings, @@ -197,7 +198,7 @@ class ApplicationMain { }, }, }; - private deviceConfig: IDeviceConfig = { accountToken: undefined, device: undefined }; + private deviceConfig: DeviceConfig = undefined; private guiSettings = new GuiSettings(); private tunnelStateExpectation?: Expectation; @@ -640,6 +641,16 @@ class ApplicationMain { return this.handleBootstrapError(error); } + // fetch device + try { + this.setDeviceConfig(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()); @@ -697,7 +708,7 @@ class ApplicationMain { } // show window when account is not set - if (!this.deviceConfig.accountToken) { + if (!this.deviceConfig) { this.windowController?.show(); } }; @@ -1095,14 +1106,21 @@ class ApplicationMain { } } - private setDeviceConfig(deviceConfig: IDeviceConfig) { + private setDeviceConfig(deviceConfig: DeviceConfig) { const oldDeviceConfig = this.deviceConfig; this.deviceConfig = deviceConfig; // make sure to invalidate the account data cache when account tokens change - this.updateAccountDataOnAccountChange(oldDeviceConfig.accountToken, deviceConfig.accountToken); + this.updateAccountDataOnAccountChange( + oldDeviceConfig?.accountToken, + deviceConfig?.accountToken, + ); void this.updateAccountHistory(); + + if (this.windowController) { + IpcMainEventChannel.account.notifyDevice(this.windowController.webContents, deviceConfig); + } } private trayIconType(tunnelState: TunnelState, blockWhenDisconnected: boolean): TrayIconType { @@ -1276,7 +1294,7 @@ class ApplicationMain { IpcMainEventChannel.account.handleLogout(() => this.logout()); IpcMainEventChannel.account.handleGetWwwAuthToken(() => this.daemonRpc.getWwwAuthToken()); IpcMainEventChannel.account.handleSubmitVoucher(async (voucherCode: string) => { - const currentAccountToken = this.deviceConfig.accountToken; + const currentAccountToken = this.deviceConfig?.accountToken; const response = await this.daemonRpc.submitVoucher(voucherCode); if (currentAccountToken) { @@ -1287,6 +1305,13 @@ class ApplicationMain { }); IpcMainEventChannel.account.handleUpdateData(() => this.updateAccountData()); + 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(); @@ -1467,10 +1492,7 @@ class ApplicationMain { private async autoConnect() { if (process.env.NODE_ENV === 'development') { log.info('Skip autoconnect in development'); - } else if ( - this.deviceConfig.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'); @@ -1530,7 +1552,7 @@ class ApplicationMain { } private updateAccountData() { - if (this.connectedToDaemon && this.deviceConfig.accountToken) { + if (this.connectedToDaemon && this.deviceConfig) { this.accountDataCache.fetch(this.deviceConfig.accountToken); } } diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index ff46b76f1a..595ded57c5 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -31,7 +31,9 @@ import { BridgeState, IAccountData, IAppVersionInfo, - IDeviceConfig, + IDevice, + DeviceConfig, + IDeviceRemoval, IDnsOptions, ILocation, ISettings, @@ -93,7 +95,7 @@ export default class AppRenderer { private relayListPair!: IRelayListPair; private tunnelState!: TunnelState; private settings!: ISettings; - private deviceConfig!: IDeviceConfig; + private deviceConfig: DeviceConfig; private guiSettings!: IGuiSettingsState; private loginState: 'none' | 'logging in' | 'creating account' = 'none'; private loginScheduler = new Scheduler(); @@ -122,10 +124,9 @@ export default class AppRenderer { this.setAccountExpiry(newAccountData?.expiry); }); - IpcRendererEventChannel.account.listenDevice((deviceConfig: IDeviceConfig) => { + IpcRendererEventChannel.account.listenDevice((deviceConfig: DeviceConfig) => { const oldDeviceConfig = this.deviceConfig; - this.deviceConfig = deviceConfig; - this.handleAccountChange(deviceConfig, oldDeviceConfig.accountToken); + this.handleAccountChange(deviceConfig, oldDeviceConfig?.accountToken); }); IpcRendererEventChannel.accountHistory.listen((newAccountHistory?: AccountToken) => { @@ -232,7 +233,7 @@ export default class AppRenderer { const navigationBase = this.getNavigationBase( initialState.isConnected, - initialState.deviceConfig.accountToken, + initialState.deviceConfig?.accountToken, ); this.history = new History(navigationBase); } @@ -307,6 +308,14 @@ export default class AppRenderer { IpcRendererEventChannel.account.updateData(); } + public listDevices(accountToken: AccountToken): Promise<Array<IDevice>> { + return IpcRendererEventChannel.account.listDevices(accountToken); + } + + public removeDevice(deviceRemoval: IDeviceRemoval): Promise<void> { + return IpcRendererEventChannel.account.removeDevice(deviceRemoval); + } + public async connectTunnel(): Promise<void> { return IpcRendererEventChannel.tunnel.connect(); } @@ -615,7 +624,7 @@ export default class AppRenderer { const pathname = this.history.location.pathname; const nextPath = this.getNavigationBase( this.connectedToDaemon, - this.deviceConfig.accountToken, + this.deviceConfig?.accountToken, ); // First level contains the possible next locations and the second level contains the possible @@ -737,10 +746,11 @@ export default class AppRenderer { } } - private handleAccountChange(newDeviceConfig: IDeviceConfig, oldAccount?: string) { + private handleAccountChange(newDeviceConfig: DeviceConfig, oldAccount?: string) { const reduxAccount = this.reduxActions.account; - const newAccount = newDeviceConfig.accountToken; + this.deviceConfig = newDeviceConfig; + const newAccount = newDeviceConfig?.accountToken; if (oldAccount && !newAccount) { this.loginScheduler.cancel(); @@ -748,8 +758,8 @@ export default class AppRenderer { this.resetNavigation(); } else if ( - newDeviceConfig.accountToken !== undefined && - newDeviceConfig.device !== undefined && + newDeviceConfig?.accountToken !== undefined && + newDeviceConfig?.device !== undefined && oldAccount !== newAccount ) { switch (this.loginState) { diff --git a/gui/src/renderer/redux/account/actions.ts b/gui/src/renderer/redux/account/actions.ts index ebc63e322c..2eb9f21a17 100644 --- a/gui/src/renderer/redux/account/actions.ts +++ b/gui/src/renderer/redux/account/actions.ts @@ -1,4 +1,4 @@ -import { AccountToken, IDeviceConfig } from '../../../shared/daemon-rpc-types'; +import { AccountToken, DeviceConfig } from '../../../shared/daemon-rpc-types'; interface IStartLoginAction { type: 'START_LOGIN'; @@ -8,7 +8,7 @@ interface IStartLoginAction { interface ILoggedInAction { type: 'LOGGED_IN'; accountToken: AccountToken; - deviceName: string; + deviceName?: string; } interface ILoginFailedAction { @@ -36,7 +36,7 @@ interface ICreateAccountFailed { interface IAccountCreated { type: 'ACCOUNT_CREATED'; accountToken: AccountToken; - deviceName: string; + deviceName?: string; expiry: string; } @@ -80,11 +80,11 @@ function startLogin(accountToken: AccountToken): IStartLoginAction { }; } -function loggedIn(deviceConfig: Required<IDeviceConfig>): ILoggedInAction { +function loggedIn(deviceConfig: NonNullable<DeviceConfig>): ILoggedInAction { return { type: 'LOGGED_IN', accountToken: deviceConfig.accountToken, - deviceName: deviceConfig.device.name, + deviceName: deviceConfig.device?.name, }; } @@ -120,11 +120,11 @@ function createAccountFailed(error: Error): ICreateAccountFailed { }; } -function accountCreated(deviceConfig: Required<IDeviceConfig>, expiry: string): IAccountCreated { +function accountCreated(deviceConfig: NonNullable<DeviceConfig>, expiry: string): IAccountCreated { return { type: 'ACCOUNT_CREATED', accountToken: deviceConfig.accountToken, - deviceName: deviceConfig.device.name, + deviceName: deviceConfig.device?.name, expiry, }; } diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 3b0f7774d3..61a068c08e 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -105,7 +105,7 @@ export type DaemonEvent = | { settings: ISettings } | { relayList: IRelayList } | { appVersionInfo: IAppVersionInfo } - | { deviceConfig: IDeviceConfig }; + | { deviceConfig: DeviceConfig }; export interface ITunnelStateRelayInfo { endpoint: ITunnelEndpoint; @@ -321,16 +321,23 @@ export interface IAppVersionInfo { suggestedIsBeta?: boolean; } -export interface IDeviceConfig { - accountToken?: AccountToken; - device?: IDevice; -} +export type DeviceConfig = + | undefined + | { + accountToken: AccountToken; + device?: IDevice; + }; export interface IDevice { id: string; name: string; } +export interface IDeviceRemoval { + accountToken: string; + deviceId: string; +} + export interface ISettings { allowLan: boolean; autoConnect: boolean; diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index 6b3082cdd7..6ea154bf89 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -6,7 +6,9 @@ import { BridgeState, IAccountData, IAppVersionInfo, - IDeviceConfig, + IDevice, + DeviceConfig, + IDeviceRemoval, IDnsOptions, ILocation, IRelayList, @@ -51,7 +53,7 @@ export interface IAppStateSnapshot { accountHistory?: AccountToken; tunnelState: TunnelState; settings: ISettings; - deviceConfig: IDeviceConfig; + deviceConfig: DeviceConfig; relayListPair: IRelayListPair; currentVersion: ICurrentAppVersionInfo; upgradeVersion: IAppVersionInfo; @@ -165,13 +167,15 @@ export const ipcSchema = { }, account: { '': notifyRenderer<IAccountData | undefined>(), - device: notifyRenderer<IDeviceConfig>(), + device: notifyRenderer<DeviceConfig>(), create: invoke<void, string>(), login: invoke<AccountToken, void>(), logout: invoke<void, void>(), getWwwAuthToken: invoke<void, string>(), submitVoucher: invoke<string, VoucherResponse>(), updateData: send<void>(), + listDevices: invoke<AccountToken, Array<IDevice>>(), + removeDevice: invoke<IDeviceRemoval, void>(), }, accountHistory: { '': notifyRenderer<AccountToken | undefined>(), |
