diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-12-06 10:42:20 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-01-03 13:48:03 +0100 |
| commit | a75abb22ed42c8c811aa4967ba82be5077f52ea2 (patch) | |
| tree | eab547f9b27566fa34fab981de6e0eb398c8fb46 /gui | |
| parent | 9b6f208a2135344cb7b6e28ac28b76d31b54130c (diff) | |
| download | mullvadvpn-a75abb22ed42c8c811aa4967ba82be5077f52ea2.tar.xz mullvadvpn-a75abb22ed42c8c811aa4967ba82be5077f52ea2.zip | |
Add entry location selection in SelectLocation
Diffstat (limited to 'gui')
| -rw-r--r-- | gui/src/renderer/components/SelectLocation.tsx | 188 | ||||
| -rw-r--r-- | gui/src/renderer/containers/SelectLocationPage.tsx | 56 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/actions.ts | 15 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/reducers.ts | 10 |
4 files changed, 168 insertions, 101 deletions
diff --git a/gui/src/renderer/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx index 528e8129ef..902ca27c80 100644 --- a/gui/src/renderer/components/SelectLocation.tsx +++ b/gui/src/renderer/components/SelectLocation.tsx @@ -1,13 +1,12 @@ import React from 'react'; import { sprintf } from 'sprintf-js'; import { colors } from '../../config.json'; -import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types'; +import { LiftedConstraint, RelayLocation, TunnelProtocol } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { IRelayLocationRedux } from '../redux/settings/reducers'; -import { LocationScope } from '../redux/userinterface/reducers'; import BridgeLocations, { SpecialBridgeLocationType } from './BridgeLocations'; import { CustomScrollbarsRef } from './CustomScrollbars'; -import ExitLocations from './ExitLocations'; +import { EntryLocations, ExitLocations } from './Locations'; import ImageView from './ImageView'; import { Layout } from './Layout'; import LocationList, { LocationSelection, LocationSelectionType } from './LocationList'; @@ -37,25 +36,32 @@ import { import { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; interface IProps { - locationScope: LocationScope; selectedExitLocation?: RelayLocation; + selectedEntryLocation?: RelayLocation; selectedBridgeLocation?: LiftedConstraint<RelayLocation>; relayLocations: IRelayLocationRedux[]; bridgeLocations: IRelayLocationRedux[]; - allowBridgeSelection: boolean; + allowEntrySelection: boolean; + tunnelProtocol: LiftedConstraint<TunnelProtocol>; providers: string[]; onClose: () => void; onViewFilterByProvider: () => void; - onChangeLocationScope: (location: LocationScope) => void; onSelectExitLocation: (location: RelayLocation) => void; + onSelectEntryLocation: (location: RelayLocation) => void; onSelectBridgeLocation: (location: RelayLocation) => void; onSelectClosestToExit: () => void; onClearProviders: () => void; } +enum LocationScope { + entry = 0, + exit, +} + interface IState { showFilterMenu: boolean; headingHeight: number; + locationScope: LocationScope; } interface ISelectLocationSnapshot { @@ -64,17 +70,19 @@ interface ISelectLocationSnapshot { } export default class SelectLocation extends React.Component<IProps, IState> { - public state = { showFilterMenu: false, headingHeight: 0 }; + public state = { showFilterMenu: false, headingHeight: 0, locationScope: LocationScope.exit }; private scrollView = React.createRef<CustomScrollbarsRef>(); private spacePreAllocationViewRef = React.createRef<SpacePreAllocationView>(); private selectedExitLocationRef = React.createRef<React.ReactInstance>(); + private selectedEntryLocationRef = React.createRef<React.ReactInstance>(); private selectedBridgeLocationRef = React.createRef<React.ReactInstance>(); private exitLocationList = React.createRef<LocationList<never>>(); + private entryLocationList = React.createRef<LocationList<never>>(); private bridgeLocationList = React.createRef<LocationList<SpecialBridgeLocationType>>(); - private snapshotByScope: { [index: number]: ISelectLocationSnapshot } = {}; + private snapshotByScope: Partial<Record<LocationScope, ISelectLocationSnapshot>> = {}; private filterButtonRef = React.createRef<HTMLDivElement>(); private headerRef = React.createRef<HTMLHeadingElement>(); @@ -87,25 +95,25 @@ export default class SelectLocation extends React.Component<IProps, IState> { } public componentDidUpdate( - prevProps: IProps, - _prevState: unknown, + _prevProps: IProps, + prevState: IState, snapshot?: ISelectLocationSnapshot, ) { - if (this.props.locationScope !== prevProps.locationScope) { - this.restoreScrollPosition(this.props.locationScope); + if (this.state.locationScope !== prevState.locationScope) { + this.restoreScrollPosition(this.state.locationScope); if (snapshot) { - this.snapshotByScope[prevProps.locationScope] = snapshot; + this.snapshotByScope[prevState.locationScope] = snapshot; } } } - public getSnapshotBeforeUpdate(prevProps: IProps): ISelectLocationSnapshot | undefined { + public getSnapshotBeforeUpdate( + prevProps: IProps, + prevState: IState, + ): ISelectLocationSnapshot | undefined { const scrollView = this.scrollView.current; - const locationList = - prevProps.locationScope === LocationScope.relay - ? this.exitLocationList.current - : this.bridgeLocationList.current; + const locationList = this.getLocationListRef(prevProps, prevState); if (scrollView && locationList) { return { @@ -164,13 +172,7 @@ export default class SelectLocation extends React.Component<IProps, IState> { messages.pgettext('select-location-view', 'Select location') } </HeaderTitle> - <HeaderSubTitle> - {this.props.allowBridgeSelection && - messages.pgettext( - 'select-location-view', - 'While connected, your traffic will be routed through two secure locations, the entry point (a bridge server) and the exit point (a VPN server).', - )} - </HeaderSubTitle> + {this.renderHeaderSubtitle()} </StyledSettingsHeader> {this.props.providers.length > 0 && ( @@ -200,10 +202,10 @@ export default class SelectLocation extends React.Component<IProps, IState> { </StyledProvidersCount> </StyledProviderCountRow> )} - {this.props.allowBridgeSelection && ( + {this.props.allowEntrySelection && ( <StyledScopeBar - defaultSelectedIndex={this.props.locationScope} - onChange={this.props.onChangeLocationScope}> + defaultSelectedIndex={this.state.locationScope} + onChange={this.onChangeLocationScope}> <ScopeBarItem> {messages.pgettext('select-location-nav', 'Entry')} </ScopeBarItem> @@ -214,31 +216,7 @@ export default class SelectLocation extends React.Component<IProps, IState> { )} </StyledNavigationBarAttachment> - <StyledContent> - {this.props.locationScope === LocationScope.relay ? ( - <ExitLocations - ref={this.exitLocationList} - source={this.props.relayLocations} - defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} - selectedValue={this.props.selectedExitLocation} - selectedElementRef={this.selectedExitLocationRef} - onSelect={this.onSelectExitLocation} - onWillExpand={this.onWillExpand} - onTransitionEnd={this.resetHeight} - /> - ) : ( - <BridgeLocations - ref={this.bridgeLocationList} - source={this.props.bridgeLocations} - defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} - selectedValue={this.props.selectedBridgeLocation} - selectedElementRef={this.selectedBridgeLocationRef} - onSelect={this.onSelectBridgeLocation} - onWillExpand={this.onWillExpand} - onTransitionEnd={this.resetHeight} - /> - )} - </StyledContent> + <StyledContent>{this.renderLocationList()}</StyledContent> </SpacePreAllocationView> </NavigationScrollbars> </NavigationContainer> @@ -257,12 +235,101 @@ export default class SelectLocation extends React.Component<IProps, IState> { } } + private getLocationListRef(prevProps: IProps, prevState: IState) { + if (prevState.locationScope === LocationScope.exit) { + return this.exitLocationList.current; + } else if (prevProps.tunnelProtocol === 'wireguard') { + return this.entryLocationList.current; + } else { + return this.bridgeLocationList.current; + } + } + + private getSelectedLocationRef() { + if (this.state.locationScope === LocationScope.exit) { + return this.selectedExitLocationRef.current; + } else if (this.props.tunnelProtocol === 'wireguard') { + return this.selectedEntryLocationRef.current; + } else { + return this.selectedBridgeLocationRef.current; + } + } + + private renderHeaderSubtitle() { + if (this.props.allowEntrySelection) { + if (this.props.tunnelProtocol === 'openvpn') { + return ( + <HeaderSubTitle> + {messages.pgettext( + 'select-location-view', + 'While connected, your traffic will be routed through two secure locations, the entry point (a bridge server) and the exit point (a VPN server).', + )} + </HeaderSubTitle> + ); + } else { + return ( + <HeaderSubTitle> + {messages.pgettext( + 'select-location-view', + 'While connected, your traffic will be routed through two secure locations, the entry point and the exit point (needs to be two different VPN servers).', + )} + </HeaderSubTitle> + ); + } + } else { + return null; + } + } + + private renderLocationList() { + if (this.state.locationScope === LocationScope.exit) { + return ( + <ExitLocations + ref={this.exitLocationList} + source={this.props.relayLocations} + defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} + selectedValue={this.props.selectedExitLocation} + selectedElementRef={this.selectedExitLocationRef} + onSelect={this.onSelectExitLocation} + onWillExpand={this.onWillExpand} + onTransitionEnd={this.resetHeight} + /> + ); + } else if (this.props.tunnelProtocol === 'any' || this.props.tunnelProtocol === 'wireguard') { + return ( + <EntryLocations + ref={this.entryLocationList} + source={this.props.relayLocations} + defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} + selectedValue={this.props.selectedEntryLocation} + selectedElementRef={this.selectedEntryLocationRef} + onSelect={this.onSelectEntryLocation} + onWillExpand={this.onWillExpand} + onTransitionEnd={this.resetHeight} + /> + ); + } else { + return ( + <BridgeLocations + ref={this.bridgeLocationList} + source={this.props.bridgeLocations} + defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} + selectedValue={this.props.selectedBridgeLocation} + selectedElementRef={this.selectedBridgeLocationRef} + onSelect={this.onSelectBridgeLocation} + onWillExpand={this.onWillExpand} + onTransitionEnd={this.resetHeight} + /> + ); + } + } + private resetHeight = () => { this.spacePreAllocationViewRef.current?.reset(); }; private getExpandedLocationsFromSnapshot(): RelayLocation[] | undefined { - const snapshot = this.snapshotByScope[this.props.locationScope]; + const snapshot = this.snapshotByScope[this.state.locationScope]; if (snapshot) { return snapshot.expandedLocations; } else { @@ -278,10 +345,7 @@ export default class SelectLocation extends React.Component<IProps, IState> { } private scrollToSelectedCell() { - const ref = - this.props.locationScope === LocationScope.relay - ? this.selectedExitLocationRef.current - : this.selectedBridgeLocationRef.current; + const ref = this.getSelectedLocationRef(); const scrollView = this.scrollView.current; if (scrollView) { @@ -295,12 +359,20 @@ export default class SelectLocation extends React.Component<IProps, IState> { } } + private onChangeLocationScope = (locationScope: LocationScope) => { + this.setState({ locationScope }); + }; + private onSelectExitLocation = (location: LocationSelection<never>) => { if (location.type === LocationSelectionType.relay) { this.props.onSelectExitLocation(location.value); } }; + private onSelectEntryLocation = (location: LocationSelection<never>) => { + this.props.onSelectEntryLocation(location.value); + }; + private onSelectBridgeLocation = (location: LocationSelection<SpecialBridgeLocationType>) => { if (location.type === LocationSelectionType.relay) { this.props.onSelectBridgeLocation(location.value); diff --git a/gui/src/renderer/containers/SelectLocationPage.tsx b/gui/src/renderer/containers/SelectLocationPage.tsx index cd455a01f6..a4f78f87a5 100644 --- a/gui/src/renderer/containers/SelectLocationPage.tsx +++ b/gui/src/renderer/containers/SelectLocationPage.tsx @@ -1,21 +1,21 @@ import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import BridgeSettingsBuilder from '../../shared/bridge-settings-builder'; import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types'; import log from '../../shared/logging'; import RelaySettingsBuilder from '../../shared/relay-settings-builder'; import SelectLocation from '../components/SelectLocation'; import withAppContext, { IAppContext } from '../context'; +import { createWireguardRelayUpdater } from '../lib/constraint-updater'; import { IHistoryProps, withHistory } from '../lib/history'; import { RoutePath } from '../lib/routes'; import { IRelayLocationRedux } from '../redux/settings/reducers'; import { IReduxState, ReduxDispatch } from '../redux/store'; -import userInterfaceActions from '../redux/userinterface/actions'; -import { LocationScope } from '../redux/userinterface/reducers'; -const mapStateToProps = (state: IReduxState) => { +const mapStateToProps = (state: IReduxState, props: IHistoryProps & IAppContext) => { let selectedExitLocation: RelayLocation | undefined; + let selectedEntryLocation: RelayLocation | undefined; let selectedBridgeLocation: LiftedConstraint<RelayLocation> | undefined; + let multihopEnabled = false; if ('normal' in state.settings.relaySettings) { const exitLocation = state.settings.relaySettings.normal.location; @@ -24,37 +24,57 @@ const mapStateToProps = (state: IReduxState) => { } } - if ('normal' in state.settings.bridgeSettings) { + const relaySettings = state.settings.relaySettings; + const tunnelProtocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : 'any'; + + if (tunnelProtocol === 'openvpn' && 'normal' in state.settings.bridgeSettings) { selectedBridgeLocation = state.settings.bridgeSettings.normal.location; + } else if ('normal' in relaySettings) { + const entryLocation = relaySettings.normal.wireguard.entryLocation; + if (entryLocation !== 'any') { + selectedEntryLocation = entryLocation; + } + + multihopEnabled = relaySettings.normal.wireguard.useMultihop; } - const allowBridgeSelection = state.settings.bridgeState === 'on'; - const locationScope = allowBridgeSelection - ? state.userInterface.locationScope - : LocationScope.relay; + const allowEntrySelection = + (tunnelProtocol === 'openvpn' && state.settings.bridgeState === 'on') || + ((tunnelProtocol === 'any' || tunnelProtocol === 'wireguard') && multihopEnabled); - const relaySettings = state.settings.relaySettings; const providers = 'normal' in relaySettings ? relaySettings.normal.providers : []; return { selectedExitLocation, + selectedEntryLocation, selectedBridgeLocation, relayLocations: filterLocationsByProvider(state.settings.relayLocations, providers), bridgeLocations: filterLocationsByProvider(state.settings.bridgeLocations, providers), - locationScope, - allowBridgeSelection, + allowEntrySelection, + tunnelProtocol, providers, + + onSelectEntryLocation: async (entryLocation: RelayLocation) => { + // dismiss the view first + props.history.dismiss(); + + const relayUpdate = createWireguardRelayUpdater(state.settings.relaySettings) + .tunnel.wireguard((wireguard) => wireguard.entryLocation.exact(entryLocation)) + .build(); + + try { + await props.app.updateRelaySettings(relayUpdate); + } catch (e) { + const error = e as Error; + log.error('Failed to select the entry location', error.message); + } + }, }; }; -const mapDispatchToProps = (dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { - const userInterface = bindActionCreators(userInterfaceActions, dispatch); - +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { onClose: () => props.history.dismiss(), onViewFilterByProvider: () => props.history.push(RoutePath.filterByProvider), - onChangeLocationScope: (scope: LocationScope) => { - userInterface.setLocationScope(scope); - }, onSelectExitLocation: async (relayLocation: RelayLocation) => { // dismiss the view first props.history.dismiss(); diff --git a/gui/src/renderer/redux/userinterface/actions.ts b/gui/src/renderer/redux/userinterface/actions.ts index 88ffbf216f..e6010d42f2 100644 --- a/gui/src/renderer/redux/userinterface/actions.ts +++ b/gui/src/renderer/redux/userinterface/actions.ts @@ -1,5 +1,4 @@ import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema'; -import { LocationScope } from './reducers'; export interface IUpdateLocaleAction { type: 'UPDATE_LOCALE'; @@ -15,11 +14,6 @@ export interface IUpdateConnectionInfoOpenAction { type: 'TOGGLE_CONNECTION_PANEL'; } -export interface ISetLocationScopeAction { - type: 'SET_LOCATION_SCOPE'; - scope: LocationScope; -} - export interface ISetWindowFocusedAction { type: 'SET_WINDOW_FOCUSED'; focused: boolean; @@ -50,7 +44,6 @@ export type UserInterfaceAction = | IUpdateLocaleAction | IUpdateWindowArrowPositionAction | IUpdateConnectionInfoOpenAction - | ISetLocationScopeAction | ISetWindowFocusedAction | IAddScrollPosition | IRemoveScrollPosition @@ -77,13 +70,6 @@ function toggleConnectionPanel(): IUpdateConnectionInfoOpenAction { }; } -function setLocationScope(scope: LocationScope): ISetLocationScopeAction { - return { - type: 'SET_LOCATION_SCOPE', - scope, - }; -} - function setWindowFocused(focused: boolean): ISetWindowFocusedAction { return { type: 'SET_WINDOW_FOCUSED', @@ -126,7 +112,6 @@ export default { updateLocale, updateWindowArrowPosition, toggleConnectionPanel, - setLocationScope, setWindowFocused, addScrollPosition, removeScrollPosition, diff --git a/gui/src/renderer/redux/userinterface/reducers.ts b/gui/src/renderer/redux/userinterface/reducers.ts index 5adac07937..9247dc8a5f 100644 --- a/gui/src/renderer/redux/userinterface/reducers.ts +++ b/gui/src/renderer/redux/userinterface/reducers.ts @@ -1,16 +1,10 @@ import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema'; import { ReduxAction } from '../store'; -export enum LocationScope { - bridge = 0, - relay, -} - export interface IUserInterfaceReduxState { locale: string; arrowPosition?: number; connectionPanelVisible: boolean; - locationScope: LocationScope; windowFocused: boolean; scrollPosition: Record<string, [number, number]>; macOsScrollbarVisibility?: MacOsScrollbarVisibility; @@ -20,7 +14,6 @@ export interface IUserInterfaceReduxState { const initialState: IUserInterfaceReduxState = { locale: 'en', connectionPanelVisible: false, - locationScope: LocationScope.relay, windowFocused: false, scrollPosition: {}, macOsScrollbarVisibility: undefined, @@ -41,9 +34,6 @@ export default function ( case 'TOGGLE_CONNECTION_PANEL': return { ...state, connectionPanelVisible: !state.connectionPanelVisible }; - case 'SET_LOCATION_SCOPE': - return { ...state, locationScope: action.scope }; - case 'SET_WINDOW_FOCUSED': return { ...state, windowFocused: action.focused }; |
