summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2017-11-23 15:53:28 +0100
committerAndrej Mihajlov <and@mullvad.net>2017-12-06 11:48:39 +0100
commitadd9f88c0070514c0de8b3ef0f37e8f7a8726222 (patch)
tree8dd1a73f93aac194790e6b008d96c1e459b05ff1
parent6cee4adfe1b314ece9773697b89370e964c5279e (diff)
downloadmullvadvpn-add9f88c0070514c0de8b3ef0f37e8f7a8726222.tar.xz
mullvadvpn-add9f88c0070514c0de8b3ef0f37e8f7a8726222.zip
Add relay constraint builder and basic tests
-rw-r--r--app/components/AdvancedSettings.js11
-rw-r--r--app/containers/AdvancedSettingsPage.js63
-rw-r--r--app/containers/SelectLocationPage.js12
-rw-r--r--app/lib/relay-settings-builder.js195
-rw-r--r--test/relay-settings-builder.spec.js151
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