diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2017-12-06 12:27:45 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2017-12-06 12:27:45 +0100 |
| commit | 7f20580a18bfde633a5d2cc5d7ed5210d414a9c1 (patch) | |
| tree | d6df9069d816bbf5958a25e04d0b025e6acae5df | |
| parent | c3fea60e7073586875b91ee047b98e157a3cb746 (diff) | |
| parent | ed18963b050fa50098dd515bb12914fceb012cea (diff) | |
| download | mullvadvpn-7f20580a18bfde633a5d2cc5d7ed5210d414a9c1.tar.xz mullvadvpn-7f20580a18bfde633a5d2cc5d7ed5210d414a9c1.zip | |
Merge branch 'normal-constraints-pr'
| -rw-r--r-- | app/app.js | 26 | ||||
| -rw-r--r-- | app/components/AdvancedSettings.js | 11 | ||||
| -rw-r--r-- | app/components/Connect.js | 99 | ||||
| -rw-r--r-- | app/components/SelectLocation.js | 74 | ||||
| -rw-r--r-- | app/config.json | 179 | ||||
| -rw-r--r-- | app/containers/AdvancedSettingsPage.js | 70 | ||||
| -rw-r--r-- | app/containers/ConnectPage.js | 4 | ||||
| -rw-r--r-- | app/containers/SelectLocationPage.js | 29 | ||||
| -rw-r--r-- | app/lib/backend.js | 122 | ||||
| -rw-r--r-- | app/lib/relay-settings-builder.js | 195 | ||||
| -rw-r--r-- | app/redux/connection/actions.js | 17 | ||||
| -rw-r--r-- | app/redux/connection/reducers.js | 4 | ||||
| -rw-r--r-- | app/redux/settings/actions.js | 6 | ||||
| -rw-r--r-- | app/redux/settings/reducers.js | 32 | ||||
| -rw-r--r-- | test/auth.spec.js | 4 | ||||
| -rw-r--r-- | test/components/Connect.spec.js | 204 | ||||
| -rw-r--r-- | test/components/SelectLocation.spec.js | 8 | ||||
| -rw-r--r-- | test/components/Settings.spec.js | 8 | ||||
| -rw-r--r-- | test/connect.spec.js | 34 | ||||
| -rw-r--r-- | test/relay-settings-builder.spec.js | 151 |
20 files changed, 850 insertions, 427 deletions
diff --git a/app/app.js b/app/app.js index 656e7dd5c0..a10d0da07c 100644 --- a/app/app.js +++ b/app/app.js @@ -9,7 +9,7 @@ import { webFrame, ipcRenderer } from 'electron'; import log from 'electron-log'; import makeRoutes from './routes'; import configureStore from './redux/store'; -import { Backend } from './lib/backend'; +import { Backend, BackendError } from './lib/backend'; import type { ConnectionState } from './redux/connection/reducers'; import type { TrayIconType } from './lib/tray-icon-manager'; @@ -22,25 +22,23 @@ const store = configureStore(initialState, memoryHistory); // Backend ////////////////////////////////////////////////////////////////////////// const backend = new Backend(store); -ipcRenderer.on('backend-info', (_event, args) => { +ipcRenderer.on('backend-info', async (_event, args) => { backend.setCredentials(args.credentials); backend.sync(); - backend.autologin() - .then( () => { - return backend.syncRelaySettings(); - }) - .then( () => { - const { settings: { relaySettings: { host, protocol, port } } } = store.getState(); - - return backend.connect(host, protocol, port); - }) - .catch( e => { - if (e.type === 'NO_ACCOUNT') { + try { + await backend.autologin(); + await backend.fetchRelaySettings(); + await backend.connect(); + } catch (e) { + if(e instanceof BackendError) { + if(e.type === 'NO_ACCOUNT') { log.debug('No user set in the backend, showing window'); ipcRenderer.send('show-window'); } - }); + } + } }); + ipcRenderer.on('shutdown', () => { log.info('Been told by the node process to shutdown'); backend.shutdown() diff --git a/app/components/AdvancedSettings.js b/app/components/AdvancedSettings.js index 1821309369..b4306b81c8 100644 --- a/app/components/AdvancedSettings.js +++ b/app/components/AdvancedSettings.js @@ -7,16 +7,14 @@ import CustomScrollbars from './CustomScrollbars'; export class AdvancedSettings extends React.Component { props: { - host: string, protocol: string, - port: string|number, - onUpdateConstraints: (host: string, protocol: string, port: string|number) => void, + port: string | number, + onUpdate: (protocol: string, port: string | number) => void, onClose: () => void, }; render() { let portSelector = null; - const host = this.props.host; let protocol = this.props.protocol.toUpperCase(); if (protocol === 'AUTOMATIC') { @@ -32,7 +30,7 @@ export class AdvancedSettings extends React.Component { values={ ['Automatic', 'UDP', 'TCP'] } value={ protocol } onSelect={ protocol => { - this.props.onUpdateConstraints(host, protocol, 'Automatic'); + this.props.onUpdate(protocol, 'Automatic'); }}/> <div className="settings__cell-spacer"></div> @@ -43,7 +41,6 @@ export class AdvancedSettings extends React.Component { } _createPortSelector() { - const host = this.props.host; const protocol = this.props.protocol.toUpperCase(); const ports = protocol === 'TCP' ? ['Automatic', 80, 443] @@ -54,7 +51,7 @@ export class AdvancedSettings extends React.Component { values={ ports } value={ this.props.port } onSelect={ port => { - this.props.onUpdateConstraints(host, protocol, port); + this.props.onUpdate(protocol, port); }} />; } } diff --git a/app/components/Connect.js b/app/components/Connect.js index 2569e4b4b3..0975e18a1a 100644 --- a/app/components/Connect.js +++ b/app/components/Connect.js @@ -11,6 +11,7 @@ import type { ServerInfo } from '../lib/backend'; import type { HeaderBarStyle } from './HeaderBar'; import type { ConnectionReduxState } from '../redux/connection/reducers'; import type { SettingsReduxState } from '../redux/settings/reducers'; +import type { RelayLocation } from '../lib/ipc-facade'; export type ConnectProps = { accountExpiry: string, @@ -18,11 +19,11 @@ export type ConnectProps = { settings: SettingsReduxState, onSettings: () => void, onSelectLocation: () => void, - onConnect: (host: string) => void, + onConnect: () => void, onCopyIP: () => void, onDisconnect: () => void, onExternalLink: (type: string) => void, - getServerInfo: (identifier: string) => ?ServerInfo + getServerInfo: (relayLocation: RelayLocation) => ?ServerInfo }; @@ -93,42 +94,52 @@ export default class Connect extends Component { ); } - _getServerInfo() { + _getServerInfo(): ServerInfo { const { relaySettings } = this.props.settings; - if (relaySettings.host === 'any') { + if(relaySettings.normal) { + const location = relaySettings.normal.location; + if(location === 'any') { + return { + address: '', + name: 'Automatic', + country: 'Automatic', + city: 'Automatic', + country_code: 'any', + city_code: 'any', + location: [0, 0], + }; + } else { + const serverInfo = this.props.getServerInfo(location); + if(!serverInfo) { + throw new Error('Server info is not available for: ' + JSON.stringify(location)); + } + return serverInfo; + } + } else if(relaySettings.custom_tunnel_endpoint) { return { - name: 'Automatic', - country: 'Automatic', - city: 'Automatic', - address: '', + address: relaySettings.custom_tunnel_endpoint.host, + name: 'Custom', + country: 'Custom', + city: '', + country_code: 'auto', + city_code: 'auto', + location: [0, 0], }; + } else { + throw new Error('Unsupported relay settings.'); } - - return this.props.getServerInfo(relaySettings.host); } renderMap(): React.Element<*> { const serverInfo = this._getServerInfo(); - let isConnecting = false; - let isConnected = false; - let isDisconnected = false; + let [ isConnecting, isConnected, isDisconnected ] = [false, false, false]; switch(this.props.connection.status) { case 'connecting': isConnecting = true; break; case 'connected': isConnected = true; break; case 'disconnected': isDisconnected = true; break; } - const { city, country } = serverInfo && (isConnecting || isConnected) - ? serverInfo - : { city: '\u2003', country: '\u2002' }; - const ip = serverInfo && isConnected - ? serverInfo.address - : '\u2003'; //this.props.connection.clientIp; - const serverName = serverInfo - ? serverInfo.name - : '\u2003'; - // We decided to not include the map in the first beta release to customers // but it MUST be included in the following releases. Therefore we choose // to just comment it out @@ -147,9 +158,14 @@ export default class Connect extends Component { let ipComponent = undefined; if (isConnected || isDisconnected) { if (this.state.showCopyIPMessage) { - ipComponent = <span>{ 'IP copied to clipboard!' }</span>; + ipComponent = (<span>{ 'IP copied to clipboard!' }</span>); } else { - ipComponent = <span>{ ip }</span>; + // TODO: remove empty IP placeholder when implemented in backend. + if(isDisconnected) { + ipComponent = (<span>{ '\u2003' }</span>); + } else { + ipComponent = (<span>{ this.props.connection.clientIp }</span>); + } } } return ( @@ -173,29 +189,31 @@ export default class Connect extends Component { ********************************** */ } - { /* location when connecting */ } - <If condition={ isConnecting }> + { /* location when disconnected. + TODO: merge with the isConnecting block below when implemented in backend. + */ } + <If condition={ isDisconnected }> <Then> <div className="connect__status-location"> - <span>{ country }</span> + <span>{ '\u2002' }</span> </div> </Then> </If> - { /* location when connected */ } - <If condition={ isConnected }> + { /* location when connecting */ } + <If condition={ isConnecting }> <Then> <div className="connect__status-location"> - { city }<br/>{ country } + <span>{ this.props.connection.country }</span> </div> </Then> </If> - { /* location when disconnected */ } - <If condition={ isDisconnected }> + { /* location when connected */ } + <If condition={ isConnected }> <Then> <div className="connect__status-location"> - { country } + { this.props.connection.city }<br/>{ this.props.connection.country } </div> </Then> </If> @@ -228,14 +246,14 @@ export default class Connect extends Component { <div className="connect__server-label">Connect to</div> <div className="connect__server-value"> - <div className="connect__server-name">{ serverName }</div> + <div className="connect__server-name">{ serverInfo.name }</div> </div> </div> </div> <div className="connect__row"> - <button className="button button--positive" onClick={ this.onConnect.bind(this) }>Secure my connection</button> + <button className="button button--positive" onClick={ this.props.onConnect }>Secure my connection</button> </div> </div> </Then> @@ -284,15 +302,6 @@ export default class Connect extends Component { // Handlers - onConnect() { - const serverInfo = this._getServerInfo(); - if(!serverInfo) { - return; - } - - this.props.onConnect(serverInfo.address); - } - onExternalLink(type: string) { this.props.onExternalLink(type); } diff --git a/app/components/SelectLocation.js b/app/components/SelectLocation.js index b5f395b7ba..e0f50a12d4 100644 --- a/app/components/SelectLocation.js +++ b/app/components/SelectLocation.js @@ -1,71 +1,73 @@ // @flow import React, { Component } from 'react'; -import { If, Then } from 'react-if'; import { Layout, Container, Header } from './Layout'; -import { servers } from '../config'; import CustomScrollbars from './CustomScrollbars'; +import { servers } from '../config'; +import type { ServerInfo } from '../lib/backend'; import type { SettingsReduxState } from '../redux/settings/reducers'; +import type { RelayLocation } from '../lib/ipc-facade'; export type SelectLocationProps = { settings: SettingsReduxState, onClose: () => void; - onSelect: (server: string) => void; + onSelect: (location: RelayLocation) => void; }; export default class SelectLocation extends Component { props: SelectLocationProps; _selectedCell: ?HTMLElement; - onSelect(name: string) { - if (!this.isSelected(name)) { - this.props.onSelect(name); + _onSelect(location: RelayLocation) { + if (!this._isSelected(location)) { + this.props.onSelect(location); } } - isSelected(server: string) { - const { host } = this.props.settings.relaySettings; - return server === host; + _isSelected(selectedLocation: RelayLocation) { + const { relaySettings } = this.props.settings; + if(relaySettings.normal) { + const otherLocation = relaySettings.normal.location; + + if(selectedLocation.country && otherLocation.country && + selectedLocation.country === otherLocation.country) { + return true; + } + + if(Array.isArray(selectedLocation.city) && Array.isArray(otherLocation.city)) { + const selectedCity = selectedLocation.city; + const otherCity = otherLocation.city; + + return selectedCity.length === otherCity.length && + selectedCity.every((v, i) => v === otherCity[i]); + } + } + return false; } - drawCell(key: string, name: string, icon: ?string, onClick: (e: Event) => void): React.Element<*> { + drawCell(key: string, name: string, selected: bool, icon: ?string, onClick: (e: Event) => void): React.Element<*> { const classes = ['select-location__cell']; - const selected = this.isSelected(key); - if(selected) { classes.push('select-location__cell--selected'); } - const cellClass = classes.join(' '); + const onRef = selected ? (element) => { + this._selectedCell = element; + } : undefined; return ( - <div key={ key } className={ cellClass } onClick={ onClick } ref={ (e) => this.onCellRef(key, e) }> + <div key={ key } className={ cellClass } onClick={ onClick } ref={ onRef }> - <If condition={ !!icon }> - <Then> - <img className="select-location__cell-icon" src={ icon } /> - </Then> - </If> + { icon && <img className="select-location__cell-icon" src={ icon } />} <div className="select-location__cell-label">{ name }</div> - <If condition={ selected } > - <Then> - <img className="select-location__cell-accessory" src="./assets/images/icon-tick.svg" /> - </Then> - </If> + { selected && <img className="select-location__cell-accessory" src="./assets/images/icon-tick.svg" /> } </div> ); } - onCellRef(key: string, element: HTMLElement) { - // save reference to selected cell - if(this.isSelected(key)) { - this._selectedCell = element; - } - } - componentDidMount() { // restore scroll to selected cell const cell = this._selectedCell; @@ -99,7 +101,15 @@ export default class SelectLocation extends Component { <div className="select-location__separator"></div> - { Object.keys(servers).map((key) => this.drawCell(key, servers[key].name, null, this.onSelect.bind(this, key))) } + { (servers: Array<ServerInfo>).map((server) => { + const { address, name, country_code, city_code } = server; + const relayLocation = { + city: [ country_code, city_code ] + }; + const selected = this._isSelected(relayLocation); + const clickHandler = () => this._onSelect(relayLocation); + return this.drawCell(address, name, selected, null, clickHandler); + }) } </div> </CustomScrollbars> diff --git a/app/config.json b/app/config.json index ed984c0642..73eb310d3a 100644 --- a/app/config.json +++ b/app/config.json @@ -11,210 +11,285 @@ "supportEmail": "mailto:support@mullvad.net" }, "defaultServer": "193.138.218.135", - "servers": { - "168.1.6.5": { + "servers": [ + { "address": "168.1.6.5", "name": "Australia", "city": "Sydney", - "country": "Australia" + "country": "Australia", + "country_code": "au", + "city_code": "syd", + "location": [0, 0] }, - "217.64.127.138": { + { "address": "217.64.127.138", "name": "Austria", "city": "Wien", - "country": "Austria" + "country": "Austria", + "country_code": "at", + "city_code": "vie", + "location": [0, 0] }, - "185.104.186.202": { + { "address": "185.104.186.202", "name": "Belgium", "city": "Brussels", - "country": "Belgium" + "country": "Belgium", + "country_code": "be", + "city_code": "bru", + "location": [0, 0] }, - "185.94.192.42": { + { "address": "185.94.192.42", "name": "Bulgaria", "city": "Sofia", - "country": "Bulgaria" + "country": "Bulgaria", + "country_code": "bg", + "city_code": "sof", + "location": [0, 0] }, - "162.219.176.250": { + { "address": "162.219.176.250", "name": "Canada", "city": "Toronto", "country": "Canada", + "country_code": "ca", + "city_code": "tor", "location": [ 45.42153, -75.697193 ] }, - "185.156.174.146": { + { "address": "185.156.174.146", "name": "Czech Republic", "city": "Prague", - "country": "Czech Republic" + "country": "Czech Republic", + "country_code": "cz", + "city_code": "prg", + "location": [0, 0] }, - "82.103.140.213": { + { "address": "82.103.140.213", "name": "Denmark", "city": "Copenhagen", "country": "Denmark", + "country_code": "dk", + "city_code": "cph", "location": [ 55.6760968, 12.5683371 ] }, - "185.103.110.69": { + { "address": "185.103.110.69", "name": "Finland", "city": "Helsinki", - "country": "Finland" + "country": "Finland", + "country_code": "fi", + "city_code": "hel", + "location": [0, 0] }, - "185.156.173.218": { + { "address": "185.156.173.218", "name": "France", "city": "Paris", - "country": "France" + "country": "France", + "country_code": "fr", + "city_code": "par", + "location": [0, 0] }, - "185.104.184.178": { - "address": "185.104.184.178", + { + "address": "89.249.64.146", "name": "Germany", "city": "Berlin", "country": "Germany", + "country_code": "de", + "city_code": "ber", "location": [ - 52.52000659999999, - 13.404954 + 54.033333, + 10.45 ] }, - "161.202.48.245": { + { "address": "161.202.48.245", "name": "Hong Kong", "city": "Hong Kong", - "country": "Hong Kong" + "country": "Hong Kong", + "country_code": "hk", + "city_code": "hkg", + "location": [0, 0] }, - "185.189.114.10": { + { "address": "185.189.114.10", "name": "Hungary", "city": "Budapest", - "country": "Hungary" + "country": "Hungary", + "country_code": "hu", + "city_code": "bud", + "location": [0, 0] }, - "213.184.122.34": { + { "address": "213.184.122.34", "name": "Israel", "city": "Petach-Tikva", - "country": "Israel" + "country": "Israel", + "country_code": "il", + "city_code": "pet", + "location": [0, 0] }, - "217.64.113.180": { + { "address": "217.64.113.180", "name": "Italy", "city": "Milan", - "country": "Italy" + "country": "Italy", + "country_code": "it", + "city_code": "mil", + "location": [0, 0] }, - "161.202.144.203": { + { "address": "161.202.144.203", "name": "Japan", "city": "Tokyo", - "country": "Japan" + "country": "Japan", + "country_code": "jp", + "city_code": "tyo", + "location": [0, 0] }, - "185.65.134.140": { + { "address": "185.65.134.140", "name": "Netherlands", "city": "Amsterdam", "country": "Netherlands", + "country_code": "nl", + "city_code": "ams", "location": [ 52.3702157, 4.895167900000001 ] }, - "31.169.51.154": { + { "address": "31.169.51.154", "name": "Norway", "city": "Oslo", "country": "Norway", + "country_code": "no", + "city_code": "osl", "location": [ 59.9138688, 10.7522454 ] }, - "212.7.217.30": { + { "address": "212.7.217.30", "name": "Poland", "city": "Warsaw", - "country": "Poland" + "country": "Poland", + "country_code": "pl", + "city_code": "waw", + "location": [0, 0] }, - "185.45.13.10": { + { "address": "185.45.13.10", "name": "Romania", "city": "Bucharest", "country": "Romania", + "country_code": "ro", + "city_code": "buh", "location": [ 44.4267674, 26.1025384 ] }, - "103.57.72.30": { + { "address": "103.57.72.30", "name": "Singapore", "city": "Singapore", "country": "Singapore", + "country_code": "sg", + "city_code": "sin", "location": [ 1.352083, 103.819836 ] }, - "89.238.178.34": { + { "address": "89.238.178.34", "name": "Spain", "city": "Madrid", "country": "Spain", + "country_code": "es", + "city_code": "mad", "location": [ 40.4167754, -3.7037902 ] }, - "185.213.152.132": { + { "address": "185.213.152.132", "name": "Sweden - Helsingborg", "city": "Helsingborg", - "country": "Sweden" + "country": "Sweden", + "country_code": "se", + "city_code": "hel", + "location": [0, 0] }, - "193.138.218.135": { + { "address": "193.138.218.135", "name": "Sweden - Malmö", "city": "Malmö", - "country": "Sweden" + "country": "Sweden", + "country_code": "se", + "city_code": "mma", + "location": [0, 0] }, - "185.65.135.143": { + { "address": "185.65.135.143", "name": "Sweden - Stockholm", "city": "Stockholm", - "country": "Sweden" + "country": "Sweden", + "country_code": "se", + "city_code": "sto", + "location": [0, 0] }, - "179.43.128.170": { + { "address": "179.43.128.170", "name": "Switzerland", "city": "Zürich", - "country": "Switzerland" + "country": "Switzerland", + "country_code": "ch", + "city_code": "zrh", + "location": [0, 0] }, - "185.16.85.170": { + { "address": "185.16.85.170", "name": "United Kingdom", "city": "London", "country": "United Kingdom", + "country_code": "gb", + "city_code": "lon", "location": [ 51.5073509, -0.1277583 ] }, - "173.199.80.130": { + { "address": "173.199.80.130", "name": "USA - Los Angeles", "city": "Los Angeles", - "country": "USA" + "country": "USA", + "country_code": "us", + "city_code": "lax", + "location": [0, 0] }, - "38.132.107.138": { + { "address": "38.132.107.138", "name": "USA - New York", "city": "New York", - "country": "USA" + "country": "USA", + "country_code": "us", + "city_code": "nyc", + "location": [0, 0] } - } + ] } diff --git a/app/containers/AdvancedSettingsPage.js b/app/containers/AdvancedSettingsPage.js index 74e59ec60a..6f456abcea 100644 --- a/app/containers/AdvancedSettingsPage.js +++ b/app/containers/AdvancedSettingsPage.js @@ -1,46 +1,56 @@ +// @flow + import { connect } from 'react-redux'; import { push } from 'react-router-redux'; import { AdvancedSettings } from '../components/AdvancedSettings'; -import settingsActions from '../redux/settings/actions'; +import RelaySettingsBuilder from '../lib/relay-settings-builder'; import log from 'electron-log'; -const mapStateToProps = (state) => { - const constraints = state.settings.relaySettings; - const { host, protocol, port } = constraints; - return { - host: host, - protocol: protocol === 'any' ? 'Automatic' : protocol, - port: port === 'any' ? 'Automatic' : port, - }; +import type { ReduxState, ReduxDispatch } from '../redux/store'; +import type { SharedRouteProps } from '../routes'; + +const mapStateToProps = (state: ReduxState) => { + const relaySettings = state.settings.relaySettings; + if(relaySettings.normal) { + const { protocol, port } = relaySettings.normal; + return { + protocol: protocol === 'any' ? 'Automatic' : protocol, + port: port === 'any' ? 'Automatic' : port, + }; + } else if(relaySettings.custom_tunnel_endpoint) { + const { protocol, port } = relaySettings.custom_tunnel_endpoint; + return { protocol, port }; + } else { + throw new Error('Unknown type of relay settings.'); + } }; -const mapDispatchToProps = (dispatch, props) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { const { backend } = props; return { onClose: () => dispatch(push('/settings')), - onUpdateConstraints: (host, protocol, port) => { - // TODO: udp and 1301 are automatic because we cannot pass `any` when using custom tunnel - const protocolConstraint = protocol === 'Automatic' ? 'udp' : protocol.toLowerCase(); - const portConstraint = port === 'Automatic' ? (protocolConstraint === 'tcp' ? 443 : 1301) : port; - const update = { - custom_tunnel_endpoint: { - host: host, - tunnel: { - openvpn: { - protocol: protocolConstraint, - port: portConstraint, - } + onUpdate: async (protocol, port) => { + const relayUpdate = RelaySettingsBuilder.normal() + .tunnel.openvpn((openvpn) => { + if(protocol === 'Automatic') { + openvpn.protocol.any(); + } else { + openvpn.protocol.exact(protocol.toLowerCase()); + } + if(port === 'Automatic') { + openvpn.port.any(); + } else { + openvpn.port.exact(port); } - }, - }; + }).build(); - backend.updateRelaySettings(update) - .then( () => dispatch(settingsActions.updateRelay({ - protocol: protocolConstraint, - port: portConstraint, - }))) - .catch( e => log.error('Failed updating relay constraints', e.message)); + try { + await backend.updateRelaySettings(relayUpdate); + await backend.fetchRelaySettings(); + } catch(e) { + log.error('Failed to update relay settings', e.message); + } }, }; }; diff --git a/app/containers/ConnectPage.js b/app/containers/ConnectPage.js index 5c6dc1fa60..9f2df48df4 100644 --- a/app/containers/ConnectPage.js +++ b/app/containers/ConnectPage.js @@ -21,11 +21,11 @@ const mapDispatchToProps = (dispatch, props) => { return { onSettings: () => dispatch(push('/settings')), onSelectLocation: () => dispatch(push('/select-location')), - onConnect: (relayEndpoint) => connect(backend, relayEndpoint), + onConnect: () => connect(backend), onCopyIP: () => copyIPAddress(), onDisconnect: () => disconnect(backend), onExternalLink: (type) => shell.openExternal(links[type]), - getServerInfo: (key) => backend.serverInfo(key) + getServerInfo: (relayLocation) => backend.serverInfo(relayLocation), }; }; diff --git a/app/containers/SelectLocationPage.js b/app/containers/SelectLocationPage.js index 336186a2dc..c39f76eba7 100644 --- a/app/containers/SelectLocationPage.js +++ b/app/containers/SelectLocationPage.js @@ -1,27 +1,30 @@ +// @flow + import { connect } from 'react-redux'; import { push } from 'react-router-redux'; import SelectLocation from '../components/SelectLocation'; -import settingsActions from '../redux/settings/actions'; +import RelaySettingsBuilder from '../lib/relay-settings-builder'; import log from 'electron-log'; +import type { ReduxDispatch } from '../redux/store'; + const mapStateToProps = (state) => state; -const mapDispatchToProps = (dispatch, props) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props) => { const { backend } = props; return { onClose: () => dispatch(push('/connect')), - onSelect: (host) => { - dispatch(async (dispatch, getState) => { - try { - const { settings: { relaySettings: { protocol, port } } } = getState(); + onSelect: async (relayLocation) => { + try { + const relayUpdate = RelaySettingsBuilder.normal().location.fromRaw(relayLocation).build(); - backend.connect(host, protocol, port); + await backend.updateRelaySettings(relayUpdate); + await backend.fetchRelaySettings(); + await backend.connect(); - dispatch(settingsActions.updateRelay({ host: host })); - dispatch(push('/connect')); - } catch (e) { - log.error('Failed to select server: ', e.message); - } - }); + dispatch(push('/connect')); + } catch (e) { + log.error('Failed to select server: ', e.message); + } } }; }; diff --git a/app/lib/backend.js b/app/lib/backend.js index 900f5b8799..778997fa38 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -10,7 +10,7 @@ import settingsActions from '../redux/settings/actions'; import { push } from 'react-router-redux'; import type { ReduxStore } from '../redux/store'; -import type { AccountToken, BackendState, RelaySettingsUpdate, RelayProtocol } from './ipc-facade'; +import type { AccountToken, BackendState, RelayLocation, RelaySettingsUpdate } from './ipc-facade'; import type { ConnectionState } from '../redux/connection/reducers'; export type ErrorType = 'NO_CREDIT' | 'NO_INTERNET' | 'INVALID_ACCOUNT' | 'NO_ACCOUNT'; @@ -20,11 +20,11 @@ export type ServerInfo = { name: string, city: string, country: string, - location: [number, number] + country_code: string, + city_code: string, + location: [number, number], }; -export type ServerInfoList = { [string]: ServerInfo }; - export class BackendError extends Error { type: ErrorType; title: string; @@ -164,8 +164,22 @@ export class Backend { this._updateAccountHistory(); } - serverInfo(identifier: string): ?ServerInfo { - return (servers: ServerInfoList)[identifier]; + serverInfo(relay: RelayLocation): ?ServerInfo { + const list: Array<ServerInfo> = servers; + if(relay.country) { + const country = relay.country; + return list.find((server) => { + return server.country_code === country; + }); + } else if(relay.city) { + const [country_code, city_code] = relay.city; + return list.find((server) => { + return server.country_code === country_code && + server.city_code === city_code; + }); + } else { + return null; + } } login(accountToken: AccountToken): Promise<void> { @@ -186,18 +200,15 @@ export class Backend { log.info('Log in complete'); this._store.dispatch(accountActions.loginSuccessful(accountData.expiry)); - return this.syncRelaySettings(); + return this.fetchRelaySettings(); }) .then( () => { // Redirect the user after some time to allow for // the 'Login Successful' screen to be visible setTimeout(() => { - const { settings: { relaySettings: { host, protocol, port } } } = this._store.getState(); - - log.debug(`Autoconnecting to ${host}`); - this._store.dispatch(push('/connect')); - this.connect(host, protocol, port); + log.debug('Autoconnecting...'); + this.connect(); }, 1000); }).catch(e => { log.error('Failed to log in,', e.message); @@ -263,20 +274,9 @@ export class Backend { }); } - connect(host: string, protocol: RelayProtocol, port: number): Promise<void> { - const newRelaySettings = { - custom_tunnel_endpoint: { - host: host, - tunnel: { - openvpn: { protocol, port } - }, - }, - }; - - this._store.dispatch(connectionActions.connectingTo(host)); - + connect(): Promise<void> { + this._store.dispatch(connectionActions.connecting()); return this._ensureAuthenticated() - .then(() => this._ipc.updateRelaySettings(newRelaySettings)) .then(() => this._ipc.connect()) .catch((e) => { log.error('Backend.connect failed because: ', e.message); @@ -304,29 +304,53 @@ export class Backend { updateRelaySettings(relaySettings: RelaySettingsUpdate): Promise<void> { return this._ensureAuthenticated() - .then( () => { - return this._ipc.updateRelaySettings(relaySettings); - }); + .then(() => this._ipc.updateRelaySettings(relaySettings)); } - syncRelaySettings(): Promise<void> { - return this._ensureAuthenticated() - .then(() => this._ipc.getRelaySettings()) - .then((constraints) => { - log.debug('Got constraints from backend', constraints); + async fetchRelaySettings(): Promise<void> { + await this._ensureAuthenticated(); - if(constraints.normal) { - // TODO: handle normal constraints - log.warn('syncRelaySettings: Normal constraints are not implemented yet.'); - } else if(constraints.custom_tunnel_endpoint) { - const custom_tunnel_endpoint = constraints.custom_tunnel_endpoint; - const { host, tunnel: { openvpn: { port, protocol } } } = custom_tunnel_endpoint; - this._store.dispatch(settingsActions.updateRelay({ host, port, protocol })); - } - }) - .catch(e => { - log.error('Failed getting relay constraints', e); - }); + const relaySettings = await this._ipc.getRelaySettings(); + log.debug('Got relay settings from backend', relaySettings); + + if(relaySettings.normal) { + const payload = {}; + const normal = relaySettings.normal; + const tunnel = normal.tunnel; + const location = normal.location; + + if(location === 'any') { + payload.location = 'any'; + } else { + payload.location = location.only; + } + + if(tunnel === 'any') { + payload.port = 'any'; + payload.protocol = 'any'; + } else { + const { port, protocol } = tunnel.only.openvpn; + payload.port = port === 'any' ? port : port.only; + payload.protocol = protocol === 'any' ? protocol : protocol.only; + } + + this._store.dispatch( + settingsActions.updateRelay({ + normal: payload + }) + ); + } else if(relaySettings.custom_tunnel_endpoint) { + const custom_tunnel_endpoint = relaySettings.custom_tunnel_endpoint; + const { host, tunnel: { openvpn: { port, protocol } } } = custom_tunnel_endpoint; + + this._store.dispatch( + settingsActions.updateRelay({ + custom_tunnel_endpoint: { + host, port, protocol + } + }) + ); + } } removeAccountFromHistory(accountToken: AccountToken): Promise<void> { @@ -349,14 +373,6 @@ export class Backend { .catch(e => log.info('Failed to fetch account history,', e.message)); } - _apiToReduxConstraints(constraint: *): * { - if (typeof(constraint) === 'object') { - return constraint.only; - } else { - return constraint; - } - } - /** * Start reachability monitoring for online/offline detection * This is currently done via HTML5 APIs but will be replaced later diff --git a/app/lib/relay-settings-builder.js b/app/lib/relay-settings-builder.js new file mode 100644 index 0000000000..4d83956a00 --- /dev/null +++ b/app/lib/relay-settings-builder.js @@ -0,0 +1,195 @@ +// @flow + +import type { + RelayLocation, + RelayProtocol, + RelaySettingsUpdate, + RelaySettingsNormalUpdate, + RelaySettingsCustom +} from './ipc-facade'; + +type LocationBuilder<Self> = { + country: (country: string) => Self, + city: (country: string, city: string) => Self, + any: () => Self, + fromRaw: (location: 'any' | RelayLocation) => Self, +}; + +type OpenVPNConfigurator<Self> = { + port: { + exact: (port: number) => Self, + any: () => Self + }, + protocol: { + exact: (protocol: RelayProtocol) => Self, + any: () => Self + } +}; + +type TunnelBuilder<Self> = { + openvpn: (configurator: (OpenVPNConfigurator<*>) => void) => Self +}; + +class NormalRelaySettingsBuilder { + _payload: RelaySettingsNormalUpdate = {}; + + build(): RelaySettingsUpdate { + return { + normal: this._payload + }; + } + + get location(): LocationBuilder<NormalRelaySettingsBuilder> { + return { + country: (country: string) => { + this._payload.location = { only: { country } }; + return this; + }, + city: (country: string, city: string) => { + this._payload.location = { only: { city: [country, city] } }; + return this; + }, + any: () => { + this._payload.location = 'any'; + return this; + }, + fromRaw: function (location: 'any' | RelayLocation) { + if(location === 'any') { + return this.any(); + } + + if(location.city) { + const [country, city] = location.city; + return this.city(country, city); + } + + if(location.country) { + return this.country(location.country); + } + + throw new Error('Unsupported value of RelayLocation' + + (location && JSON.stringify(location)) ); + }, + }; + } + + get tunnel(): TunnelBuilder<NormalRelaySettingsBuilder> { + const updateOpenvpn = (next) => { + const tunnel = this._payload.tunnel; + if(typeof(tunnel) === 'string' || typeof(tunnel) === 'undefined') { + this._payload.tunnel = { + only: { + openvpn: next + } + }; + } else if(typeof(tunnel) === 'object') { + const prev = (tunnel.only && tunnel.only.openvpn) || {}; + this._payload.tunnel = { + only: { + openvpn: { ...prev, ...next } + } + }; + } + }; + + return { + openvpn: (configurator) => { + const openvpnBuilder = { + get port() { + const apply = (port) => { + updateOpenvpn({ port }); + return this; + }; + return { + exact: (value: number) => apply({ only: value }), + any: () => apply('any'), + }; + }, + get protocol() { + const apply = (protocol) => { + updateOpenvpn({ protocol }); + return this; + }; + return { + exact: (value: RelayProtocol) => apply({ only: value }), + any: () => apply('any'), + }; + } + }; + + configurator(openvpnBuilder); + + return this; + }, + any: () => { + this._payload.tunnel = 'any'; + return this; + } + }; + } + +} + + +type CustomOpenVPNConfigurator<Self> = { + port: (port: number) => Self, + protocol: (protocol: RelayProtocol) => Self +}; + +type CustomTunnelBuilder<Self> = { + openvpn: (configurator: (CustomOpenVPNConfigurator<*>) => void) => Self +}; + +class CustomRelaySettingsBuilder { + _payload: RelaySettingsCustom = { + host: '', + tunnel: { + openvpn: { + port: 0, + protocol: 'udp' + } + } + }; + + build(): RelaySettingsUpdate { + return { + custom_tunnel_endpoint: this._payload + }; + } + + host(value: string) { + this._payload.host = value; + return this; + } + + get tunnel(): CustomTunnelBuilder<CustomRelaySettingsBuilder> { + const updateOpenvpn = (next) => { + const tunnel = this._payload.tunnel || {}; + const prev = tunnel.openvpn || {}; + this._payload.tunnel = { + openvpn: { ...prev, ...next } + }; + }; + + return { + openvpn: (configurator) => { + configurator({ + port: function (port: number) { + updateOpenvpn({ port }); + return this; + }, + protocol: function (protocol: RelayProtocol) { + updateOpenvpn({ protocol }); + return this; + } + }); + return this; + } + }; + } +} + +export default { + normal: () => new NormalRelaySettingsBuilder(), + custom: () => new CustomRelaySettingsBuilder(), +};
\ No newline at end of file diff --git a/app/redux/connection/actions.js b/app/redux/connection/actions.js index fb4320bf3a..f2ebf2c7ba 100644 --- a/app/redux/connection/actions.js +++ b/app/redux/connection/actions.js @@ -6,12 +6,7 @@ import type { Backend } from '../../lib/backend'; import type { ReduxThunk } from '../store'; import type { Coordinate2d } from '../../types'; -const connect = (backend: Backend, relay: string): ReduxThunk => { - return (_, getState) => { - const { settings: { relaySettings: { protocol, port } } } = getState(); - backend.connect(relay, protocol, port); - }; -}; +const connect = (backend: Backend): ReduxThunk => () => backend.connect(); const disconnect = (backend: Backend) => () => backend.disconnect(); const copyIPAddress = (): ReduxThunk => { return (_, getState) => { @@ -25,7 +20,6 @@ const copyIPAddress = (): ReduxThunk => { type ConnectingAction = { type: 'CONNECTING', - host?: string, }; type ConnectedAction = { type: 'CONNECTED', @@ -66,13 +60,6 @@ export type ConnectionAction = NewPublicIpAction | OnlineAction | OfflineAction; -function connectingTo(host: string): ConnectingAction { - return { - type: 'CONNECTING', - host: host, - }; -} - function connecting(): ConnectingAction { return { type: 'CONNECTING', @@ -118,5 +105,5 @@ function offline(): OfflineAction { } -export default { connect, disconnect, copyIPAddress, newPublicIp, newLocation, connectingTo, connecting, connected, disconnected, online, offline }; +export default { connect, disconnect, copyIPAddress, newPublicIp, newLocation, connecting, connected, disconnected, online, offline }; diff --git a/app/redux/connection/reducers.js b/app/redux/connection/reducers.js index 48177fc5b6..52c1d648c1 100644 --- a/app/redux/connection/reducers.js +++ b/app/redux/connection/reducers.js @@ -7,7 +7,6 @@ export type ConnectionState = 'disconnected' | 'connecting' | 'connected'; export type ConnectionReduxState = { status: ConnectionState, isOnline: boolean, - serverAddress: ?string, clientIp: ?string, location: ?Coordinate2d, country: ?string, @@ -17,7 +16,6 @@ export type ConnectionReduxState = { const initialState: ConnectionReduxState = { status: 'disconnected', isOnline: true, - serverAddress: null, clientIp: null, location: null, country: null, @@ -38,7 +36,7 @@ export default function(state: ConnectionReduxState = initialState, action: Redu return { ...state, ...action.newLocation }; case 'CONNECTING': - return { ...state, ...{ status: 'connecting', serverAddress: action.host }}; + return { ...state, ...{ status: 'connecting' }}; case 'CONNECTED': return { ...state, ...{ status: 'connected' }}; diff --git a/app/redux/settings/actions.js b/app/redux/settings/actions.js index 4cc4910e01..74f8f120f7 100644 --- a/app/redux/settings/actions.js +++ b/app/redux/settings/actions.js @@ -1,15 +1,15 @@ // @flow -import type { RelaySettings } from './reducers'; +import type { RelaySettingsRedux } from './reducers'; export type UpdateRelayAction = { type: 'UPDATE_RELAY', - relay: RelaySettings, + relay: RelaySettingsRedux, }; export type SettingsAction = UpdateRelayAction; -function updateRelay(relay: RelaySettings): UpdateRelayAction { +function updateRelay(relay: RelaySettingsRedux): UpdateRelayAction { return { type: 'UPDATE_RELAY', relay: relay, diff --git a/app/redux/settings/reducers.js b/app/redux/settings/reducers.js index 2754088d1b..56548a8d12 100644 --- a/app/redux/settings/reducers.js +++ b/app/redux/settings/reducers.js @@ -1,24 +1,33 @@ // @flow -import { defaultServer } from '../../config'; - import type { ReduxAction } from '../store'; +import type { RelayProtocol, RelayLocation } from '../../lib/ipc-facade'; -export type RelaySettings = { +export type RelaySettingsRedux = {| + normal: { + location: 'any' | RelayLocation, + port: 'any' | number, + protocol: 'any' | RelayProtocol, + } +|} | {| + custom_tunnel_endpoint: { host: string, port: number, - protocol: 'tcp' | 'udp', -}; + protocol: RelayProtocol, + } +|}; export type SettingsReduxState = { - relaySettings: RelaySettings + relaySettings: RelaySettingsRedux }; const initialState: SettingsReduxState = { relaySettings: { - host: defaultServer, - port: 1301, - protocol: 'udp', + normal: { + location: 'any', + port: 'any', + protocol: 'any', + } }, }; @@ -26,10 +35,7 @@ export default function(state: SettingsReduxState = initialState, action: ReduxA if (action.type === 'UPDATE_RELAY') { return { ...state, - relaySettings: { - ...state.relaySettings, - ...action.relay, - }, + relaySettings: action.relay, }; } diff --git a/test/auth.spec.js b/test/auth.spec.js index 1725038acd..c3fd78c83d 100644 --- a/test/auth.spec.js +++ b/test/auth.spec.js @@ -29,7 +29,7 @@ describe('authentication', () => { const backend = new Backend(store, credentials, mockIpc); - backend.connect('example.com', 'udp', 1301); + backend.connect(); }); it('reauthenticates on reconnect', (done) => { @@ -48,7 +48,7 @@ describe('authentication', () => { }, done); - backend.connect('example.com', 'udp', 1301); + backend.connect(); checkNextTick(() => { expect(authCount).to.equal(1); }, done); diff --git a/test/components/Connect.spec.js b/test/components/Connect.spec.js index d672aa77d6..748dd25761 100644 --- a/test/components/Connect.spec.js +++ b/test/components/Connect.spec.js @@ -7,12 +7,17 @@ import { mount } from 'enzyme'; import Connect from '../../app/components/Connect'; import Header from '../../app/components/HeaderBar'; -import type { ReactWrapper } from 'enzyme'; +import type { ConnectProps } from '../../app/components/Connect'; describe('components/Connect', () => { - it('shows unsecured hints when not connected', () => { - const component = renderNotConnected(); + it('shows unsecured hints when disconnected', () => { + const component = renderWithProps({ + connection: { + ...defaultConnection, + status: 'disconnected', + } + }); const header = component.find(Header); const securityMessage = component.find('.connect__status-security--unsecured'); @@ -24,7 +29,12 @@ describe('components/Connect', () => { }); it('shows secured hints when connected', () => { - const component = renderConnected(); + const component = renderWithProps({ + connection: { + ...defaultConnection, + status: 'connected', + } + }); const header = component.find(Header); const securityMessage = component.find('.connect__status-security--secure'); @@ -36,90 +46,106 @@ describe('components/Connect', () => { }); it('shows the connection location when connecting', () => { - const component = renderConnecting({ - getServerInfo: (_s) => ({ - address: '185.65.132.102', - name: '', - location: [0, 0], - country: 'norway', - city: 'oslo', - }), - }, { - clientIp: '185.65.132.102', + const component = renderWithProps({ + connection: { + ...defaultConnection, + status: 'connecting', + country: 'Norway', + city: 'Oslo', + } }); const countryAndCity = component.find('.connect__status-location'); const ipAddr = component.find('.connect__status-ipaddress'); - expect(countryAndCity.text()).to.contain('norway'); - expect(countryAndCity.text()).not.to.contain('oslo'); + expect(countryAndCity.text()).to.contain('Norway'); + expect(countryAndCity.text()).not.to.contain('Oslo'); expect(ipAddr.text()).to.be.empty; }); it('shows the connection location when connected', () => { - const component = renderConnected({ - getServerInfo: (_s) => ({ - address: '185.65.132.102', - name: '', - location: [0, 0], - country: 'sweden', - city: 'gothenburg', - }), - }, { - clientIp: '185.65.132.102', + const component = renderWithProps({ + connection: { + ...defaultConnection, + status: 'connected', + country: 'Norway', + city: 'Oslo', + clientIp: '4.3.2.1', + } }); const countryAndCity = component.find('.connect__status-location'); const ipAddr = component.find('.connect__status-ipaddress'); - expect(countryAndCity.text()).to.contain('sweden'); - expect(countryAndCity.text()).to.contain('gothenburg'); - expect(ipAddr.text()).to.contain('185.65.132.102'); + expect(countryAndCity.text()).to.contain('Norway'); + expect(countryAndCity.text()).to.contain('Oslo'); + expect(ipAddr.text()).to.contain('4.3.2.1'); }); it('shows the connection location when disconnected', () => { - const component = renderNotConnected({ - getServerInfo: (_s) => ({ - address: '\u2003', - name: '', - location: [0, 0], - country: 'sweden', - city: 'gothenburg', - }), - }, { - clientIp: '\u2003', + const component = renderWithProps({ + connection: { + ...defaultConnection, + status: 'disconnected', + country: 'Norway', + city: 'Oslo', + clientIp: '4.3.2.1', + } }); const countryAndCity = component.find('.connect__status-location'); const ipAddr = component.find('.connect__status-ipaddress'); expect(countryAndCity.text()).to.contain('\u2002'); - expect(countryAndCity.text()).to.not.contain('\u2003'); + expect(countryAndCity.text()).to.not.contain('Oslo'); expect(ipAddr.text()).to.contain('\u2003'); }); it('shows the country name in the location switcher', () => { - const servers = { - 'se1.mullvad.net': { name: 'Sweden' }, - }; - const getServerInfo = (key) => servers[key] || defaultServer; - const component = renderNotConnected({ - getServerInfo: getServerInfo, - }); - const locationSwitcher = component.find('.connect__server'); + const servers = [{ + address: '1.2.3.4', + name: 'Sweden - Malmö', + city: 'Malmö', + country: 'Sweden', + country_code: 'se', + city_code: 'mma', + location: [0, 0] + }]; - component.setProps({ + const component = renderWithProps({ + connection: { + ...defaultConnection, + status: 'disconnected', + }, settings: { relaySettings: { - host: 'se1.mullvad.net', - protocol: 'udp', - port: 1301, + normal: { + location: { city: ['se', 'mma'] }, + protocol: 'any', + port: 'any', + } }, }, + getServerInfo: (location) => { + return servers.find((server) => { + if(location.city) { + const [country_code, city_code] = location.city; + return (server.city_code === city_code && + server.country_code === country_code); + } + return false; + }); + }, }); - expect(locationSwitcher.text()).to.contain(servers['se1.mullvad.net'].name); + + const locationSwitcher = component.find('.connect__server'); + expect(locationSwitcher.text()).to.contain(servers[0].name); }); it('invokes the onConnect prop', (done) => { - const component = renderNotConnected({ + const component = renderWithProps({ onConnect: () => done(), + connection: { + ...defaultConnection, + status: 'disconnected', + } }); const connectButton = component.find('.button .button--positive'); @@ -127,72 +153,38 @@ describe('components/Connect', () => { }); }); -function renderNotConnected(customProps, customConnectionProps) { - const connection = Object.assign({}, defaultConnection, { - status: 'disconnected', - }, customConnectionProps); - - const props = Object.assign({}, customProps, {connection}); - return renderWithProps(props); -} - -function renderConnecting(customProps, customConnectionProps) { - const connection = Object.assign({}, defaultConnection, { - status: 'connecting', - }, customConnectionProps); - - const props = Object.assign({}, customProps, {connection}); - return renderWithProps(props); -} - -function renderConnected(customProps, customConnectionProps) { - const connection = Object.assign({}, defaultConnection, { - status: 'connected', - }, customConnectionProps); - - const props = Object.assign({}, customProps, {connection}); - return renderWithProps(props); -} - -function renderWithProps(customProps): ReactWrapper { - const props = Object.assign({}, defaultProps, customProps); - return mount( <Connect { ...props } /> ); -} -const noop = () => {}; -const defaultServer = { - address: '', - name: '', - city: '', - country: '', - location: [0, 0], -}; const defaultConnection = { status: 'disconnected', isOnline: true, - serverAddress: null, clientIp: null, location: null, country: null, city: null, }; -const defaultProps = { - onSettings: noop, - onSelectLocation: noop, - onConnect: noop, - onCopyIP: noop, - onDisconnect: noop, - onExternalLink: noop, - getServerInfo: (_) => { return defaultServer; }, - +const defaultProps: ConnectProps = { + onSettings: () => {}, + onSelectLocation: () => {}, + onConnect: () => {}, + onCopyIP: () => {}, + onDisconnect: () => {}, + onExternalLink: () => {}, + getServerInfo: _ => null, accountExpiry: '', settings: { relaySettings: { - host: 'www.example.com', - protocol: 'udp', - port: 1301, + normal: { + location: 'any', + protocol: 'any', + port: 'any', + } }, }, connection: defaultConnection, }; + +function renderWithProps(customProps: $Shape<ConnectProps>) { + const props = { ...defaultProps, ...customProps }; + return mount( <Connect { ...props } /> ); +} diff --git a/test/components/SelectLocation.spec.js b/test/components/SelectLocation.spec.js index 937622cb36..439edc1865 100644 --- a/test/components/SelectLocation.spec.js +++ b/test/components/SelectLocation.spec.js @@ -11,9 +11,11 @@ import type { SelectLocationProps } from '../../app/components/SelectLocation'; describe('components/SelectLocation', () => { const state: SettingsReduxState = { relaySettings: { - host: 'example.com', - protocol: 'udp', - port: 1301, + normal: { + location: 'any', + protocol: 'any', + port: 'any', + } }, }; diff --git a/test/components/Settings.spec.js b/test/components/Settings.spec.js index 419c3e509c..31a5fa09a9 100644 --- a/test/components/Settings.spec.js +++ b/test/components/Settings.spec.js @@ -36,9 +36,11 @@ describe('components/Settings', () => { const settingsState: SettingsReduxState = { relaySettings: { - host: 'example.com', - protocol: 'udp', - port: 1301, + normal: { + location: 'any', + protocol: 'udp', + port: 1301, + }, }, }; diff --git a/test/connect.spec.js b/test/connect.spec.js index a7f991ec50..056e1bfe15 100644 --- a/test/connect.spec.js +++ b/test/connect.spec.js @@ -3,34 +3,9 @@ import { expect } from 'chai'; import connectionActions from '../app/redux/connection/actions'; import { setupBackendAndStore, checkNextTick } from './helpers/ipc-helpers'; -import { IpcChain } from './helpers/IpcChain'; describe('connect', () => { - it('should invoke update_relay_settings and then connect in the backend', (done) => { - const { store, mockIpc, backend } = setupBackendAndStore(); - - const chain = new IpcChain(mockIpc); - chain.require('updateRelaySettings') - .withInputValidation( - relayEndpoint => { - if (relayEndpoint) { - expect(relayEndpoint.custom_tunnel_endpoint.host).to.equal(arbitraryRelay); - } else { - expect.fail(); - } - }, - ) - .done(); - - chain.require('connect') - .done(); - - chain.onSuccessOrFailure(done); - - store.dispatch(connectionActions.connect(backend, arbitraryRelay)); - }); - it('should set the connection state to \'disconnected\' on failed attempts', (done) => { const { store, mockIpc, backend } = setupBackendAndStore(); @@ -41,7 +16,7 @@ describe('connect', () => { expect(store.getState().connection.status).not.to.equal('disconnected'); - store.dispatch(connectionActions.connect(backend, arbitraryRelay)); + store.dispatch(connectionActions.connect(backend)); checkNextTick(() => { @@ -52,11 +27,10 @@ describe('connect', () => { it('should update the state with the server address', () => { const { store, backend } = setupBackendAndStore(); - return backend.connect('www.example.com', 'udp', 1301) + return backend.connect() .then( () => { const state = store.getState().connection; expect(state.status).to.equal('connecting'); - expect(state.serverAddress).to.equal('www.example.com'); }); }); @@ -90,6 +64,4 @@ describe('connect', () => { expect(store.getState().connection.status).to.equal('disconnected'); }, done); }); -}); - -const arbitraryRelay = 'www.example.com'; +});
\ No newline at end of file diff --git a/test/relay-settings-builder.spec.js b/test/relay-settings-builder.spec.js new file mode 100644 index 0000000000..3f27d2df23 --- /dev/null +++ b/test/relay-settings-builder.spec.js @@ -0,0 +1,151 @@ +// @flow + +import { expect } from 'chai'; +import RelaySettingsBuilder from '../app/lib/relay-settings-builder'; + +describe('Relay settings builder', () => { + + it('should set location to any', () => { + expect( + RelaySettingsBuilder.normal() + .location + .any() + .build() + ).to.deep.equal({ + normal: { + location: 'any' + } + }); + }); + + it('should bound location to city', () => { + expect( + RelaySettingsBuilder.normal() + .location.city('se', 'mma').build() + ).to.deep.equal({ + normal: { + location: { + only: { + city: ['se', 'mma'] + } + } + } + }); + }); + + it('should bound location to country', () => { + expect( + RelaySettingsBuilder.normal() + .location.country('se').build() + ).to.deep.equal({ + normal: { + location: { + only: { country: 'se' } + } + } + }); + }); + + it('should set openvpn settings to any', () => { + expect( + RelaySettingsBuilder.normal() + .tunnel.openvpn(openvpn => { + openvpn.port.any() + .protocol.any(); + }) + .build() + ).to.deep.equal({ + normal: { + tunnel: { + only: { + openvpn: { + port: 'any', + protocol: 'any' + } + } + } + } + }); + }); + + it('should set openvpn settings to exact values', () => { + expect( + RelaySettingsBuilder.normal() + .tunnel.openvpn(openvpn => { + openvpn.port.exact(80) + .protocol.exact('tcp'); + }) + .build() + ).to.deep.equal({ + normal: { + tunnel: { + only: { + openvpn: { + port: { only: 80 }, + protocol: { only: 'tcp' } + } + } + } + } + }); + }); + + it('should set location from raw RelayLocation', () => { + expect( + RelaySettingsBuilder.normal() + .location.fromRaw('any') + .build() + ).to.deep.equal({ + normal: { + location: 'any' + } + }); + + expect( + RelaySettingsBuilder.normal() + .location.fromRaw({ country: 'se'}) + .build() + ).to.deep.equal({ + normal: { + location: { + only: { country: 'se' } + } + } + }); + + expect( + RelaySettingsBuilder.normal() + .location.fromRaw({ city: ['se', 'mma']}) + .build() + ).to.deep.equal({ + normal: { + location: { + only: { city: ['se', 'mma'] } + } + } + }); + }); + + it('should set custom endpoint settings', () => { + expect( + RelaySettingsBuilder.custom() + .host('se2.mullvad.net') + .tunnel.openvpn((openvpn) => { + openvpn.port(80) + .protocol('tcp'); + }) + .build() + ).to.deep.equal({ + custom_tunnel_endpoint: { + host: 'se2.mullvad.net', + tunnel: { + openvpn: { + port: 80, + protocol: 'tcp' + } + } + } + }); + }); + +});
\ No newline at end of file |
