diff options
| author | Linus Färnstrand <linus@mullvad.net> | 2018-09-05 18:39:51 +0200 |
|---|---|---|
| committer | Linus Färnstrand <linus@mullvad.net> | 2018-09-05 18:39:51 +0200 |
| commit | 619e081cb90c3cf6b49c51e81ea6f6c3fbe1d9a9 (patch) | |
| tree | 5a5601eddbc639aa80808f80bf184ab97fe00220 | |
| parent | fb40dedf46348e8764aed95610b135e40c3b1ba1 (diff) | |
| parent | c3eeeaaa9db90bbf4c6328286854e850d2f8156b (diff) | |
| download | mullvadvpn-619e081cb90c3cf6b49c51e81ea6f6c3fbe1d9a9.tar.xz mullvadvpn-619e081cb90c3cf6b49c51e81ea6f6c3fbe1d9a9.zip | |
Merge branch 'select-individual-servers'
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/app.js | 1 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/SelectLocation.js | 94 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/SelectLocationStyles.js | 18 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/containers/ConnectPage.js | 9 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/lib/daemon-rpc.js | 26 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/lib/relay-settings-builder.js | 9 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/redux/settings/reducers.js | 9 | ||||
| -rw-r--r-- | gui/packages/desktop/test/components/SelectLocation.spec.js | 18 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/relay.rs | 32 | ||||
| -rw-r--r-- | mullvad-daemon/src/relays.rs | 18 | ||||
| -rw-r--r-- | mullvad-types/src/location.rs | 1 | ||||
| -rw-r--r-- | mullvad-types/src/relay_constraints.rs | 4 | ||||
| -rw-r--r-- | mullvad-types/src/relay_list.rs | 12 |
14 files changed, 230 insertions, 22 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 720dac928a..7c9ee2c6cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Line wrap the file at 100 chars. Th configuration, which prevents leaking traffic until the user specifically requests to disconnect. - Add support for Ubuntu 14.04 and other distributions that use the Upstart init system. - Make scrollbar thumb draggable. +- Ability to expand cities with multiple servers and configure the app to use a specific server. #### Windows - Extend uninstaller to also remove logs, cache and optionally settings. diff --git a/gui/packages/desktop/src/renderer/app.js b/gui/packages/desktop/src/renderer/app.js index 8c170c1446..6cf203f4cb 100644 --- a/gui/packages/desktop/src/renderer/app.js +++ b/gui/packages/desktop/src/renderer/app.js @@ -346,6 +346,7 @@ export default class AppRenderer { latitude: city.latitude, longitude: city.longitude, hasActiveRelays: city.has_active_relays, + relays: city.relays, })), })); diff --git a/gui/packages/desktop/src/renderer/components/SelectLocation.js b/gui/packages/desktop/src/renderer/components/SelectLocation.js index 70e2bed978..ee1bb13c2c 100644 --- a/gui/packages/desktop/src/renderer/components/SelectLocation.js +++ b/gui/packages/desktop/src/renderer/components/SelectLocation.js @@ -15,6 +15,7 @@ import type { SettingsReduxState, RelayLocationRedux, RelayLocationCityRedux, + RelayLocationRelayRedux, } from '../redux/settings/reducers'; import type { RelayLocation } from '../lib/daemon-rpc'; @@ -48,7 +49,15 @@ export default class SelectLocation extends React.Component<SelectLocationProps, } else if (location.country) { this.state.expanded.push(location.country); } else if (location.city) { - this.state.expanded.push(location.city[0]); + const countryCode = location.city[0]; + + this.state.expanded.push(countryCode); + } else if (location.hostname) { + const countryCode = location.hostname[0]; + const cityCode = location.hostname[1]; + + this.state.expanded.push(countryCode); + this.state.expanded.push(`${countryCode}_${cityCode}`); } } } @@ -124,6 +133,16 @@ export default class SelectLocation extends React.Component<SelectLocationProps, selectedCity.every((v, i) => v === otherCity[i]) ); } + + if (Array.isArray(selectedLocation.hostname) && Array.isArray(otherLocation.hostname)) { + const selectedRelay = selectedLocation.hostname; + const otherRelay = otherLocation.hostname; + + return ( + selectedRelay.length === otherRelay.length && + selectedRelay.every((v, i) => v === otherRelay[i]) + ); + } } return false; } @@ -210,6 +229,7 @@ export default class SelectLocation extends React.Component<SelectLocationProps, } _renderCity(countryCode: string, relayCity: RelayLocationCityRedux) { + const expandedCode = `${countryCode}_${relayCity.code}`; const relayLocation: RelayLocation = { city: [countryCode, relayCity.code] }; const isSelected = this._isSelected(relayLocation); @@ -220,6 +240,9 @@ export default class SelectLocation extends React.Component<SelectLocationProps, } : undefined; + // either expanded by user or when the city or a relay from the city is selected + const isExpanded = this.state.expanded.includes(expandedCode); + const handleSelect = relayCity.hasActiveRelays && !isSelected ? () => { @@ -227,18 +250,73 @@ export default class SelectLocation extends React.Component<SelectLocationProps, } : undefined; + const handleCollapse = (e) => { + this._toggleCollapse(expandedCode); + e.stopPropagation(); + }; + + return ( + <View key={expandedCode}> + <Cell.CellButton + onPress={handleSelect} + disabled={!relayCity.hasActiveRelays} + cellHoverStyle={isSelected ? styles.sub_cell__selected : null} + style={isSelected ? styles.sub_cell__selected : styles.sub_cell} + testName="city" + ref={onRef}> + {this._relayStatusIndicator(relayCity.hasActiveRelays, isSelected)} + + <Cell.Label>{relayCity.name}</Cell.Label> + + {relayCity.relays.length > 1 ? ( + <Cell.Img + style={styles.collapse_button} + hoverStyle={styles.expand_chevron_hover} + onPress={handleCollapse} + source={isExpanded ? 'icon-chevron-up' : 'icon-chevron-down'} + height={24} + width={24} + /> + ) : null} + </Cell.CellButton> + + {relayCity.relays.length > 1 && ( + <Accordion height={isExpanded ? 'auto' : 0}> + {relayCity.relays.map((relay) => this._renderRelay(countryCode, relayCity.code, relay))} + </Accordion> + )} + </View> + ); + } + + _renderRelay(countryCode: string, cityCode: string, relay: RelayLocationRelayRedux) { + const relayLocation: RelayLocation = { hostname: [countryCode, cityCode, relay.hostname] }; + + const isSelected = this._isSelected(relayLocation); + + const onRef = isSelected + ? (element) => { + this._selectedCell = element; + } + : undefined; + + const handleSelect = !isSelected + ? () => { + this.props.onSelect(relayLocation); + } + : undefined; + return ( <Cell.CellButton - key={`${countryCode}_${relayCity.code}`} + key={`${countryCode}_${cityCode}_${relay.hostname}`} onPress={handleSelect} - disabled={!relayCity.hasActiveRelays} - cellHoverStyle={isSelected ? styles.sub_cell__selected : null} - style={isSelected ? styles.sub_cell__selected : styles.sub_cell} - testName="city" + cellHoverStyle={isSelected ? styles.sub_sub_cell__selected : null} + style={isSelected ? styles.sub_sub_cell__selected : styles.sub_sub_cell} + testName="relay" ref={onRef}> - {this._relayStatusIndicator(relayCity.hasActiveRelays, isSelected)} + {this._relayStatusIndicator(true, isSelected)} - <Cell.Label>{relayCity.name}</Cell.Label> + <Cell.Label>{relay.hostname}</Cell.Label> </Cell.CellButton> ); } diff --git a/gui/packages/desktop/src/renderer/components/SelectLocationStyles.js b/gui/packages/desktop/src/renderer/components/SelectLocationStyles.js index 49cbd441d5..d6d1abec65 100644 --- a/gui/packages/desktop/src/renderer/components/SelectLocationStyles.js +++ b/gui/packages/desktop/src/renderer/components/SelectLocationStyles.js @@ -60,6 +60,13 @@ export default { paddingLeft: 20, paddingRight: 0, }), + cell_selected: Styles.createViewStyle({ + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 20, + paddingRight: 0, + backgroundColor: colors.green, + }), sub_cell: Styles.createViewStyle({ paddingTop: 0, paddingBottom: 0, @@ -74,11 +81,18 @@ export default { paddingLeft: 40, backgroundColor: colors.green, }), - cell_selected: Styles.createViewStyle({ + sub_sub_cell: Styles.createViewStyle({ + paddingTop: 0, + paddingBottom: 0, + paddingRight: 0, + paddingLeft: 60, + backgroundColor: colors.blue20, + }), + sub_sub_cell__selected: Styles.createViewStyle({ paddingTop: 0, paddingBottom: 0, - paddingLeft: 20, paddingRight: 0, + paddingLeft: 60, backgroundColor: colors.green, }), expand_chevron_hover: Styles.createViewStyle({ diff --git a/gui/packages/desktop/src/renderer/containers/ConnectPage.js b/gui/packages/desktop/src/renderer/containers/ConnectPage.js index a1390fab6f..828d92959e 100644 --- a/gui/packages/desktop/src/renderer/containers/ConnectPage.js +++ b/gui/packages/desktop/src/renderer/containers/ConnectPage.js @@ -36,6 +36,15 @@ function getRelayName( return city.name; } } + } else if (location.hostname) { + const [countryCode, cityCode, hostname] = location.hostname; + const country = relayLocations.find(({ code }) => code === countryCode); + if (country) { + const city = country.cities.find(({ code }) => code === cityCode); + if (city) { + return `${city.name} (${hostname})`; + } + } } return 'Unknown'; diff --git a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js index e87f51930b..a712f9b73e 100644 --- a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js +++ b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js @@ -70,7 +70,10 @@ export type TunnelState = | BlockedState; export type RelayProtocol = 'tcp' | 'udp'; -export type RelayLocation = {| city: [string, string] |} | {| country: string |}; +export type RelayLocation = + | {| hostname: [string, string, string] |} + | {| city: [string, string] |} + | {| country: string |}; type OpenVpnConstraints = { port: 'any' | { only: number }, @@ -139,6 +142,9 @@ const RelaySettingsSchema = oneOf( location: constraint( oneOf( object({ + hostname: arrayOf(string), + }), + object({ city: arrayOf(string), }), object({ @@ -185,6 +191,15 @@ export type RelayListCity = { latitude: number, longitude: number, has_active_relays: boolean, + relays: Array<RelayListHostname>, +}; + +export type RelayListHostname = { + hostname: string, + ipv4_addr_in: string, + ipv4_addr_exit: string, + include_in_country: boolean, + weight: number, }; const RelayListSchema = object({ @@ -199,6 +214,15 @@ const RelayListSchema = object({ latitude: number, longitude: number, has_active_relays: boolean, + relays: arrayOf( + object({ + hostname: string, + ipv4_addr_in: string, + ipv4_addr_exit: string, + include_in_country: boolean, + weight: number, + }), + ), }), ), }), diff --git a/gui/packages/desktop/src/renderer/lib/relay-settings-builder.js b/gui/packages/desktop/src/renderer/lib/relay-settings-builder.js index 5f6c279e3f..5065a485f6 100644 --- a/gui/packages/desktop/src/renderer/lib/relay-settings-builder.js +++ b/gui/packages/desktop/src/renderer/lib/relay-settings-builder.js @@ -49,6 +49,10 @@ class NormalRelaySettingsBuilder { this._payload.location = { only: { city: [country, city] } }; return this; }, + hostname: (country: string, city: string, hostname: string) => { + this._payload.location = { only: { hostname: [country, city, hostname] } }; + return this; + }, any: () => { this._payload.location = 'any'; return this; @@ -58,6 +62,11 @@ class NormalRelaySettingsBuilder { return this.any(); } + if (location.hostname) { + const [country, city, hostname] = location.hostname; + return this.hostname(country, city, hostname); + } + if (location.city) { const [country, city] = location.city; return this.city(country, city); diff --git a/gui/packages/desktop/src/renderer/redux/settings/reducers.js b/gui/packages/desktop/src/renderer/redux/settings/reducers.js index 9870066edc..096dede5c2 100644 --- a/gui/packages/desktop/src/renderer/redux/settings/reducers.js +++ b/gui/packages/desktop/src/renderer/redux/settings/reducers.js @@ -19,12 +19,21 @@ export type RelaySettingsRedux = }, |}; +export type RelayLocationRelayRedux = { + hostname: string, + ipv4AddrIn: string, + ipv4AddrExit: string, + includeInCountry: boolean, + weight: number, +}; + export type RelayLocationCityRedux = { name: string, code: string, latitude: number, longitude: number, hasActiveRelays: boolean, + relays: Array<RelayLocationRelayRedux>, }; export type RelayLocationRedux = { diff --git a/gui/packages/desktop/test/components/SelectLocation.spec.js b/gui/packages/desktop/test/components/SelectLocation.spec.js index cd45ff2e76..3d35da0edd 100644 --- a/gui/packages/desktop/test/components/SelectLocation.spec.js +++ b/gui/packages/desktop/test/components/SelectLocation.spec.js @@ -29,6 +29,15 @@ describe('components/SelectLocation', () => { latitude: 0, longitude: 0, hasActiveRelays: true, + relays: [ + { + hostname: 'fake1.mullvad.net', + ipv4AddrIn: '192.168.0.100', + ipv4AddrExit: '192.168.1.100', + includeInCountry: true, + weight: 1, + }, + ], }, { name: 'Stockholm', @@ -36,6 +45,15 @@ describe('components/SelectLocation', () => { latitude: 0, longitude: 0, hasActiveRelays: true, + relays: [ + { + hostname: 'fake2.mullvad.net', + ipv4AddrIn: '192.168.0.101', + ipv4AddrExit: '192.168.1.101', + includeInCountry: true, + weight: 1, + }, + ], }, ], }, diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index 4de7571307..57cc934b03 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -24,7 +24,9 @@ impl Command for Relay { .setting(clap::AppSettings::SubcommandRequired) .subcommand( clap::SubCommand::with_name("set") - .setting(clap::AppSettings::SubcommandRequired) + .about( + "Set relay server selection parameters. Such as location and port/protocol", + ).setting(clap::AppSettings::SubcommandRequired) .subcommand( clap::SubCommand::with_name("custom") .about("Set a custom VPN relay") @@ -67,6 +69,10 @@ impl Command for Relay { .help("The three letter city code") .index(2) .validator(city_code_validator), + ).arg( + clap::Arg::with_name("hostname") + .help("The relay hostname") + .index(3), ), ).subcommand( clap::SubCommand::with_name("tunnel") @@ -137,18 +143,32 @@ impl Relay { fn set_location(&self, matches: &clap::ArgMatches) -> Result<()> { let country = matches.value_of("country").unwrap(); let city = matches.value_of("city"); + let hostname = matches.value_of("hostname"); - let location_constraint = match (country, city) { - ("any", None) => Constraint::Any, - ("any", _) => clap::Error::with_description( + let location_constraint = match (country, city, hostname) { + ("any", None, None) => Constraint::Any, + ("any", ..) => clap::Error::with_description( "City can't be given when selecting 'any' country", clap::ErrorKind::InvalidValue, ).exit(), - (country, None) => Constraint::Only(LocationConstraint::Country(country.to_owned())), - (country, Some(city)) => Constraint::Only(LocationConstraint::City( + (country, None, None) => { + Constraint::Only(LocationConstraint::Country(country.to_owned())) + } + (country, Some(city), None) => Constraint::Only(LocationConstraint::City( country.to_owned(), city.to_owned(), )), + (country, Some(city), Some(hostname)) => { + Constraint::Only(LocationConstraint::Hostname( + country.to_owned(), + city.to_owned(), + hostname.to_owned(), + )) + } + (..) => clap::Error::with_description( + "Invalid country, city and hostname combination given", + clap::ErrorKind::InvalidValue, + ).exit(), }; self.update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { diff --git a/mullvad-daemon/src/relays.rs b/mullvad-daemon/src/relays.rs index 86d4efbc51..fea9bd8340 100644 --- a/mullvad-daemon/src/relays.rs +++ b/mullvad-daemon/src/relays.rs @@ -70,8 +70,9 @@ impl ParsedRelays { let city_code = city.code.clone(); let latitude = city.latitude; let longitude = city.longitude; - relays.extend(city.relays.drain(..).map(|mut relay| { - relay.location = Some(Location { + for relay in &mut city.relays { + let mut relay_with_location = relay.clone(); + relay_with_location.location = Some(Location { country: country_name.clone(), country_code: country_code.clone(), city: city_name.clone(), @@ -79,8 +80,9 @@ impl ParsedRelays { latitude, longitude, }); - relay - })); + relays.push(relay_with_location); + relay.tunnels.clear(); + } } } ParsedRelays { @@ -259,6 +261,14 @@ impl RelaySelector { loc.country_code == *country && loc.city_code == *city }) } + Constraint::Only(LocationConstraint::Hostname(ref country, ref city, ref hostname)) => { + relay + .location + .as_ref() + .map_or(relay.hostname == *hostname, |loc| { + loc.country_code == *country && loc.city_code == *city + }) + } }; if !matches_location { return None; diff --git a/mullvad-types/src/location.rs b/mullvad-types/src/location.rs index 6c4ed94b00..5d14318da6 100644 --- a/mullvad-types/src/location.rs +++ b/mullvad-types/src/location.rs @@ -2,6 +2,7 @@ use std::net::IpAddr; pub type CountryCode = String; pub type CityCode = String; +pub type Hostname = String; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Location { diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index f2369ce7fa..f0a56a9d76 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -1,4 +1,4 @@ -use location::{CityCode, CountryCode}; +use location::{CityCode, CountryCode, Hostname}; use CustomTunnelEndpoint; use std::fmt; @@ -96,6 +96,8 @@ pub enum LocationConstraint { Country(CountryCode), /// A city is composed of a country code and a city code. City(CountryCode, CityCode), + /// An single hostname in a given city. + Hostname(CountryCode, CityCode, Hostname), } diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs index 17d6e7d5f2..402b4f220b 100644 --- a/mullvad-types/src/relay_list.rs +++ b/mullvad-types/src/relay_list.rs @@ -44,6 +44,7 @@ pub struct Relay { pub ipv4_addr_exit: Ipv4Addr, pub include_in_country: bool, pub weight: u64, + #[serde(skip_serializing_if = "RelayTunnels::is_empty", default)] pub tunnels: RelayTunnels, #[serde(skip)] pub location: Option<Location>, @@ -55,3 +56,14 @@ pub struct RelayTunnels { pub openvpn: Vec<OpenVpnEndpointData>, pub wireguard: Vec<WireguardEndpointData>, } + +impl RelayTunnels { + pub fn is_empty(&self) -> bool { + self.openvpn.is_empty() && self.wireguard.is_empty() + } + + pub fn clear(&mut self) { + self.openvpn.clear(); + self.wireguard.clear(); + } +} |
