diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2017-11-23 15:53:28 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2017-12-06 11:48:39 +0100 |
| commit | add9f88c0070514c0de8b3ef0f37e8f7a8726222 (patch) | |
| tree | 8dd1a73f93aac194790e6b008d96c1e459b05ff1 | |
| parent | 6cee4adfe1b314ece9773697b89370e964c5279e (diff) | |
| download | mullvadvpn-add9f88c0070514c0de8b3ef0f37e8f7a8726222.tar.xz mullvadvpn-add9f88c0070514c0de8b3ef0f37e8f7a8726222.zip | |
Add relay constraint builder and basic tests
| -rw-r--r-- | app/components/AdvancedSettings.js | 11 | ||||
| -rw-r--r-- | app/containers/AdvancedSettingsPage.js | 63 | ||||
| -rw-r--r-- | app/containers/SelectLocationPage.js | 12 | ||||
| -rw-r--r-- | app/lib/relay-settings-builder.js | 195 | ||||
| -rw-r--r-- | test/relay-settings-builder.spec.js | 151 |
5 files changed, 394 insertions, 38 deletions
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/containers/AdvancedSettingsPage.js b/app/containers/AdvancedSettingsPage.js index 74e59ec60a..9155ba9a19 100644 --- a/app/containers/AdvancedSettingsPage.js +++ b/app/containers/AdvancedSettingsPage.js @@ -1,17 +1,25 @@ +// @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, - }; + 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) => { @@ -19,28 +27,27 @@ const mapDispatchToProps = (dispatch, 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/SelectLocationPage.js b/app/containers/SelectLocationPage.js index 0841f6a760..987173856b 100644 --- a/app/containers/SelectLocationPage.js +++ b/app/containers/SelectLocationPage.js @@ -1,7 +1,9 @@ +// @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'; const mapStateToProps = (state) => state; @@ -11,8 +13,12 @@ const mapDispatchToProps = (dispatch, props) => { onClose: () => dispatch(push('/connect')), onSelect: async (relayLocation) => { try { - await backend.connect(relayLocation); - dispatch(settingsActions.updateRelay({ normal: { location: relayLocation } })); + const relayUpdate = RelaySettingsBuilder.normal().location.fromRaw(relayLocation).build(); + + await backend.updateRelaySettings(relayUpdate); + await backend.fetchRelaySettings(); + await backend.connect(); + dispatch(push('/connect')); } catch (e) { log.error('Failed to select server: ', e.message); 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/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 |
