summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-08-16 15:38:40 +0300
committerAndrej Mihajlov <and@mullvad.net>2019-08-16 15:38:40 +0300
commit65553ebf473ae849b79b326861629eb08babe48e (patch)
tree7d4b50da9869b77b228b3efbbbb956c0492a43d0
parent95af6cd0baf777978d9892c610fe2ee2a0974ebb (diff)
parent6180eb5f9d496ecff9f7526d63bdb15b5b1f6c45 (diff)
downloadmullvadvpn-65553ebf473ae849b79b326861629eb08babe48e.tar.xz
mullvadvpn-65553ebf473ae849b79b326861629eb08babe48e.zip
Merge branch 'move-login-to-main-process'
-rw-r--r--gui/locales/messages.pot11
-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.ts6
-rw-r--r--gui/src/main/index.ts112
-rw-r--r--gui/src/main/window-controller.ts4
-rw-r--r--gui/src/renderer/app.tsx98
-rw-r--r--gui/src/shared/ipc-event-channel.ts44
-rw-r--r--gui/test/account-data-cache.spec.ts2
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';