diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-08-16 15:38:40 +0300 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2019-08-16 15:38:40 +0300 |
| commit | 65553ebf473ae849b79b326861629eb08babe48e (patch) | |
| tree | 7d4b50da9869b77b228b3efbbbb956c0492a43d0 /gui | |
| parent | 95af6cd0baf777978d9892c610fe2ee2a0974ebb (diff) | |
| parent | 6180eb5f9d496ecff9f7526d63bdb15b5b1f6c45 (diff) | |
| download | mullvadvpn-65553ebf473ae849b79b326861629eb08babe48e.tar.xz mullvadvpn-65553ebf473ae849b79b326861629eb08babe48e.zip | |
Merge branch 'move-login-to-main-process'
Diffstat (limited to 'gui')
| -rw-r--r-- | gui/locales/messages.pot | 11 | ||||
| -rw-r--r-- | gui/src/main/account-data-cache.ts (renamed from gui/src/renderer/lib/account-data-cache.ts) | 2 | ||||
| -rw-r--r-- | gui/src/main/errors.ts | 6 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 112 | ||||
| -rw-r--r-- | gui/src/main/window-controller.ts | 4 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 98 | ||||
| -rw-r--r-- | gui/src/shared/ipc-event-channel.ts | 44 | ||||
| -rw-r--r-- | gui/test/account-data-cache.spec.ts | 2 |
8 files changed, 160 insertions, 119 deletions
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 24dde8b1bc..b7c2f8a411 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -8,6 +8,9 @@ msgstr "" msgid "CREATING SECURE CONNECTION" msgstr "" +msgid "Invalid account number" +msgstr "" + msgid "SECURE CONNECTION" msgstr "" @@ -273,6 +276,10 @@ msgid "Failed to apply firewall rules. The device might currently be unsecured" msgstr "" msgctxt "in-app-notifications" +msgid "Failed to resolve host of custom tunnel. Consider changing the settings" +msgstr "" + +msgctxt "in-app-notifications" msgid "Failed to set system DNS server" msgstr "" @@ -323,6 +330,10 @@ msgctxt "in-app-notifications" msgid "UPDATE AVAILABLE" msgstr "" +msgctxt "in-app-notifications" +msgid "WireGuard key not published to our servers. You can manage your key in Advanced settings." +msgstr "" + #. The in-app banner displayed to the user when the running app becomes unsupported. #. Available placeholders: #. %(version)s - the newest available version of the app diff --git a/gui/src/renderer/lib/account-data-cache.ts b/gui/src/main/account-data-cache.ts index 51154c9792..24f9c0453a 100644 --- a/gui/src/renderer/lib/account-data-cache.ts +++ b/gui/src/main/account-data-cache.ts @@ -1,5 +1,5 @@ import log from 'electron-log'; -import { AccountToken, IAccountData } from '../../shared/daemon-rpc-types'; +import { AccountToken, IAccountData } from '../shared/daemon-rpc-types'; export enum AccountFetchRetryAction { stop, diff --git a/gui/src/main/errors.ts b/gui/src/main/errors.ts index 85adf965a5..261ee7a164 100644 --- a/gui/src/main/errors.ts +++ b/gui/src/main/errors.ts @@ -1,9 +1,3 @@ -export class NoCreditError extends Error { - constructor() { - super("Account doesn't have enough credit available for connection"); - } -} - export class NoDaemonError extends Error { constructor() { super('Could not connect to Mullvad daemon'); diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 1d766629cf..d7e13615f9 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -8,6 +8,7 @@ import { AccountToken, BridgeState, DaemonEvent, + IAccountData, IAppVersionInfo, ILocation, IRelayList, @@ -27,6 +28,7 @@ import { getRendererLogFile, setupLogging, } from '../shared/logging'; +import AccountDataCache, { AccountFetchRetryAction } from './account-data-cache'; import { getOpenAtLogin, setOpenAtLogin } from './autostart'; import { ConnectionObserver, @@ -34,6 +36,7 @@ import { ResponseParseError, SubscriptionListener, } from './daemon-rpc'; +import { InvalidAccountError } from './errors'; import GuiSettings from './gui-settings'; import NotificationController from './notification-controller'; import { resolveBin } from './proc'; @@ -63,6 +66,8 @@ export interface IAppUpgradeInfo extends IAppVersionInfo { upToDate: boolean; } +type AccountVerification = { status: 'verified' } | { status: 'deferred'; error: Error }; + class ApplicationMain { private notificationController = new NotificationController(); private windowController?: WindowController; @@ -73,6 +78,7 @@ class ApplicationMain { private connectedToDaemon = false; private quitStage = AppQuitStage.unready; + private accountData?: IAccountData = undefined; private accountHistory: AccountToken[] = []; private tunnelState: TunnelState = { state: 'disconnected' }; private settings: ISettings = { @@ -136,6 +142,19 @@ class ApplicationMain { private wireguardPublicKey?: string; + private accountDataCache = new AccountDataCache( + (accountToken) => { + return this.daemonRpc.getAccountData(accountToken); + }, + (accountData) => { + this.accountData = accountData; + + if (this.windowController) { + IpcMainEventChannel.account.notify(this.windowController.webContents, accountData); + } + }, + ); + public run() { // Since electron's GPU blacklists are broken, GPU acceleration won't work on older distros if (process.platform === 'linux') { @@ -554,6 +573,10 @@ class ApplicationMain { if (this.windowController) { IpcMainEventChannel.tunnel.notify(this.windowController.webContents, newState); } + + if (this.accountData) { + this.detectStaleAccountExpiry(newState, new Date(this.accountData.expiry)); + } } private setSettings(newSettings: ISettings) { @@ -562,6 +585,9 @@ 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) { this.updateAccountHistory(); this.fetchWireguardKey(); @@ -840,7 +866,7 @@ class ApplicationMain { // cancel notifications when window appears this.notificationController.cancelPendingNotifications(); - windowController.send('window-shown'); + this.updateAccountExpiryIfNeeded(); }); windowController.window.on('hide', () => { @@ -854,6 +880,7 @@ class ApplicationMain { locale: this.locale, isConnected: this.connectedToDaemon, autoStart: getOpenAtLogin(), + accountData: this.accountData, accountHistory: this.accountHistory, tunnelState: this.tunnelState, settings: this.settings, @@ -907,13 +934,8 @@ class ApplicationMain { this.guiSettings.monochromaticIcon = monochromaticIcon; }); - IpcMainEventChannel.account.handleSet((token: AccountToken) => - this.daemonRpc.setAccount(token), - ); - IpcMainEventChannel.account.handleUnset(() => this.daemonRpc.setAccount()); - IpcMainEventChannel.account.handleGetData((token: AccountToken) => - this.daemonRpc.getAccountData(token), - ); + IpcMainEventChannel.account.handleLogin((token: AccountToken) => this.login(token)); + IpcMainEventChannel.account.handleLogout(() => this.logout()); IpcMainEventChannel.accountHistory.handleRemoveItem(async (token: AccountToken) => { await this.daemonRpc.removeAccountFromHistory(token); @@ -1003,6 +1025,80 @@ 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}`); + } + + await this.daemonRpc.setAccount(accountToken); + } catch (error) { + log.error(`Failed to login: ${error.message}`); + + if (error instanceof InvalidAccountError) { + throw Error(messages.gettext('Invalid account number')); + } else { + throw error; + } + } + } + + private async logout(): Promise<void> { + try { + await this.daemonRpc.setAccount(); + } catch (error) { + log.info(`Failed to logout: ${error.message}`); + + throw error; + } + } + + private verifyAccount(accountToken: AccountToken): Promise<AccountVerification> { + return new Promise((resolve, reject) => { + this.accountDataCache.invalidate(); + this.accountDataCache.fetch(accountToken, { + onFinish: () => resolve({ status: 'verified' }), + onError: (error): AccountFetchRetryAction => { + if (error instanceof InvalidAccountError) { + reject(error); + return AccountFetchRetryAction.stop; + } else { + resolve({ status: 'deferred', error }); + return AccountFetchRetryAction.retry; + } + }, + }); + }); + } + + private updateAccountDataOnAccountChange(oldAccount?: string, newAccount?: string) { + if (oldAccount && !newAccount) { + this.accountDataCache.invalidate(); + } else if (!oldAccount && newAccount) { + this.accountDataCache.fetch(newAccount); + } else if (oldAccount && newAccount && oldAccount !== newAccount) { + this.accountDataCache.fetch(newAccount); + } + } + + private updateAccountExpiryIfNeeded() { + if (this.connectedToDaemon && this.settings.accountToken) { + this.accountDataCache.fetch(this.settings.accountToken); + } + } + + private detectStaleAccountExpiry(tunnelState: TunnelState, accountExpiry: Date) { + const hasExpired = new Date() >= accountExpiry; + + // It's likely that the account expiry is stale if the daemon managed to establish the tunnel. + if (tunnelState.state === 'connected' && hasExpired) { + log.info('Detected the stale account expiry.'); + this.accountDataCache.invalidate(); + } + } + private async updateAccountHistory(): Promise<void> { try { this.setAccountHistory(await this.daemonRpc.getAccountHistory()); diff --git a/gui/src/main/window-controller.ts b/gui/src/main/window-controller.ts index 3822ecb23f..e681b2a595 100644 --- a/gui/src/main/window-controller.ts +++ b/gui/src/main/window-controller.ts @@ -1,4 +1,5 @@ import { BrowserWindow, Display, screen, Tray, WebContents } from 'electron'; +import { IpcMainEventChannel } from '../shared/ipc-event-channel'; interface IPosition { x: number; @@ -201,7 +202,8 @@ export default class WindowController { private notifyUpdateWindowShape() { const shapeParameters = this.windowPositioning.getWindowShapeParameters(this.windowValue); - this.windowValue.webContents.send('update-window-shape', shapeParameters); + + IpcMainEventChannel.windowShape.notify(this.windowValue.webContents, shapeParameters); } // Installs display event handlers to update the window position on any changes in the display or diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index f4a3e052dc..a1f5560ce5 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -10,7 +10,6 @@ import * as React from 'react'; import { Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { InvalidAccountError } from '../main/errors'; import ErrorBoundary from './components/ErrorBoundary'; import AppRoutes from './routes'; @@ -22,17 +21,15 @@ import userInterfaceActions from './redux/userinterface/actions'; import versionActions from './redux/version/actions'; import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main'; -import { IWindowShapeParameters } from '../main/window-controller'; import { cities, countries, loadTranslations, messages, relayLocations } from '../shared/gettext'; import { IGuiSettingsState } from '../shared/gui-settings-state'; import { IpcRendererEventChannel } from '../shared/ipc-event-channel'; import { getRendererLogFile, setupLogging } from '../shared/logging'; -import AccountDataCache, { AccountFetchRetryAction } from './lib/account-data-cache'; -import AccountExpiry from './lib/account-expiry'; import { AccountToken, BridgeState, + IAccountData, ILocation, IRelayList, ISettings, @@ -43,8 +40,6 @@ import { TunnelState, } from '../shared/daemon-rpc-types'; -type AccountVerification = { status: 'verified' } | { status: 'deferred'; error: Error }; - export default class AppRenderer { private memoryHistory = createMemoryHistory(); private reduxStore = configureStore(this.memoryHistory); @@ -62,20 +57,11 @@ export default class AppRenderer { this.reduxStore.dispatch, ), }; - private accountDataCache = new AccountDataCache( - (accountToken) => { - return IpcRendererEventChannel.account.getData(accountToken); - }, - (accountData) => { - this.setAccountExpiry(accountData && accountData.expiry); - }, - ); private locale: string; private tunnelState: TunnelState; private settings: ISettings; private guiSettings: IGuiSettingsState; - private accountExpiry?: AccountExpiry; private connectedToDaemon = false; private autoConnected = false; private doingLogin = false; @@ -84,18 +70,9 @@ export default class AppRenderer { constructor() { setupLogging(getRendererLogFile()); - ipcRenderer.on( - 'update-window-shape', - (_event: Electron.Event, shapeParams: IWindowShapeParameters) => { - if (typeof shapeParams.arrowPosition === 'number') { - this.reduxActions.userInterface.updateWindowArrowPosition(shapeParams.arrowPosition); - } - }, - ); - - ipcRenderer.on('window-shown', () => { - if (this.connectedToDaemon) { - this.updateAccountExpiry(); + IpcRendererEventChannel.windowShape.listen((windowShapeParams) => { + if (typeof windowShapeParams.arrowPosition === 'number') { + this.reduxActions.userInterface.updateWindowArrowPosition(windowShapeParams.arrowPosition); } }); @@ -107,6 +84,10 @@ export default class AppRenderer { this.onDaemonDisconnected(errorMessage ? new Error(errorMessage) : undefined); }); + IpcRendererEventChannel.account.listen((newAccountData?: IAccountData) => { + this.setAccountExpiry(newAccountData && newAccountData.expiry); + }); + IpcRendererEventChannel.accountHistory.listen((newAccountHistory: AccountToken[]) => { this.setAccountHistory(newAccountHistory); }); @@ -114,10 +95,6 @@ export default class AppRenderer { IpcRendererEventChannel.tunnel.listen((newState: TunnelState) => { this.setTunnelState(newState); this.updateBlockedState(newState, this.settings.blockWhenDisconnected); - - if (this.accountExpiry) { - this.detectStaleAccountExpiry(newState, this.accountExpiry); - } }); IpcRendererEventChannel.settings.listen((newSettings: ISettings) => { @@ -168,6 +145,7 @@ export default class AppRenderer { this.settings = initialState.settings; this.guiSettings = initialState.guiSettings; + this.setAccountExpiry(initialState.accountData && initialState.accountData.expiry); this.setAccountHistory(initialState.accountHistory); this.setSettings(initialState.settings); this.setTunnelState(initialState.tunnelState); @@ -223,13 +201,7 @@ export default class AppRenderer { this.doingLogin = true; try { - const verification = await this.verifyAccount(accountToken); - - if (verification.status === 'deferred') { - log.warn(`Failed to get account data, logging in anyway: ${verification.error.message}`); - } - - await IpcRendererEventChannel.account.set(accountToken); + await IpcRendererEventChannel.account.login(accountToken); // Redirect the user after some time to allow for the 'Logged in' screen to be visible this.loginTimer = global.setTimeout(async () => { @@ -243,33 +215,13 @@ export default class AppRenderer { } }, 1000); } catch (error) { - log.error('Failed to log in,', error.message); - actions.account.loginFailed(error); } } - public verifyAccount(accountToken: AccountToken): Promise<AccountVerification> { - return new Promise((resolve, reject) => { - this.accountDataCache.invalidate(); - this.accountDataCache.fetch(accountToken, { - onFinish: () => resolve({ status: 'verified' }), - onError: (error): AccountFetchRetryAction => { - if (error.message === new InvalidAccountError().message) { - reject(error); - return AccountFetchRetryAction.stop; - } else { - resolve({ status: 'deferred', error }); - return AccountFetchRetryAction.retry; - } - }, - }); - }); - } - public async logout() { try { - await IpcRendererEventChannel.account.unset(); + await IpcRendererEventChannel.account.logout(); } catch (e) { log.info('Failed to logout: ', e.message); } @@ -295,12 +247,6 @@ export default class AppRenderer { return IpcRendererEventChannel.settings.updateRelaySettings(relaySettings); } - public updateAccountExpiry() { - if (this.settings.accountToken) { - this.accountDataCache.fetch(this.settings.accountToken); - } - } - public async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { return IpcRendererEventChannel.accountHistory.removeItem(accountToken); } @@ -544,21 +490,12 @@ export default class AppRenderer { private handleAccountChange(oldAccount?: string, newAccount?: string) { if (oldAccount && !newAccount) { - this.accountDataCache.invalidate(); - if (this.loginTimer) { clearTimeout(this.loginTimer); } - this.memoryHistory.replace('/login'); - } else if (!oldAccount && newAccount) { - this.accountDataCache.fetch(newAccount); - - if (!this.doingLogin) { - this.memoryHistory.replace('/connect'); - } - } else if (oldAccount && newAccount && oldAccount !== newAccount) { - this.accountDataCache.fetch(newAccount); + } else if (!oldAccount && newAccount && !this.doingLogin) { + this.memoryHistory.replace('/connect'); } this.doingLogin = false; @@ -604,18 +541,9 @@ export default class AppRenderer { } private setAccountExpiry(expiry?: string) { - this.accountExpiry = expiry ? new AccountExpiry(expiry, this.locale) : undefined; this.reduxActions.account.updateAccountExpiry(expiry); } - private detectStaleAccountExpiry(tunnelState: TunnelState, accountExpiry: AccountExpiry) { - // It's likely that the account expiry is stale if the daemon managed to establish the tunnel. - if (tunnelState.state === 'connected' && accountExpiry.hasExpired()) { - log.info('Detected the stale account expiry.'); - this.accountDataCache.invalidate(); - } - } - private storeAutoStart(autoStart: boolean) { this.reduxActions.settings.updateAutoStart(autoStart); } diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts index 2bb594f5e6..a11d41bf10 100644 --- a/gui/src/shared/ipc-event-channel.ts +++ b/gui/src/shared/ipc-event-channel.ts @@ -5,6 +5,7 @@ import * as uuid from 'uuid'; import { IGuiSettingsState } from './gui-settings-state'; import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main/index'; +import { IWindowShapeParameters } from '../main/window-controller'; import { AccountToken, BridgeState, @@ -21,6 +22,7 @@ export interface IAppStateSnapshot { locale: string; isConnected: boolean; autoStart: boolean; + accountData?: IAccountData; accountHistory: AccountToken[]; tunnelState: TunnelState; settings: ISettings; @@ -86,16 +88,14 @@ interface IGuiSettingsHandlers extends ISender<IGuiSettingsState> { handleMonochromaticIcon(fn: (monochromaticIcon: boolean) => void): void; } -interface IAccountHandlers { - handleSet(fn: (token: AccountToken) => Promise<void>): void; - handleUnset(fn: () => Promise<void>): void; - handleGetData(fn: (token: AccountToken) => Promise<IAccountData>): void; +interface IAccountHandlers extends ISender<IAccountData | undefined> { + handleLogin(fn: (token: AccountToken) => Promise<void>): void; + handleLogout(fn: () => Promise<void>): void; } -interface IAccountMethods { - set(token: AccountToken): Promise<void>; - unset(): Promise<void>; - getData(token: AccountToken): Promise<IAccountData>; +interface IAccountMethods extends IReceiver<IAccountData | undefined> { + login(token: AccountToken): Promise<void>; + logout(): Promise<void>; } interface IAccountHistoryHandlers extends ISender<AccountToken[]> { @@ -128,6 +128,8 @@ interface IWireguardKeyHandlers extends ISender<string | undefined> { /// Events names +const WINDOW_SHAPE_CHANGED = 'window-shape-changed'; + const DAEMON_CONNECTED = 'daemon-connected'; const DAEMON_DISCONNECTED = 'daemon-disconnected'; @@ -159,9 +161,9 @@ const GET_APP_STATE = 'get-app-state'; const ACCOUNT_HISTORY_CHANGED = 'account-history-changed'; const REMOVE_ACCOUNT_HISTORY_ITEM = 'remove-account-history-item'; -const SET_ACCOUNT = 'set-account'; -const UNSET_ACCOUNT = 'unset-account'; -const GET_ACCOUNT_DATA = 'get-account-data'; +const DO_LOGIN = 'do-login'; +const DO_LOGOUT = 'do-logout'; +const ACCOUNT_DATA_CHANGED = 'account-data-changed'; const AUTO_START_CHANGED = 'auto-start-changed'; const SET_AUTO_START = 'set-auto-start'; @@ -184,6 +186,10 @@ export class IpcRendererEventChannel { }, }; + public static windowShape: IReceiver<IWindowShapeParameters> = { + listen: listen(WINDOW_SHAPE_CHANGED), + }; + public static daemonConnected: IReceiver<void> = { listen: listen(DAEMON_CONNECTED), }; @@ -238,9 +244,9 @@ export class IpcRendererEventChannel { }; public static account: IAccountMethods = { - set: requestSender(SET_ACCOUNT), - unset: requestSender(UNSET_ACCOUNT), - getData: requestSender(GET_ACCOUNT_DATA), + listen: listen(ACCOUNT_DATA_CHANGED), + login: requestSender(DO_LOGIN), + logout: requestSender(DO_LOGOUT), }; public static accountHistory: IAccountHistoryMethods = { @@ -265,6 +271,10 @@ export class IpcMainEventChannel { }, }; + public static windowShape: ISender<IWindowShapeParameters> = { + notify: sender<IWindowShapeParameters>(WINDOW_SHAPE_CHANGED), + }; + public static daemonConnected: ISenderVoid = { notify: senderVoid(DAEMON_CONNECTED), }; @@ -319,9 +329,9 @@ export class IpcMainEventChannel { }; public static account: IAccountHandlers = { - handleSet: requestHandler(SET_ACCOUNT), - handleUnset: requestHandler(UNSET_ACCOUNT), - handleGetData: requestHandler(GET_ACCOUNT_DATA), + notify: sender<IAccountData | undefined>(ACCOUNT_DATA_CHANGED), + handleLogin: requestHandler(DO_LOGIN), + handleLogout: requestHandler(DO_LOGOUT), }; public static accountHistory: IAccountHistoryHandlers = { diff --git a/gui/test/account-data-cache.spec.ts b/gui/test/account-data-cache.spec.ts index f9838a6dbb..c3ef53dc51 100644 --- a/gui/test/account-data-cache.spec.ts +++ b/gui/test/account-data-cache.spec.ts @@ -1,4 +1,4 @@ -import AccountDataCache, { AccountFetchRetryAction } from '../src/renderer/lib/account-data-cache'; +import AccountDataCache, { AccountFetchRetryAction } from '../src/main/account-data-cache'; import { IAccountData } from '../src/shared/daemon-rpc-types'; import * as sinon from 'sinon'; import { expect, spy } from 'chai'; |
