diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2018-10-16 11:31:17 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2018-10-16 11:31:17 -0300 |
| commit | df0022a3977fb6ee7557773a03aa1c85e2c4d63f (patch) | |
| tree | 62683bb3df6af7b6d9b680306eb7471c85ef8484 /gui | |
| parent | 43dfaeddf5e70c8cb667b5385eba1645553a5c3e (diff) | |
| parent | 12abe68cb03edb0342f9ed7d412aa6e299c632cc (diff) | |
| download | mullvadvpn-df0022a3977fb6ee7557773a03aa1c85e2c4d63f.tar.xz mullvadvpn-df0022a3977fb6ee7557773a03aa1c85e2c4d63f.zip | |
Merge branch 'in-out-ip-in-gui'
Diffstat (limited to 'gui')
| -rw-r--r-- | gui/packages/components/src/ConnectionInfo.tsx | 105 | ||||
| -rw-r--r-- | gui/packages/components/src/index.ts | 1 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/app.js | 14 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/Connect.js | 307 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/lib/daemon-rpc.js | 46 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/redux/connection/actions.js | 10 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/redux/connection/reducers.js | 4 |
7 files changed, 337 insertions, 150 deletions
diff --git a/gui/packages/components/src/ConnectionInfo.tsx b/gui/packages/components/src/ConnectionInfo.tsx new file mode 100644 index 0000000000..b22c510c23 --- /dev/null +++ b/gui/packages/components/src/ConnectionInfo.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { Component, Styles, Text, View } from 'reactxp'; +import { default as Accordion } from './Accordion'; + +const styles = { + toggle: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 14, + fontWeight: '800', + color: 'rgb(255, 255, 255, 0.4)', + paddingBottom: 2, + }), + content: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 16, + fontWeight: '800', + color: 'rgb(255, 255, 255)', + paddingBottom: 2, + }), +}; + +interface IInAddress { + ip: string; + port: number; + protocol: string; +} + +interface IOutAddress { + ipv4: string | null; + ipv6: string | null; +} + +interface IProps { + inAddress: IInAddress; + outAddress: IOutAddress; + startExpanded: boolean; + onToggle: (expanded: boolean) => void; +} + +interface IState { + expanded: boolean; +} + +export default class ConnectionInfo extends Component<IProps, IState> { + public static defaultProps = { + inAddress: { + ip: null, + port: null, + protocol: null, + }, + outAddress: null, + startExpanded: false, + onToggle: (_: boolean) => {}, + }; + + constructor(props: IProps) { + super(props); + + this.state = { + expanded: props.startExpanded, + }; + } + + public render() { + return ( + <View> + <Accordion height={this.state.expanded ? 'auto' : 0}> + <Text style={styles.content}>{this.inAddress()}</Text> + <Text style={styles.content}>{this.outAddress()}</Text> + </Accordion> + <Text style={styles.toggle} onPress={() => this.toggle()}> + {this.state.expanded ? 'LESS' : 'MORE'} + </Text> + </View> + ); + } + + private toggle() { + this.setState((state, props) => { + const expanded = !state.expanded; + + props.onToggle(expanded); + + return { expanded }; + }); + } + + private inAddress() { + const { ip, port, protocol } = this.props.inAddress; + + return ( + 'IN: ' + (ip || '<unknown>') + (port ? `:${port}` : '') + (protocol ? ` - ${protocol}` : '') + ); + } + + private outAddress() { + const { ipv4, ipv6 } = this.props.outAddress; + + if (ipv4 || ipv6) { + return `OUT: ${ipv4}` + (ipv6 ? ` / ${ipv6}` : ''); + } else { + return ''; + } + } +} diff --git a/gui/packages/components/src/index.ts b/gui/packages/components/src/index.ts index 7d5ad156f0..648ddabdf9 100644 --- a/gui/packages/components/src/index.ts +++ b/gui/packages/components/src/index.ts @@ -1,3 +1,4 @@ export { default as Accordion } from './Accordion'; export { default as ClipboardLabel } from './ClipboardLabel'; +export { default as ConnectionInfo } from './ConnectionInfo'; export { default as SecuredLabel, SecuredDisplayStyle } from './SecuredLabel'; diff --git a/gui/packages/desktop/src/renderer/app.js b/gui/packages/desktop/src/renderer/app.js index 1ef54ca4e9..2ae7b66f9c 100644 --- a/gui/packages/desktop/src/renderer/app.js +++ b/gui/packages/desktop/src/renderer/app.js @@ -229,7 +229,7 @@ export default class AppRenderer { if (tunnelState.state === 'disconnected' || tunnelState.state === 'blocked') { // switch to connecting state immediately to prevent a lag that may be caused by RPC // communication delay - actions.connection.connecting(); + actions.connection.connecting(null); await this._daemonRpc.connectTunnel(); } @@ -478,12 +478,6 @@ export default class AppRenderer { await this._setStartView(); try { - await this._fetchLocation(); - } catch (error) { - log.error(`Cannot fetch the location: ${error.message}`); - } - - try { await this._fetchLatestVersionInfo(); } catch (error) { log.error(`Cannot fetch the latest version information: ${error.message}`); @@ -602,7 +596,7 @@ export default class AppRenderer { } async _updateUserLocation(tunnelState: TunnelState) { - if (tunnelState === 'connecting' || tunnelState === 'disconnected') { + if (['connected', 'connecting', 'disconnected'].includes(tunnelState)) { try { await this._fetchLocation(); } catch (error) { @@ -616,11 +610,11 @@ export default class AppRenderer { switch (stateTransition.state) { case 'connecting': - actions.connection.connecting(); + actions.connection.connecting(stateTransition.details); break; case 'connected': - actions.connection.connected(); + actions.connection.connected(stateTransition.details); break; case 'disconnecting': diff --git a/gui/packages/desktop/src/renderer/components/Connect.js b/gui/packages/desktop/src/renderer/components/Connect.js index a50227b289..3602410c86 100644 --- a/gui/packages/desktop/src/renderer/components/Connect.js +++ b/gui/packages/desktop/src/renderer/components/Connect.js @@ -3,7 +3,7 @@ import moment from 'moment'; import * as React from 'react'; import { Component, Text, View, Types } from 'reactxp'; -import { SecuredLabel, SecuredDisplayStyle } from '@mullvad/components'; +import { ConnectionInfo, SecuredLabel, SecuredDisplayStyle } from '@mullvad/components'; import { Layout, Container, Header } from './Layout'; import { SettingsBarButton, Brand } from './HeaderBar'; import NotificationArea from './NotificationArea'; @@ -136,6 +136,19 @@ export default class Connect extends Component<Props> { } renderMap() { + const tunnelState = this.props.connection.status.state; + const details = this.props.connection.status.details; + + let relayIp = null; + let relayPort = null; + let relayProtocol = null; + + if ((tunnelState === 'connecting' || tunnelState === 'connected') && details) { + relayIp = details.address; + relayPort = details.tunnel.openvpn.port; + relayProtocol = details.tunnel.openvpn.protocol; + } + return ( <View style={styles.connect}> <View style={styles.map}> @@ -143,19 +156,23 @@ export default class Connect extends Component<Props> { </View> <View style={styles.container}> {/* show spinner when connecting */} - {this.props.connection.status.state === 'connecting' ? ( + {tunnelState === 'connecting' ? ( <View style={styles.status_icon}> <Img source="icon-spinner" height={60} width={60} alt="" /> </View> ) : null} <TunnelControl - style={styles.tunnel_control} tunnelState={this.props.connection.status.state} selectedRelayName={this.props.selectedRelayName} city={this.props.connection.city} country={this.props.connection.country} hostname={this.props.connection.hostname} + relayIp={relayIp} + relayPort={relayPort} + relayProtocol={relayProtocol} + outIpv4={this.props.connection.ip} + outIpv6={null} onConnect={this.props.onConnect} onDisconnect={this.props.onDisconnect} onSelectLocation={this.props.onSelectLocation} @@ -220,144 +237,190 @@ type TunnelControlProps = { city: ?string, country: ?string, hostname: ?string, + relayIp: ?string, + relayPort: ?number, + relayProtocol: ?string, + outIpv4: ?string, + outIpv6: ?string, onConnect: () => void, onDisconnect: () => void, onSelectLocation: () => void, - style: Types.ViewStyleRuleSet, }; -export function TunnelControl(props: TunnelControlProps) { - const Location = ({ children }) => <View style={styles.status_location}>{children}</View>; - const City = () => <Text style={styles.status_location_text}>{props.city}</Text>; - const Country = () => <Text style={styles.status_location_text}>{props.country}</Text>; - const Hostname = () => <Text style={styles.status_hostname}>{props.hostname || ''}</Text>; +type TunnelControlState = { + showConnectionInfo: boolean, +}; - const SwitchLocation = () => { - return ( +class TunnelControl extends Component<TunnelControlProps, TunnelControlState> { + state = { + showConnectionInfo: false, + }; + + render() { + const Location = ({ children }) => <View style={styles.status_location}>{children}</View>; + const City = () => <Text style={styles.status_location_text}>{this.props.city}</Text>; + const Country = () => <Text style={styles.status_location_text}>{this.props.country}</Text>; + const Hostname = () => <Text style={styles.status_hostname}>{this.props.hostname || ''}</Text>; + + const SwitchLocation = () => { + return ( + <AppButton.TransparentButton + style={styles.switch_location_button} + onPress={this.props.onSelectLocation}> + <AppButton.Label>{'Switch location'}</AppButton.Label> + </AppButton.TransparentButton> + ); + }; + + const SelectedLocation = () => ( <AppButton.TransparentButton style={styles.switch_location_button} - onPress={props.onSelectLocation}> - <AppButton.Label>{'Switch location'}</AppButton.Label> + onPress={this.props.onSelectLocation}> + <AppButton.Label>{this.props.selectedRelayName}</AppButton.Label> + <Img height={12} width={7} source="icon-chevron" /> </AppButton.TransparentButton> ); - }; - const SelectedLocation = () => ( - <AppButton.TransparentButton - style={styles.switch_location_button} - onPress={props.onSelectLocation}> - <AppButton.Label>{props.selectedRelayName}</AppButton.Label> - <Img height={12} width={7} source="icon-chevron" /> - </AppButton.TransparentButton> - ); + const Connect = () => ( + <AppButton.GreenButton onPress={this.props.onConnect}> + {'Secure my connection'} + </AppButton.GreenButton> + ); - const Connect = () => ( - <AppButton.GreenButton onPress={props.onConnect}> - {'Secure my connection'} - </AppButton.GreenButton> - ); + const Disconnect = () => ( + <AppButton.RedTransparentButton onPress={this.props.onDisconnect}> + {'Disconnect'} + </AppButton.RedTransparentButton> + ); - const Disconnect = () => ( - <AppButton.RedTransparentButton onPress={props.onDisconnect}> - {'Disconnect'} - </AppButton.RedTransparentButton> - ); + const Cancel = () => ( + <AppButton.RedTransparentButton onPress={this.props.onDisconnect}> + {'Cancel'} + </AppButton.RedTransparentButton> + ); - const Cancel = () => ( - <AppButton.RedTransparentButton onPress={props.onDisconnect}> - {'Cancel'} - </AppButton.RedTransparentButton> - ); + const Secured = ({ displayStyle }) => ( + <SecuredLabel style={styles.status_security} displayStyle={displayStyle} /> + ); + const Footer = ({ children }) => <View style={styles.footer}>{children}</View>; - const Secured = ({ displayStyle }) => ( - <SecuredLabel style={styles.status_security} displayStyle={displayStyle} /> - ); - const Wrapper = ({ children }) => <View style={props.style}>{children}</View>; - const Body = ({ children }) => <View style={styles.body}>{children}</View>; - const Footer = ({ children }) => <View style={styles.footer}>{children}</View>; + const connectionInfoProps = { + inAddress: { + ip: this.props.relayIp, + port: this.props.relayPort, + protocol: this.props.relayProtocol, + }, + outAddress: { + ipv4: this.props.outIpv4, + ipv6: this.props.outIpv6, + }, + startExpanded: this.state.showConnectionInfo, + onToggle: (expanded) => { + this.setState({ showConnectionInfo: expanded }); + }, + }; - switch (props.tunnelState) { - case 'connecting': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.securing} /> - <Location> - <City /> - <Country /> - </Location> - <Hostname /> - </Body> - <Footer> - <SwitchLocation /> - <Cancel /> - </Footer> - </Wrapper> - ); - case 'connected': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.secured} /> - <Location> - <City /> - <Country /> - </Location> - <Hostname /> - </Body> - <Footer> - <SwitchLocation /> - <Disconnect /> - </Footer> - </Wrapper> - ); + switch (this.props.tunnelState) { + case 'connecting': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.securing} /> + <Location> + <City /> + <Country /> + </Location> + <Hostname /> + <ConnectionInfo {...connectionInfoProps} /> + </Body> + <Footer> + <SwitchLocation /> + <Cancel /> + </Footer> + </Wrapper> + ); + case 'connected': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.secured} /> + <Location> + <City /> + <Country /> + </Location> + <Hostname /> + <ConnectionInfo {...connectionInfoProps} /> + </Body> + <Footer> + <SwitchLocation /> + <Disconnect /> + </Footer> + </Wrapper> + ); - case 'blocked': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.blocked} /> - </Body> - <Footer> - <SwitchLocation /> - <Cancel /> - </Footer> - </Wrapper> - ); + case 'blocked': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.blocked} /> + </Body> + <Footer> + <SwitchLocation /> + <Cancel /> + </Footer> + </Wrapper> + ); - case 'disconnecting': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.secured} /> - <Location> - <Country /> - </Location> - </Body> - <Footer> - <SelectedLocation /> - <Connect /> - </Footer> - </Wrapper> - ); + case 'disconnecting': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.secured} /> + <Location> + <Country /> + </Location> + </Body> + <Footer> + <SelectedLocation /> + <Connect /> + </Footer> + </Wrapper> + ); - case 'disconnected': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.unsecured} /> - <Location> - <Country /> - </Location> - </Body> - <Footer> - <SelectedLocation /> - <Connect /> - </Footer> - </Wrapper> - ); + case 'disconnected': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.unsecured} /> + <Location> + <Country /> + </Location> + </Body> + <Footer> + <SelectedLocation /> + <Connect /> + </Footer> + </Wrapper> + ); + + default: + throw new Error(`Unknown TunnelState: ${(this.props.tunnelState: empty)}`); + } + } +} + +type ContainerProps = { + children?: Types.ReactNode, +}; - default: - throw new Error(`Unknown TunnelState: ${(props.tunnelState: empty)}`); +class Wrapper extends Component<ContainerProps> { + render() { + return <View style={styles.tunnel_control}>{this.props.children}</View>; + } +} + +class Body extends Component<ContainerProps> { + render() { + return <View style={styles.body}>{this.props.children}</View>; } } diff --git a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js index 572954b26b..594a33fdf6 100644 --- a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js +++ b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js @@ -25,6 +25,7 @@ export type AccountData = { expiry: string }; export type AccountToken = string; export type Ip = string; export type Location = { + ip: ?string, country: string, city: ?string, latitude: number, @@ -33,6 +34,7 @@ export type Location = { hostname: ?string, }; const LocationSchema = object({ + ip: maybe(string), country: string, city: maybe(string), latitude: number, @@ -55,8 +57,22 @@ export type AfterDisconnect = 'nothing' | 'block' | 'reconnect'; export type TunnelState = 'connecting' | 'connected' | 'disconnecting' | 'disconnected' | 'blocked'; +export type TunnelEndpoint = { + address: string, + tunnel: TunnelEndpointData, +}; + +export type TunnelEndpointData = { + openvpn: { + port: number, + protocol: RelayProtocol, + }, +}; + export type TunnelStateTransition = - | { state: 'disconnected' | 'connecting' | 'connected' } + | { state: 'disconnected' } + | { state: 'connecting', details: ?TunnelEndpoint } + | { state: 'connected', details: TunnelEndpoint } | { state: 'disconnecting', details: AfterDisconnect } | { state: 'blocked', details: BlockReason }; @@ -91,12 +107,7 @@ type RelaySettingsNormal<TTunnelConstraints> = { // types describing the structure of RelaySettings export type RelaySettingsCustom = { host: string, - tunnel: { - openvpn: { - port: number, - protocol: RelayProtocol, - }, - }, + tunnel: TunnelEndpointData, }; export type RelaySettings = | {| @@ -127,6 +138,13 @@ const constraint = <T>(constraintValue: SchemaNode<T>) => { ); }; +const TunnelEndpointDataSchema = object({ + openvpn: object({ + port: number, + protocol: enumeration('udp', 'tcp'), + }), +}); + const RelaySettingsSchema = oneOf( object({ normal: object({ @@ -156,12 +174,7 @@ const RelaySettingsSchema = oneOf( object({ custom_tunnel_endpoint: object({ host: string, - tunnel: object({ - openvpn: object({ - port: number, - protocol: enumeration('udp', 'tcp'), - }), - }), + tunnel: TunnelEndpointDataSchema, }), }), ); @@ -240,6 +253,13 @@ const TunnelStateTransitionSchema = oneOf( details: enumeration('nothing', 'block', 'reconnect'), }), object({ + state: enumeration('connecting', 'connected'), + details: object({ + address: string, + tunnel: TunnelEndpointDataSchema, + }), + }), + object({ state: enumeration('blocked'), details: oneOf( object({ diff --git a/gui/packages/desktop/src/renderer/redux/connection/actions.js b/gui/packages/desktop/src/renderer/redux/connection/actions.js index 668eb2a67b..6f8a297678 100644 --- a/gui/packages/desktop/src/renderer/redux/connection/actions.js +++ b/gui/packages/desktop/src/renderer/redux/connection/actions.js @@ -1,13 +1,15 @@ // @flow -import type { AfterDisconnect, BlockReason } from '../../lib/daemon-rpc'; +import type { AfterDisconnect, BlockReason, TunnelEndpoint } from '../../lib/daemon-rpc'; type ConnectingAction = { type: 'CONNECTING', + tunnelEndpoint: ?TunnelEndpoint, }; type ConnectedAction = { type: 'CONNECTED', + tunnelEndpoint: TunnelEndpoint, }; type DisconnectedAction = { @@ -54,15 +56,17 @@ export type ConnectionAction = | OnlineAction | OfflineAction; -function connecting(): ConnectingAction { +function connecting(tunnelEndpoint: ?TunnelEndpoint): ConnectingAction { return { type: 'CONNECTING', + tunnelEndpoint, }; } -function connected(): ConnectedAction { +function connected(tunnelEndpoint: TunnelEndpoint): ConnectedAction { return { type: 'CONNECTED', + tunnelEndpoint, }; } diff --git a/gui/packages/desktop/src/renderer/redux/connection/reducers.js b/gui/packages/desktop/src/renderer/redux/connection/reducers.js index b90f285e4f..622663dc14 100644 --- a/gui/packages/desktop/src/renderer/redux/connection/reducers.js +++ b/gui/packages/desktop/src/renderer/redux/connection/reducers.js @@ -32,10 +32,10 @@ export default function( return { ...state, ...action.newLocation }; case 'CONNECTING': - return { ...state, status: { state: 'connecting' } }; + return { ...state, status: { state: 'connecting', details: action.tunnelEndpoint } }; case 'CONNECTED': - return { ...state, status: { state: 'connected' } }; + return { ...state, status: { state: 'connected', details: action.tunnelEndpoint } }; case 'DISCONNECTED': return { ...state, status: { state: 'disconnected' } }; |
