diff options
| author | Erik Larkö <erik@mullvad.net> | 2017-11-01 07:33:17 +0100 |
|---|---|---|
| committer | Erik Larkö <erik@mullvad.net> | 2017-11-08 15:21:06 +0100 |
| commit | a77e01a50b165fd8d6e2db96652a1fcc9a220723 (patch) | |
| tree | 4866f64a6956f1cfc2a4a47f7dac792af3b07a47 | |
| parent | e53e809b9b674e70dc1f540ed25076ca5bc6fae2 (diff) | |
| download | mullvadvpn-a77e01a50b165fd8d6e2db96652a1fcc9a220723.tar.xz mullvadvpn-a77e01a50b165fd8d6e2db96652a1fcc9a220723.zip | |
tcp and port
27 files changed, 449 insertions, 138 deletions
diff --git a/app/app.js b/app/app.js index c07f267617..6536de4983 100644 --- a/app/app.js +++ b/app/app.js @@ -27,6 +27,9 @@ ipcRenderer.on('backend-info', (_event, args) => { backend.setCredentials(args.credentials); backend.sync(); backend.autologin() + .then( () => { + return backend.syncRelayConstraints(); + }) .catch( e => { if (e.type === 'NO_ACCOUNT') { log.debug('No user set in the backend, showing window'); diff --git a/app/components/AdvancedSettings.js b/app/components/AdvancedSettings.js new file mode 100644 index 0000000000..1ba25a924f --- /dev/null +++ b/app/components/AdvancedSettings.js @@ -0,0 +1,127 @@ +// @flow + +import React from 'react'; +import { Layout, Container, Header } from './Layout'; +import CustomScrollbars from './CustomScrollbars'; + +type Props = { + onClose: () => void, + protocol: string, + port: string|number, + updateConstraints: (string, string|number) => void, +}; +export function AdvancedSettings(props: Props) { + + let portSelector = null; + let protocol = props.protocol.toUpperCase(); + + if (protocol === 'AUTOMATIC') { + protocol = 'Automatic'; + } else { + portSelector = createPortSelector(props); + } + + return <BaseLayout onClose={ props.onClose }> + + <Selector + title={ 'Network protocols' } + values={ ['Automatic', 'UDP', 'TCP'] } + value={ protocol } + onSelect={ protocol => { + // $FlowFixMe + props.updateConstraints(protocol, 'Automatic'); + }}/> + + <div className="settings__cell-spacer"></div> + + { portSelector } + + </BaseLayout>; + +} + +function createPortSelector(props) { + const protocol = props.protocol.toUpperCase(); + const ports = protocol === 'TCP' + ? ['Automatic', 80, 443] + : ['Automatic', 1194, 1195, 1196, 1197, 1300, 1301, 1302]; + + return <Selector + title={ protocol + ' port' } + values={ ports } + value={ props.port } + onSelect={ port => { + props.updateConstraints(protocol, port); + }} />; +} + +function Selector(props) { + return <div> + <Cell + label={ props.title } + /> + + { props.values.map(value => renderCell(value)) } + </div>; + + function renderCell(value) { + const selected = value === props.value; + + let className = 'settings__sub-cell'; + let tick = null; + if (selected) { + className = 'settings__cell--selected'; + tick = <img src='./assets/images/icon-tick.svg' />; + } + const label = <div className={ 'settings__sub-cell--label' }> + { tick } + { value } + </div>; + + const onCellClick = () => props.onSelect(value); + + return <Cell + key={ value } + label={ label } + className={ className } + onClick={ onCellClick } />; + } +} + +function BaseLayout(props) { + return <Layout> + <Header hidden={ true } style={ 'defaultDark' } /> + <Container> + <div className="settings"> + <button className="settings__close" onClick={ props.onClose } /> + <div className="settings__container"> + <div className="settings__header"> + <h2 className="settings__title">Advanced Settings</h2> + </div> + <CustomScrollbars autoHide={ true }> + <div className="settings__content"> + <div className="settings__main"> + <div className="settings__advanced"> + { props.children } + </div> + </div> + </div> + </CustomScrollbars> + </div> + </div> + </Container> + </Layout>; +} + +function Cell(props) { + + const className = props.className || ''; + return <div + className={ className + ' settings__cell' } + onClick={ props.onClick || null } > + <div className="settings__cell-label">{ props.label }</div> + <div className="settings__cell-value"> + { props.value || null } + </div> + </div>; +} diff --git a/app/components/Connect.js b/app/components/Connect.js index c49912349d..531174b464 100644 --- a/app/components/Connect.js +++ b/app/components/Connect.js @@ -10,15 +10,15 @@ import ExternalLinkSVG from '../assets/images/icon-extLink.svg'; import type { ServerInfo } from '../lib/backend'; import type { HeaderBarStyle } from './HeaderBar'; import type { ConnectionReduxState } from '../redux/connection/reducers'; -import type { RelayEndpoint } from '../lib/ipc-facade'; +import type { SettingsReduxState } from '../redux/settings/reducers'; export type ConnectProps = { accountExpiry: string, connection: ConnectionReduxState, - preferredServer: string, + settings: SettingsReduxState, onSettings: () => void, onSelectLocation: () => void, - onConnect: (relayEndpoint: RelayEndpoint) => void, + onConnect: (host: string) => void, onCopyIP: () => void, onDisconnect: () => void, onExternalLink: (type: string) => void, @@ -93,9 +93,23 @@ export default class Connect extends Component { ); } + _getServerInfo() { + const { relayConstraints } = this.props.settings; + if (relayConstraints.host === 'any') { + return { + name: 'Automatic', + country: 'Automatic', + city: 'Automatic', + address: '', + }; + } + + return this.props.getServerInfo(relayConstraints.host.only); + + } + renderMap(): React.Element<*> { - const preferredServer = this.props.preferredServer; - const serverInfo = this.props.getServerInfo(preferredServer); + const serverInfo = this._getServerInfo(); if(!serverInfo) { throw new Error('Server info cannot be null.'); } @@ -272,16 +286,12 @@ export default class Connect extends Component { // Handlers onConnect() { - const preferredServer = this.props.preferredServer; - const serverInfo = this.props.getServerInfo(preferredServer); - if(serverInfo) { - // TODO: Don't use these hardcoded values - this.props.onConnect({ - host: serverInfo.address, - port: 1300, - protocol: 'udp', - }); + const serverInfo = this._getServerInfo(); + if(!serverInfo) { + return; } + + this.props.onConnect(serverInfo.address); } onExternalLink(type: string) { diff --git a/app/components/SelectLocation.js b/app/components/SelectLocation.js index 5df5603f71..c5180a3ffa 100644 --- a/app/components/SelectLocation.js +++ b/app/components/SelectLocation.js @@ -24,7 +24,11 @@ export default class SelectLocation extends Component { } isSelected(server: string) { - return server === this.props.settings.preferredServer; + const { host } = this.props.settings.relayConstraints; + if (host === 'any') { + return false; + } + return server === host.only; } drawCell(key: string, name: string, icon: ?string, onClick: (e: Event) => void): React.Element<*> { diff --git a/app/components/Settings.css b/app/components/Settings.css index 7bb3dfcec7..00067a1074 100644 --- a/app/components/Settings.css +++ b/app/components/Settings.css @@ -63,6 +63,11 @@ height: 24px; } +.settings__cell--selected, +.settings__cell--selected:hover { + background-color: #44AD4D; +} + .settings__cell--active:hover { background-color: rgba(41,71,115,0.9); } @@ -91,6 +96,21 @@ flex: 0 0 auto; } +.settings__sub-cell { + background-color: rgb(36, 57, 84); +} +.settings__sub-cell:hover { + background-color: rgba(41,71,115,0.9); +} + +.settings__sub-cell--label { + padding-left: 15px; +} + +.settings__sub-cell--label img { + padding-right: 8px; +} + .settings__account-paid-until-label { font-family: "Open Sans"; font-size: 13px; @@ -117,4 +137,4 @@ padding: 24px; } -.settings__footer .button + .button { margin-top: 16px; }
\ No newline at end of file +.settings__footer .button + .button { margin-top: 16px; } diff --git a/app/components/Settings.js b/app/components/Settings.js index 4c906a9dc1..fd9fbebebd 100644 --- a/app/components/Settings.js +++ b/app/components/Settings.js @@ -15,6 +15,7 @@ export type SettingsProps = { onClose: () => void, onViewAccount: () => void, onViewSupport: () => void, + onViewAdvancedSettings: () => void, onExternalLink: (type: string) => void }; @@ -67,8 +68,20 @@ export default class Settings extends Component { <img className="settings__cell-disclosure" src="assets/images/icon-chevron.svg" /> </div> <div className="settings__cell-spacer"></div> + </div> + </Then> + </If> - <div className="settings__cell-footer"></div> + <If condition={ isLoggedIn }> + <Then> + <div className="settings__advanced"> + <div className="settings__cell settings__cell--active" onClick={ this.props.onViewAdvancedSettings }> + <div className="settings__cell-label">Advanced</div> + <div className="settings__cell-value"> + <img className="settings__cell-disclosure" src="assets/images/icon-chevron.svg" /> + </div> + </div> + <div className="settings__cell-spacer"></div> </div> </Then> </If> diff --git a/app/containers/AdvancedSettingsPage.js b/app/containers/AdvancedSettingsPage.js new file mode 100644 index 0000000000..22dbdf8ec1 --- /dev/null +++ b/app/containers/AdvancedSettingsPage.js @@ -0,0 +1,51 @@ +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; +import { AdvancedSettings } from '../components/AdvancedSettings'; +import settingsActions from '../redux/settings/actions'; +import log from 'electron-log'; + +const mapStateToProps = (state) => { + return { + protocol: tryOrElse( () => state.settings.relayConstraints.tunnel.openvpn.protocol.only, 'Automatic'), + port: tryOrElse( () => state.settings.relayConstraints.tunnel.openvpn.port.only, 'Automatic'), + }; +}; + +function tryOrElse(toTry, orElse) { + try { + return toTry() || orElse; + } catch (e) { + return orElse; + } +} + +const mapDispatchToProps = (dispatch, props) => { + const { backend } = props; + return { + onClose: () => dispatch(push('/settings')), + + updateConstraints: (protocol, port) => { + + const protConstraint = protocol === 'Automatic' + ? 'any' + : { only: protocol.toLowerCase() }; + + const portConstraint = port === 'Automatic' + ? 'any' + : { only: port }; + + const update = { + tunnel: { openvpn: { + protocol: protConstraint, + port: portConstraint, + }}, + }; + + backend.updateRelayConstraints(update) + .then( () => dispatch(settingsActions.updateRelay(update))) + .catch( e => log.error('Failed updating relay constraints', e.message)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(AdvancedSettings); diff --git a/app/containers/ConnectPage.js b/app/containers/ConnectPage.js index d629e83fc7..5c6dc1fa60 100644 --- a/app/containers/ConnectPage.js +++ b/app/containers/ConnectPage.js @@ -10,7 +10,7 @@ const mapStateToProps = (state) => { return { accountExpiry: state.account.expiry, connection: state.connection, - preferredServer: state.settings.preferredServer, + settings: state.settings, }; }; diff --git a/app/containers/SelectLocationPage.js b/app/containers/SelectLocationPage.js index c11c4b59dd..9e5551130c 100644 --- a/app/containers/SelectLocationPage.js +++ b/app/containers/SelectLocationPage.js @@ -1,30 +1,31 @@ import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import { push } from 'react-router-redux'; import SelectLocation from '../components/SelectLocation'; import settingsActions from '../redux/settings/actions'; +import log from 'electron-log'; const mapStateToProps = (state) => state; const mapDispatchToProps = (dispatch, props) => { const { backend } = props; - const settings = bindActionCreators(settingsActions, dispatch); return { onClose: () => dispatch(push('/connect')), onSelect: (preferredServer) => { - const server = backend.serverInfo(preferredServer); dispatch(push('/connect')); // add delay to let the map load setTimeout(() => { - settings.updateSettings({ preferredServer }); + const update = { + host: { only: preferredServer }, + tunnel: { openvpn: { + }}, + }; + + backend.updateRelayConstraints(update) + .then( () => dispatch(settingsActions.updateRelay(update))) + .then( () => backend.connect()) + .catch( e => log.error('Failed updating relay constraints', e.message)); - // TODO: Don't use these hardcoded values - backend.connect({ - host: server.address, - port: 1300, - protocol: 'udp', - }); }, 600); } }; diff --git a/app/containers/SettingsPage.js b/app/containers/SettingsPage.js index 520f27ce6e..72eafb9431 100644 --- a/app/containers/SettingsPage.js +++ b/app/containers/SettingsPage.js @@ -14,6 +14,7 @@ const mapDispatchToProps = (dispatch, _props) => { onClose: () => dispatch(push('/connect')), onViewAccount: () => dispatch(push('/settings/account')), onViewSupport: () => dispatch(push('/settings/support')), + onViewAdvancedSettings: () => dispatch(push('/settings/advanced')), onExternalLink: (type) => shell.openExternal(links[type]), }; }; diff --git a/app/lib/backend.js b/app/lib/backend.js index b210e65e85..89a895b4c4 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -1,16 +1,17 @@ // @flow -//import log from 'electron-log'; -const log = console; +import log from 'electron-log'; import EventEmitter from 'events'; import { servers } from '../config'; import { IpcFacade, RealIpc } from './ipc-facade'; import accountActions from '../redux/account/actions'; import connectionActions from '../redux/connection/actions'; -import type { ReduxStore } from '../redux/store'; +import settingsActions from '../redux/settings/actions'; import { push } from 'react-router-redux'; +import { defaultServer } from '../config'; -import type { BackendState, RelayEndpoint } from './ipc-facade'; +import type { ReduxStore } from '../redux/store'; +import type { BackendState, RelayConstraintsUpdate } from './ipc-facade'; import type { ConnectionState } from '../redux/connection/reducers'; export type EventType = 'connect' | 'connecting' | 'disconnect' | 'login' | 'logging' | 'logout' | 'updatedIp' | 'updatedLocation' | 'updatedReachability'; @@ -114,7 +115,7 @@ export class Backend { } setCredentials(credentials: IpcCredentials) { - log.info('Got connection info to backend', credentials.connectionString); + log.debug('Got connection info to backend', credentials.connectionString); this._credentials = credentials; if (this._ipc) { @@ -168,7 +169,7 @@ export class Backend { } login(accountToken: string): Promise<void> { - log.info('Attempting to login with account number', accountToken); + log.debug('Attempting to login with account number', accountToken); this._store.dispatch(accountActions.startLogin(accountToken)); @@ -176,7 +177,7 @@ export class Backend { .then( () => { return this._ipc.getAccountData(accountToken) .then( response => { - log.info('Account exists', response); + log.debug('Account exists', response); return this._ipc.setAccount(accountToken) .then( () => response ); @@ -203,7 +204,7 @@ export class Backend { } autologin() { - log.info('Attempting to log in automatically'); + log.debug('Attempting to log in automatically'); this._store.dispatch(accountActions.startLogin()); @@ -220,7 +221,7 @@ export class Backend { return this._ipc.getAccountData(accountToken); }) .then( accountData => { - log.info('The stored account number still exists', accountData); + log.debug('The stored account number still exists', accountData); this._store.dispatch(accountActions.loginSuccessful(accountData.expiry)); @@ -259,33 +260,26 @@ export class Backend { }); } - connect(aRelayEndpoint?: RelayEndpoint): Promise<void> { - - const relayEndpoint = aRelayEndpoint; - if (relayEndpoint) { - this._store.dispatch(connectionActions.connectingTo(relayEndpoint)); + connect(aHost?: string): Promise<void> { + const host = aHost; - return this._ensureAuthenticated() - .then( () => { - return this._ipc.setCustomRelay(relayEndpoint) - .then( () => { - return this._ipc.connect(); - }) - .catch(e => { - log.info('Failed connecting to', relayEndpoint.host, '-', e.message); - this._store.dispatch(connectionActions.disconnected()); - }); - }); - } else { - return this._ensureAuthenticated() - .then( () => { - return this._ipc.connect() - .catch(e => { - log.info('Failed connecting to the relay set in the backend, ', e.message); - this._store.dispatch(connectionActions.disconnected()); - }); - }); + let setHostPromise = () => Promise.resolve(); + if (host) { + this._store.dispatch(connectionActions.connectingTo(host || 'unknown')); + setHostPromise = () => this._ipc.updateRelayConstraints({ + host: { only: host }, + tunnel: { openvpn: { + }}, + }); } + + return this._ensureAuthenticated() + .then( setHostPromise ) + .then( () => this._ipc.connect() ) + .catch(e => { + log.info('Failed connecting to the relay set in the backend, ', e.message); + this._store.dispatch(connectionActions.disconnected()); + }); } disconnect(): Promise<void> { @@ -306,6 +300,35 @@ export class Backend { }); } + updateRelayConstraints(relayConstraints: RelayConstraintsUpdate): Promise<void> { + return this._ensureAuthenticated() + .then( () => { + return this._ipc.updateRelayConstraints(relayConstraints); + }); + } + + syncRelayConstraints(): Promise<void> { + return this._ensureAuthenticated() + .then( () => { + return this._ipc.getRelayContraints(); + }) + .then( constraints => { + log.debug('Got constraints from backend', constraints); + + const host = constraints.host === 'any' + ? defaultServer + : constraints.host || defaultServer; + + this._store.dispatch(settingsActions.updateRelay({ + host: host, + tunnel: constraints.tunnel, + })); + }) + .catch( e => { + log.error('Failed getting relay constraints', e); + }); + } + /** * Start reachability monitoring for online/offline detection * This is currently done via HTML5 APIs but will be replaced later @@ -335,7 +358,7 @@ export class Backend { return this._ensureAuthenticated() .then( () => { return this._ipc.registerStateListener(newState => { - log.info('Got new state from backend', newState); + log.debug('Got new state from backend', newState); const newStatus = this._securityStateToConnectionState(newState); switch(newStatus) { diff --git a/app/lib/ipc-facade.js b/app/lib/ipc-facade.js index 8939e82823..9f3f900331 100644 --- a/app/lib/ipc-facade.js +++ b/app/lib/ipc-facade.js @@ -1,7 +1,7 @@ // @flow import JsonRpcWs, { InvalidReply } from './jsonrpc-ws-ipc'; -import { object, string, arrayOf, number } from 'validated/schema'; +import { object, string, arrayOf, number, enumeration, oneOf } from 'validated/schema'; import { validate } from 'validated/object'; import type { Coordinate2d } from '../types'; @@ -25,11 +25,36 @@ export type BackendState = { state: SecurityState, target_state: SecurityState, }; -export type RelayEndpoint = { - host: string, - port: number, - protocol: 'tcp' | 'udp', +export type RelayConstraints = { + host: 'any' | { only: string }, + tunnel: { + openvpn: { + port: 'any' | { only: number }, + protocol: 'any' | { only: 'tcp' | 'udp' }, + }, + }, }; +export type RelayConstraintsUpdate = { + host?: 'any' | { only: string }, + tunnel: { + openvpn: { + port?: 'any' | { only: number }, + protocol?: 'any' | { only: 'tcp' | 'udp' }, + }, + }, +}; +const Constraint = (v) => oneOf(string, object({ + only: v, +})); +const RelayConstraintsSchema = object({ + host: Constraint(string), + tunnel: object({ + openvpn: object({ + port: Constraint(number), + protocol: Constraint(enumeration('udp', 'tcp')), + }), + }), +}); export interface IpcFacade { @@ -37,7 +62,8 @@ export interface IpcFacade { getAccountData(AccountToken): Promise<AccountData>, getAccount(): Promise<?AccountToken>, setAccount(accountToken: ?AccountToken): Promise<void>, - setCustomRelay(RelayEndpoint): Promise<void>, + updateRelayConstraints(RelayConstraintsUpdate): Promise<void>, + getRelayContraints(): Promise<RelayConstraints>, connect(): Promise<void>, disconnect(): Promise<void>, shutdown(): Promise<void>, @@ -95,11 +121,23 @@ export class RealIpc implements IpcFacade { return; } - setCustomRelay(relayEndpoint: RelayEndpoint): Promise<void> { - return this._ipc.send('set_custom_relay', [relayEndpoint]) + updateRelayConstraints(relayConstraints: RelayConstraintsUpdate): Promise<void> { + return this._ipc.send('update_relay_constraints', [relayConstraints]) .then(this._ignoreResponse); } + getRelayContraints(): Promise<RelayConstraints> { + return this._ipc.send('get_relay_constraints') + .then( raw => { + try { + const validated: any = validate(RelayConstraintsSchema, raw); + return (validated: RelayConstraints); + } catch (e) { + throw new InvalidReply(raw, e); + } + }); + } + connect(): Promise<void> { return this._ipc.send('connect') .then(this._ignoreResponse); diff --git a/app/lib/jsonrpc-ws-ipc.js b/app/lib/jsonrpc-ws-ipc.js index deefce3d66..909b4b0775 100644 --- a/app/lib/jsonrpc-ws-ipc.js +++ b/app/lib/jsonrpc-ws-ipc.js @@ -98,7 +98,7 @@ export default class Ipc { on(event: string, listener: (mixed) => void): Promise<*> { - log.info('Adding a listener to', event); + log.debug('Adding a listener to', event); return this.send(event + '_subscribe') .then(subscriptionId => { if (typeof subscriptionId === 'string' || typeof subscriptionId === 'number') { diff --git a/app/main.js b/app/main.js index cc47a57cb2..3af6a4863e 100644 --- a/app/main.js +++ b/app/main.js @@ -233,7 +233,7 @@ const appDelegate = { const credentials = parseIpcCredentials(data); if(credentials) { - log.info('Read IPC connection info', credentials.connectionString); + log.debug('Read IPC connection info', credentials.connectionString); window.webContents.send('backend-info', { credentials }); } else { log.error('Could not parse IPC credentials.'); diff --git a/app/redux/connection/actions.js b/app/redux/connection/actions.js index 73a94fcc06..dcc90f7273 100644 --- a/app/redux/connection/actions.js +++ b/app/redux/connection/actions.js @@ -3,12 +3,11 @@ import { clipboard } from 'electron'; import type { Backend } from '../../lib/backend'; -import type { RelayEndpoint } from '../../lib/ipc-facade'; import type { ReduxGetState, ReduxDispatch } from '../store'; import type { Coordinate2d } from '../../types'; -const connect = (backend: Backend, relay: RelayEndpoint) => () => backend.connect(relay); +const connect = (backend: Backend, relay: string) => () => backend.connect(relay); const disconnect = (backend: Backend) => () => backend.disconnect(); const copyIPAddress = () => { return (_dispatch: ReduxDispatch, getState: ReduxGetState) => { @@ -22,7 +21,7 @@ const copyIPAddress = () => { type ConnectingAction = { type: 'CONNECTING', - relayEndpoint?: RelayEndpoint, + host?: string, }; type ConnectedAction = { type: 'CONNECTED', @@ -63,10 +62,10 @@ export type ConnectionAction = NewPublicIpAction | OnlineAction | OfflineAction; -function connectingTo(relayEndpoint: RelayEndpoint): ConnectingAction { +function connectingTo(host: string): ConnectingAction { return { type: 'CONNECTING', - relayEndpoint: relayEndpoint, + host: host, }; } diff --git a/app/redux/connection/reducers.js b/app/redux/connection/reducers.js index 8adfac594a..ce4cb79344 100644 --- a/app/redux/connection/reducers.js +++ b/app/redux/connection/reducers.js @@ -62,8 +62,8 @@ function onConnecting(state, action) { status: 'connecting', }; - if (action.relayEndpoint) { - newState.serverAddress = action.relayEndpoint.host; + if (action.host) { + newState.serverAddress = action.host; } return { ...state, ...newState}; } diff --git a/app/redux/settings/actions.js b/app/redux/settings/actions.js index 7b806096b8..041da76389 100644 --- a/app/redux/settings/actions.js +++ b/app/redux/settings/actions.js @@ -1,17 +1,19 @@ // @flow -import type { SettingsReduxState } from './reducers'; +import type { RelayConstraints } from '../../lib/ipc-facade'; -export type UpdateSettingsAction = { - type: 'UPDATE_SETTINGS', - newSettings: $Shape<SettingsReduxState>, +export type UpdateRelayAction = { + type: 'UPDATE_RELAY', + relay: RelayConstraints, }; -function updateSettings(newSettings: $Shape<SettingsReduxState>): UpdateSettingsAction { +export type SettingsAction = UpdateRelayAction; + +function updateRelay(relay: RelayConstraints): UpdateRelayAction { return { - type: 'UPDATE_SETTINGS', - newSettings: newSettings, + type: 'UPDATE_RELAY', + relay: relay, }; } -export default { updateSettings }; +export default { updateRelay }; diff --git a/app/redux/settings/reducers.js b/app/redux/settings/reducers.js index da23a5a6ce..665a4c465e 100644 --- a/app/redux/settings/reducers.js +++ b/app/redux/settings/reducers.js @@ -3,19 +3,31 @@ import { defaultServer } from '../../config'; import type { ReduxAction } from '../store'; +import type { RelayConstraints } from '../../lib/ipc-facade'; export type SettingsReduxState = { - preferredServer: string + relayConstraints: RelayConstraints, }; const initialState: SettingsReduxState = { - preferredServer: defaultServer + relayConstraints: { + host: { only: defaultServer }, + tunnel: { openvpn: { + port: 'any', + protocol: 'any', + }}, + }, }; export default function(state: SettingsReduxState = initialState, action: ReduxAction): SettingsReduxState { - if (action.type === 'UPDATE_SETTINGS') { - return { ...state, ...action.newSettings }; + if (action.type === 'UPDATE_RELAY') { + return { ...state, + relayConstraints: { + ...state.relayConstraints, + ...action.relay, + }, + }; } return state; diff --git a/app/redux/store.js b/app/redux/store.js index b4aa0375e1..3f73574103 100644 --- a/app/redux/store.js +++ b/app/redux/store.js @@ -18,7 +18,7 @@ import type { SettingsReduxState } from './settings/reducers.js'; import type { ConnectionAction } from './connection/actions.js'; import type { AccountAction } from './account/actions.js'; -import type { UpdateSettingsAction } from './settings/actions.js'; +import type { SettingsAction } from './settings/actions.js'; export type ReduxState = { account: AccountReduxState, @@ -27,7 +27,7 @@ export type ReduxState = { }; export type ReduxAction = AccountAction - | UpdateSettingsAction + | SettingsAction | ConnectionAction; export type ReduxStore = Store<ReduxState, ReduxAction, ReduxDispatch>; diff --git a/app/routes.js b/app/routes.js index 5736e7f37a..f428c8093b 100644 --- a/app/routes.js +++ b/app/routes.js @@ -7,6 +7,7 @@ import WindowChrome from './components/WindowChrome'; import LoginPage from './containers/LoginPage'; import ConnectPage from './containers/ConnectPage'; import SettingsPage from './containers/SettingsPage'; +import AdvancedSettingsPage from './containers/AdvancedSettingsPage'; import AccountPage from './containers/AccountPage'; import SupportPage from './containers/SupportPage'; import SelectLocationPage from './containers/SelectLocationPage'; @@ -97,6 +98,7 @@ export default function makeRoutes(getState: ReduxGetState, componentProps: Shar <PublicRoute exact path="/settings" component={ SettingsPage } /> <PrivateRoute exact path="/settings/account" component={ AccountPage } /> <PublicRoute exact path="/settings/support" component={ SupportPage } /> + <PublicRoute exact path="/settings/advanced" component={ AdvancedSettingsPage } /> <PrivateRoute exact path="/select-location" component={ SelectLocationPage } /> </Switch> </CSSTransitionGroup> diff --git a/app/transitions.js b/app/transitions.js index 0ce94e2d16..fe6da53563 100644 --- a/app/transitions.js +++ b/app/transitions.js @@ -50,6 +50,7 @@ const transitions: TransitionMap = { const transitionRules = [ r('/settings', '/settings/account', transitions.push), r('/settings', '/settings/support', transitions.push), + r('/settings', '/settings/advanced', transitions.push), r(null, '/settings', transitions.slide), r(null, '/select-location', transitions.slide) ]; diff --git a/test/components/Connect.spec.js b/test/components/Connect.spec.js index 07c67a57bc..5c342bff12 100644 --- a/test/components/Connect.spec.js +++ b/test/components/Connect.spec.js @@ -106,7 +106,11 @@ describe('components/Connect', () => { const locationSwitcher = component.find('.connect__server'); component.setProps({ - preferredServer: 'se1.mullvad.net', + settings: { + relayConstraints: { + host: { only: 'se1.mullvad.net' }, + }, + }, }); expect(locationSwitcher.text()).to.contain(servers['se1.mullvad.net'].name); }); @@ -181,6 +185,14 @@ const defaultProps = { getServerInfo: (_) => { return defaultServer; }, accountExpiry: '', - preferredServer: '', + settings: { + relayConstraints: { + host: { only: 'www.example.com' }, + tunnel: { openvpn: { + port: 'any', + protocol: 'any', + }}, + }, + }, connection: defaultConnection, }; diff --git a/test/components/SelectLocation.spec.js b/test/components/SelectLocation.spec.js index 783a3adbf9..a3565a6704 100644 --- a/test/components/SelectLocation.spec.js +++ b/test/components/SelectLocation.spec.js @@ -4,15 +4,19 @@ import { expect } from 'chai'; import React from 'react'; import ReactTestUtils, { Simulate } from 'react-dom/test-utils'; import SelectLocation from '../../app/components/SelectLocation'; -import { defaultServer } from '../../app/config'; import type { SettingsReduxState } from '../../app/redux/settings/reducers'; import type { SelectLocationProps } from '../../app/components/SelectLocation'; describe('components/SelectLocation', () => { const state: SettingsReduxState = { - autoSecure: true, - preferredServer: defaultServer + relayConstraints: { + host: 'any', + tunnel: { openvpn: { + port: 'any', + protocol: 'any', + }}, + }, }; const makeProps = (state: SettingsReduxState, mergeProps: $Shape<SelectLocationProps>): SelectLocationProps => { diff --git a/test/components/Settings.spec.js b/test/components/Settings.spec.js index f90d658f9b..2f562051b5 100644 --- a/test/components/Settings.spec.js +++ b/test/components/Settings.spec.js @@ -4,7 +4,6 @@ import { expect } from 'chai'; import React from 'react'; import ReactTestUtils, { Simulate } from 'react-dom/test-utils'; import Settings from '../../app/components/Settings'; -import { defaultServer } from '../../app/config'; import type { AccountReduxState } from '../../app/redux/account/reducers'; import type { SettingsReduxState } from '../../app/redux/settings/reducers'; @@ -33,7 +32,13 @@ describe('components/Settings', () => { }; const settingsState: SettingsReduxState = { - preferredServer: defaultServer + relayConstraints: { + host: 'any', + tunnel: { openvpn: { + port: 'any', + protocol: 'any', + }}, + }, }; const makeProps = (anAccountState: AccountReduxState, aSettingsState: SettingsReduxState, mergeProps: $Shape<SettingsProps> = {}): SettingsProps => { @@ -44,6 +49,7 @@ describe('components/Settings', () => { onClose: () => {}, onViewAccount: () => {}, onViewSupport: () => {}, + onViewAdvancedSettings: () => {}, onExternalLink: (_type) => {} }; return Object.assign({}, defaultProps, mergeProps); diff --git a/test/connect.spec.js b/test/connect.spec.js index 07e3091c6e..586df45980 100644 --- a/test/connect.spec.js +++ b/test/connect.spec.js @@ -7,14 +7,18 @@ import { IpcChain } from './helpers/IpcChain'; describe('connect', () => { - it('should invoke set_custom_relay and then connect in the backend', (done) => { + it('should invoke update_relay_constraints and then connect in the backend', (done) => { const { store, mockIpc, backend } = setupBackendAndStore(); const chain = new IpcChain(mockIpc); - chain.require('setCustomRelay') + chain.require('updateRelayConstraints') .withInputValidation( - (relayEndpoint) => { - expect(relayEndpoint).to.equal(arbitraryRelay); + relayEndpoint => { + if (relayEndpoint) { + expect(relayEndpoint.host.only).to.equal(arbitraryRelay); + } else { + expect.fail(); + } }, ) .done(); @@ -47,13 +51,8 @@ describe('connect', () => { it('should update the state with the server address', () => { const { store, backend } = setupBackendAndStore(); - const relay = { - host: 'www.example.com', - port: 1, - protocol: 'udp', - }; - return backend.connect(relay) + return backend.connect('www.example.com') .then( () => { const state = store.getState().connection; expect(state.status).to.equal('connecting'); @@ -93,8 +92,4 @@ describe('connect', () => { }); }); -const arbitraryRelay = { - host: 'www.example.com', - port: 1, - protocol: 'udp', -}; +const arbitraryRelay = 'www.example.com'; diff --git a/test/mocks/ipc.js b/test/mocks/ipc.js index c02eb20de4..67c11c7366 100644 --- a/test/mocks/ipc.js +++ b/test/mocks/ipc.js @@ -28,7 +28,15 @@ export function newMockIpc() { setAccount: () => Promise.resolve(), - setCustomRelay: () => Promise.resolve(), + updateRelayConstraints: () => Promise.resolve(), + + getRelayContraints: () => Promise.resolve({ + host: { only: 'www.example.com' }, + tunnel: { openvpn: { + port: 'any', + protocol: 'any', + }}, + }), connect: () => Promise.resolve(), diff --git a/test/reducers.spec.js b/test/reducers.spec.js deleted file mode 100644 index 92392ad5d3..0000000000 --- a/test/reducers.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow - -import { expect } from 'chai'; -import settingsReducer from '../app/redux/settings/reducers'; -import { defaultServer } from '../app/config'; - -describe('reducers', () => { - const previousState: any = {}; - - it('should handle SETTINGS_UPDATE', () => { - const action = { - type: 'UPDATE_SETTINGS', - newSettings: { - preferredServer: defaultServer - } - }; - const test = Object.assign({}, action.newSettings); - expect(settingsReducer(previousState, action)).to.deep.equal(test); - }); - -}); |
