summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-07-16 16:33:56 +0200
committerAndrej Mihajlov <and@mullvad.net>2018-07-16 16:33:56 +0200
commit7aa861495ffe90e0b5a9b9c67b8ccf0ad62c2882 (patch)
tree212056880e20c6a3bdcc9e1e807bd75b1a4199af
parent901d0135651d010088105e98204af7618347389d (diff)
parent6c0618e8b500499576cb1b6969ce268c90df01b1 (diff)
downloadmullvadvpn-7aa861495ffe90e0b5a9b9c67b8ccf0ad62c2882.tar.xz
mullvadvpn-7aa861495ffe90e0b5a9b9c67b8ccf0ad62c2882.zip
Merge branch 'auto-connect-settings'
-rw-r--r--CHANGELOG.md1
-rw-r--r--app/app.js31
-rw-r--r--app/components/Preferences.js56
-rw-r--r--app/containers/PreferencesPage.js21
-rw-r--r--app/lib/daemon-rpc.js15
-rw-r--r--app/lib/platform.android.js10
-rw-r--r--app/lib/platform.js33
-rw-r--r--app/redux/settings/actions.js22
-rw-r--r--app/redux/settings/reducers.js8
-rw-r--r--test/components/Preferences.spec.js6
-rw-r--r--test/components/SelectLocation.spec.js1
-rw-r--r--test/components/Settings.spec.js1
-rw-r--r--test/mocks/rpc.js103
13 files changed, 185 insertions, 123 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0da782c87e..10c76b6b7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@ Line wrap the file at 100 chars. Th
- Add a unique UUID to problem reports. Makes it easier for Mullvad support staff to find reports.
- Add "auto-connect" setting in daemon, and make it configurable from CLI. Determines if the daemon
should secure the network and start establishing a tunnel directly when it starts on boot.
+- Add "auto-connect" and "auto-start" options to the application preferences view.
#### Windows
- Include version information (meta data) in executables and DLLs.
diff --git a/app/app.js b/app/app.js
index 9cbc368cde..76d92a1b55 100644
--- a/app/app.js
+++ b/app/app.js
@@ -133,10 +133,15 @@ export default class AppRenderer {
// Redirect the user after some time to allow for
// the 'Login Successful' screen to be visible
- setTimeout(() => {
+ setTimeout(async () => {
actions.history.push('/connect');
- log.debug('Autoconnecting...');
- this.connectTunnel();
+
+ try {
+ log.debug('Auto-connecting the tunnel...');
+ await this.connectTunnel();
+ } catch (error) {
+ log.error(`Failed to auto-connect the tunnel: ${error.message}`);
+ }
}, 1000);
} catch (error) {
log.error('Failed to log in,', error.message);
@@ -338,12 +343,24 @@ export default class AppRenderer {
actions.settings.updateAllowLan(allowLan);
}
+ async setAutoConnect(autoConnect: boolean) {
+ const actions = this._reduxActions;
+ await this._daemonRpc.setAutoConnect(autoConnect);
+ actions.settings.updateAutoConnect(autoConnect);
+ }
+
async _fetchAllowLan() {
const actions = this._reduxActions;
const allowLan = await this._daemonRpc.getAllowLan();
actions.settings.updateAllowLan(allowLan);
}
+ async _fetchAutoConnect() {
+ const actions = this._reduxActions;
+ const autoConnect = await this._daemonRpc.getAutoConnect();
+ actions.settings.updateAutoConnect(autoConnect);
+ }
+
async _fetchSecurityState() {
const securityState = await this._daemonRpc.getState();
const connectionState = this._securityStateToConnectionState(securityState);
@@ -402,13 +419,6 @@ export default class AppRenderer {
} catch (error) {
log.error(`Cannot fetch initial state: ${error.message}`);
}
-
- // auto connect the tunnel
- try {
- await this.connectTunnel();
- } catch (error) {
- log.error(`Cannot autoconnect the tunnel: ${error.message}`);
- }
}
async _onCloseConnection(error: ?Error) {
@@ -487,6 +497,7 @@ export default class AppRenderer {
this.fetchRelaySettings(),
this._fetchRelayLocations(),
this._fetchAllowLan(),
+ this._fetchAutoConnect(),
this._fetchLocation(),
this._fetchAccountHistory(),
]);
diff --git a/app/components/Preferences.js b/app/components/Preferences.js
index dfe098999a..26b2e4eb16 100644
--- a/app/components/Preferences.js
+++ b/app/components/Preferences.js
@@ -7,12 +7,29 @@ import Switch from './Switch';
import styles from './PreferencesStyles';
export type PreferencesProps = {
+ autoConnect: boolean,
allowLan: boolean,
- onChangeAllowLan: (boolean) => void,
+ getAutoStart: () => boolean,
+ setAutoStart: (boolean) => void,
+ setAutoConnect: (boolean) => void,
+ setAllowLan: (boolean) => void,
onClose: () => void,
};
-export default class Preferences extends Component<PreferencesProps> {
+type State = {
+ autoStart: boolean,
+};
+
+export default class Preferences extends Component<PreferencesProps, State> {
+ state = {
+ autoStart: false,
+ };
+
+ constructor(props: PreferencesProps) {
+ super();
+ this.state.autoStart = props.getAutoStart();
+ }
+
render() {
return (
<Layout>
@@ -36,10 +53,38 @@ export default class Preferences extends Component<PreferencesProps> {
<View style={styles.preferences__content}>
<View style={styles.preferences__cell}>
<View style={styles.preferences__cell_label_container}>
+ <Text style={styles.preferences__cell_label}>Auto-connect</Text>
+ </View>
+ <View style={styles.preferences__cell_accessory}>
+ <Switch isOn={this.props.autoConnect} onChange={this.props.setAutoConnect} />
+ </View>
+ </View>
+ <View style={styles.preferences__cell_footer}>
+ <Text style={styles.preferences__cell_footer_label}>
+ {'Automatically connect the VPN at login to the system.'}
+ </Text>
+ </View>
+
+ <View style={styles.preferences__cell}>
+ <View style={styles.preferences__cell_label_container}>
+ <Text style={styles.preferences__cell_label}>Auto-start</Text>
+ </View>
+ <View style={styles.preferences__cell_accessory}>
+ <Switch isOn={this.state.autoStart} onChange={this._onChangeAutoStart} />
+ </View>
+ </View>
+ <View style={styles.preferences__cell_footer}>
+ <Text style={styles.preferences__cell_footer_label}>
+ {'Automatically open Mullvad VPN at login to the system.'}
+ </Text>
+ </View>
+
+ <View style={styles.preferences__cell}>
+ <View style={styles.preferences__cell_label_container}>
<Text style={styles.preferences__cell_label}>Local network sharing</Text>
</View>
<View style={styles.preferences__cell_accessory}>
- <Switch isOn={this.props.allowLan} onChange={this.props.onChangeAllowLan} />
+ <Switch isOn={this.props.allowLan} onChange={this.props.setAllowLan} />
</View>
</View>
<View style={styles.preferences__cell_footer}>
@@ -56,4 +101,9 @@ export default class Preferences extends Component<PreferencesProps> {
</Layout>
);
}
+
+ _onChangeAutoStart = (autoStart: boolean) => {
+ this.props.setAutoStart(autoStart);
+ this.setState({ autoStart });
+ };
}
diff --git a/app/containers/PreferencesPage.js b/app/containers/PreferencesPage.js
index 6ee813209a..e11655f3a4 100644
--- a/app/containers/PreferencesPage.js
+++ b/app/containers/PreferencesPage.js
@@ -4,11 +4,13 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { push } from 'react-router-redux';
import Preferences from '../components/Preferences';
+import { log, getOpenAtLogin, setOpenAtLogin } from '../lib/platform';
import type { ReduxState, ReduxDispatch } from '../redux/store';
import type { SharedRouteProps } from '../routes';
const mapStateToProps = (state: ReduxState) => ({
+ autoConnect: state.settings.autoConnect,
allowLan: state.settings.allowLan,
});
@@ -16,7 +18,24 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) =>
const { push: pushHistory } = bindActionCreators({ push }, dispatch);
return {
onClose: () => pushHistory('/settings'),
- onChangeAllowLan: (allowLan) => {
+ getAutoStart: () => {
+ return getOpenAtLogin();
+ },
+ setAutoStart: async (autoStart) => {
+ try {
+ await setOpenAtLogin(autoStart);
+ } catch (error) {
+ log.error(`Cannot set auto-start: ${error.message}`);
+ }
+ },
+ setAutoConnect: async (autoConnect) => {
+ try {
+ props.app.setAutoConnect(autoConnect);
+ } catch (error) {
+ log.error(`Cannot set auto-connect: ${error.message}`);
+ }
+ },
+ setAllowLan: (allowLan) => {
props.app.setAllowLan(allowLan);
},
};
diff --git a/app/lib/daemon-rpc.js b/app/lib/daemon-rpc.js
index 2ef3d06e13..72421ed88c 100644
--- a/app/lib/daemon-rpc.js
+++ b/app/lib/daemon-rpc.js
@@ -198,6 +198,8 @@ export interface DaemonRpcProtocol {
getRelaySettings(): Promise<RelaySettings>;
setAllowLan(boolean): Promise<void>;
getAllowLan(): Promise<boolean>;
+ setAutoConnect(boolean): Promise<void>;
+ getAutoConnect(): Promise<boolean>;
connectTunnel(): Promise<void>;
disconnectTunnel(): Promise<void>;
getLocation(): Promise<Location>;
@@ -332,6 +334,19 @@ export class DaemonRpc implements DaemonRpcProtocol {
}
}
+ async setAutoConnect(autoConnect: boolean): Promise<void> {
+ await this._transport.send('set_auto_connect', [autoConnect]);
+ }
+
+ async getAutoConnect(): Promise<boolean> {
+ const response = await this._transport.send('get_auto_connect');
+ if (typeof response === 'boolean') {
+ return response;
+ } else {
+ throw new ResponseParseError('Invalid response from get_auto_connect', null);
+ }
+ }
+
async connectTunnel(): Promise<void> {
await this._transport.send('connect');
}
diff --git a/app/lib/platform.android.js b/app/lib/platform.android.js
index 1dc450b359..5b3abb4496 100644
--- a/app/lib/platform.android.js
+++ b/app/lib/platform.android.js
@@ -9,6 +9,14 @@ const getAppVersion = () => {
return version;
};
+const getOpenAtLogin = () => {
+ throw new Error('Not implemented');
+};
+
+const setOpenAtLogin = (_autoStart: boolean) => {
+ throw new Error('Not implemented');
+};
+
const exit = () => {
BackHandler.exitApp();
};
@@ -21,4 +29,4 @@ const openItem = (path: string) => {
MobileAppBridge.openItem(path);
};
-export { log, exit, openLink, openItem, getAppVersion };
+export { log, exit, openLink, openItem, getAppVersion, getOpenAtLogin, setOpenAtLogin };
diff --git a/app/lib/platform.js b/app/lib/platform.js
index ecbd32a27a..0e46341149 100644
--- a/app/lib/platform.js
+++ b/app/lib/platform.js
@@ -1,6 +1,10 @@
// @flow
import { remote, shell } from 'electron';
import electronLog from 'electron-log';
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+
+const execFileAsync = promisify(execFile);
const log = electronLog;
@@ -8,6 +12,33 @@ const getAppVersion = () => {
return remote.app.getVersion();
};
+const getOpenAtLogin = () => {
+ return remote.app.getLoginItemSettings().openAtLogin;
+};
+
+const setOpenAtLogin = async (openAtLogin: boolean) => {
+ // setLoginItemSettings is broken on macOS and cannot delete login items.
+ // Issue: https://github.com/electron/electron/issues/10880
+ if (process.platform === 'darwin' && openAtLogin === false) {
+ // process.execPath in renderer process points to the sub-bundle of Electron Helper.
+ // This regular expression extracts the path to the app bundle, which is the first occurrence of
+ // file with .app extension.
+ const matches = process.execPath.match(/([a-z0-9 ]+)\.app/i);
+ if (matches && matches.length > 1) {
+ const bundleName = matches[1];
+ const appleScript = `on run argv
+ set itemName to item 1 of argv
+ tell application "System Events" to delete login item itemName
+ end run`;
+ await execFileAsync('osascript', ['-e', appleScript, bundleName]);
+ } else {
+ log.error(`Cannot extract the app bundle name from ${process.execPath}`);
+ }
+ } else {
+ remote.app.setLoginItemSettings({ openAtLogin });
+ }
+};
+
const exit = () => {
remote.app.quit();
};
@@ -20,4 +51,4 @@ const openItem = (path: string) => {
shell.openItem(path);
};
-export { log, exit, openLink, openItem, getAppVersion };
+export { log, exit, openLink, openItem, getAppVersion, getOpenAtLogin, setOpenAtLogin };
diff --git a/app/redux/settings/actions.js b/app/redux/settings/actions.js
index 0d3e252778..fd3ad651d9 100644
--- a/app/redux/settings/actions.js
+++ b/app/redux/settings/actions.js
@@ -12,12 +12,21 @@ export type UpdateRelayLocationsAction = {
relayLocations: Array<RelayLocationRedux>,
};
+export type UpdateAutoConnectAction = {
+ type: 'UPDATE_AUTO_CONNECT',
+ autoConnect: boolean,
+};
+
export type UpdateAllowLanAction = {
type: 'UPDATE_ALLOW_LAN',
allowLan: boolean,
};
-export type SettingsAction = UpdateRelayAction | UpdateRelayLocationsAction | UpdateAllowLanAction;
+export type SettingsAction =
+ | UpdateRelayAction
+ | UpdateRelayLocationsAction
+ | UpdateAutoConnectAction
+ | UpdateAllowLanAction;
function updateRelay(relay: RelaySettingsRedux): UpdateRelayAction {
return {
@@ -35,11 +44,18 @@ function updateRelayLocations(
};
}
+function updateAutoConnect(autoConnect: boolean): UpdateAutoConnectAction {
+ return {
+ type: 'UPDATE_AUTO_CONNECT',
+ autoConnect,
+ };
+}
+
function updateAllowLan(allowLan: boolean): UpdateAllowLanAction {
return {
type: 'UPDATE_ALLOW_LAN',
- allowLan: allowLan,
+ allowLan,
};
}
-export default { updateRelay, updateRelayLocations, updateAllowLan };
+export default { updateRelay, updateRelayLocations, updateAutoConnect, updateAllowLan };
diff --git a/app/redux/settings/reducers.js b/app/redux/settings/reducers.js
index 4a25bf9502..2bd5798aa5 100644
--- a/app/redux/settings/reducers.js
+++ b/app/redux/settings/reducers.js
@@ -37,6 +37,7 @@ export type RelayLocationRedux = {
export type SettingsReduxState = {
relaySettings: RelaySettingsRedux,
relayLocations: Array<RelayLocationRedux>,
+ autoConnect: boolean,
allowLan: boolean,
};
@@ -49,6 +50,7 @@ const initialState: SettingsReduxState = {
},
},
relayLocations: [],
+ autoConnect: false,
allowLan: false,
};
@@ -75,6 +77,12 @@ export default function(
allowLan: action.allowLan,
};
+ case 'UPDATE_AUTO_CONNECT':
+ return {
+ ...state,
+ autoConnect: action.autoConnect,
+ };
+
default:
return state;
}
diff --git a/test/components/Preferences.spec.js b/test/components/Preferences.spec.js
index 280b24ccd7..f8c38f9493 100644
--- a/test/components/Preferences.spec.js
+++ b/test/components/Preferences.spec.js
@@ -17,7 +17,11 @@ describe('components/Preferences', () => {
function makeProps(props) {
return {
onClose: () => {},
- onChangeAllowLan: () => {},
+ setAutoConnect: () => {},
+ setAutoStart: (_autoStart) => Promise.resolve(),
+ getAutoStart: () => false,
+ setAllowLan: () => {},
+ allowAutoConnect: false,
allowLan: false,
...props,
};
diff --git a/test/components/SelectLocation.spec.js b/test/components/SelectLocation.spec.js
index c9a1f08864..dbfe15221a 100644
--- a/test/components/SelectLocation.spec.js
+++ b/test/components/SelectLocation.spec.js
@@ -39,6 +39,7 @@ describe('components/SelectLocation', () => {
],
},
],
+ autoConnect: false,
allowLan: false,
};
diff --git a/test/components/Settings.spec.js b/test/components/Settings.spec.js
index ffdf0bf545..2a152e1cc8 100644
--- a/test/components/Settings.spec.js
+++ b/test/components/Settings.spec.js
@@ -42,6 +42,7 @@ describe('components/Settings', () => {
},
},
relayLocations: [],
+ autoConnect: false,
allowLan: false,
};
diff --git a/test/mocks/rpc.js b/test/mocks/rpc.js
deleted file mode 100644
index 4b37ca35aa..0000000000
--- a/test/mocks/rpc.js
+++ /dev/null
@@ -1,103 +0,0 @@
-// @flow
-import type {
- DaemonRpcProtocol,
- AccountToken,
- AccountData,
- BackendState,
-} from '../../app/lib/daemon-rpc';
-
-interface MockRpc {
- sendNewState: (BackendState) => void;
- -getAccountData: (AccountToken) => Promise<AccountData>;
- -connectTunnel: () => Promise<void>;
- -getAccount: () => Promise<?AccountToken>;
- -authenticate: (string) => Promise<void>;
-}
-
-export function newMockRpc() {
- const stateListeners = [];
- const openListeners = [];
- const closeListeners = [];
-
- const mockIpc: DaemonRpcProtocol & MockRpc = {
- setConnectionString: (_str: string) => {},
- getAccountData: (accountToken) =>
- Promise.resolve({
- accountToken: accountToken,
- expiry: '',
- }),
- getRelayLocations: () =>
- Promise.resolve({
- countries: [],
- }),
- getAccount: () => Promise.resolve('1111'),
- setAccount: () => Promise.resolve(),
- updateRelaySettings: () => Promise.resolve(),
- getRelaySettings: () =>
- Promise.resolve({
- custom_tunnel_endpoint: {
- host: 'www.example.com',
- tunnel: {
- openvpn: {
- port: 1301,
- protocol: 'udp',
- },
- },
- },
- }),
- setAllowLan: (_allowLan: boolean) => Promise.resolve(),
- getAllowLan: () => Promise.resolve(true),
- connect: () => {
- for (const listener of openListeners) {
- listener();
- }
- },
- disconnect: () => {
- for (const listener of closeListeners) {
- listener();
- }
- },
- connectTunnel: () => Promise.resolve(),
- disconnectTunnel: () => Promise.resolve(),
- getLocation: () =>
- Promise.resolve({
- ip: '',
- country: '',
- city: '',
- latitude: 0.0,
- longitude: 0.0,
- mullvad_exit_ip: false,
- }),
- getState: () =>
- Promise.resolve({
- state: 'unsecured',
- target_state: 'unsecured',
- }),
- subscribeStateListener: (listener: (state: ?BackendState, error: ?Error) => void) => {
- stateListeners.push(listener);
- return Promise.resolve();
- },
- sendNewState: (state: BackendState) => {
- for (const listener of stateListeners) {
- listener(state);
- }
- },
- addOpenConnectionObserver: (listener: () => void) => {
- openListeners.push(listener);
- return {
- unsubscribe: () => {},
- };
- },
- addCloseConnectionObserver: (listener: (error: ?Error) => void) => {
- closeListeners.push(listener);
- return {
- unsubscribe: () => {},
- };
- },
- authenticate: (_secret: string) => Promise.resolve(),
- getAccountHistory: () => Promise.resolve([]),
- removeAccountFromHistory: (_accountToken) => Promise.resolve(),
- };
-
- return mockIpc;
-}