diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-07-16 16:33:56 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-07-16 16:33:56 +0200 |
| commit | 7aa861495ffe90e0b5a9b9c67b8ccf0ad62c2882 (patch) | |
| tree | 212056880e20c6a3bdcc9e1e807bd75b1a4199af | |
| parent | 901d0135651d010088105e98204af7618347389d (diff) | |
| parent | 6c0618e8b500499576cb1b6969ce268c90df01b1 (diff) | |
| download | mullvadvpn-7aa861495ffe90e0b5a9b9c67b8ccf0ad62c2882.tar.xz mullvadvpn-7aa861495ffe90e0b5a9b9c67b8ccf0ad62c2882.zip | |
Merge branch 'auto-connect-settings'
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | app/app.js | 31 | ||||
| -rw-r--r-- | app/components/Preferences.js | 56 | ||||
| -rw-r--r-- | app/containers/PreferencesPage.js | 21 | ||||
| -rw-r--r-- | app/lib/daemon-rpc.js | 15 | ||||
| -rw-r--r-- | app/lib/platform.android.js | 10 | ||||
| -rw-r--r-- | app/lib/platform.js | 33 | ||||
| -rw-r--r-- | app/redux/settings/actions.js | 22 | ||||
| -rw-r--r-- | app/redux/settings/reducers.js | 8 | ||||
| -rw-r--r-- | test/components/Preferences.spec.js | 6 | ||||
| -rw-r--r-- | test/components/SelectLocation.spec.js | 1 | ||||
| -rw-r--r-- | test/components/Settings.spec.js | 1 | ||||
| -rw-r--r-- | test/mocks/rpc.js | 103 |
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; -} |
