diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-07-05 16:19:24 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-07-05 16:19:24 +0200 |
| commit | f5628a7a1a185e5d71cbeb8d1c3d56bd87ff30b5 (patch) | |
| tree | da38bcd09eaca01af7a5b4fd1adf041cf47aa8ef | |
| parent | e288920bec3000b6cb0da7e16b95c27e4e34be6c (diff) | |
| parent | 09adf211269ff66eb53af007289765fc53e59b15 (diff) | |
| download | mullvadvpn-f5628a7a1a185e5d71cbeb8d1c3d56bd87ff30b5.tar.xz mullvadvpn-f5628a7a1a185e5d71cbeb8d1c3d56bd87ff30b5.zip | |
Merge branch 'remove-backend'
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | app/app.js | 594 | ||||
| -rw-r--r-- | app/components/AccountInput.js | 14 | ||||
| -rw-r--r-- | app/components/Connect.js | 2 | ||||
| -rw-r--r-- | app/components/Login.js | 2 | ||||
| -rw-r--r-- | app/config.json | 2 | ||||
| -rw-r--r-- | app/containers/AccountPage.js | 8 | ||||
| -rw-r--r-- | app/containers/AdvancedSettingsPage.js | 5 | ||||
| -rw-r--r-- | app/containers/ConnectPage.js | 5 | ||||
| -rw-r--r-- | app/containers/LoginPage.js | 10 | ||||
| -rw-r--r-- | app/containers/PreferencesPage.js | 3 | ||||
| -rw-r--r-- | app/containers/SelectLocationPage.js | 7 | ||||
| -rw-r--r-- | app/errors.js | 61 | ||||
| -rw-r--r-- | app/index.js | 10 | ||||
| -rw-r--r-- | app/lib/backend.js | 631 | ||||
| -rw-r--r-- | app/lib/reconnection-backoff.js | 26 | ||||
| -rw-r--r-- | app/redux/account/actions.js | 6 | ||||
| -rw-r--r-- | app/routes.js | 4 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | yarn.lock | 79 |
20 files changed, 700 insertions, 774 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e0931a8d..0838ba1620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ Line wrap the file at 100 chars. Th ## [Unreleased] +### Fixed +- Disable account input when logging in. + ## [2018.2-beta1] - 2018-07-02 ### Added diff --git a/app/app.js b/app/app.js index d2eacfe751..9cbc368cde 100644 --- a/app/app.js +++ b/app/app.js @@ -1,28 +1,435 @@ // @flow import React from 'react'; -import { Component } from 'reactxp'; +import { bindActionCreators } from 'redux'; import { Provider } from 'react-redux'; -import { ConnectedRouter } from 'react-router-redux'; +import { ConnectedRouter, push as pushHistory } from 'react-router-redux'; import { createMemoryHistory } from 'history'; import { webFrame, ipcRenderer } from 'electron'; -import { log } from './lib/platform'; + import makeRoutes from './routes'; -import configureStore from './redux/store'; -import { Backend } from './lib/backend'; +import { log } from './lib/platform'; +import ReconnectionBackoff from './lib/reconnection-backoff'; import { DaemonRpc } from './lib/daemon-rpc'; import { setShutdownHandler } from './shutdown-handler'; +import { + RemoteError as JsonRpcTransportRemoteError, + TimeOutError as JsonRpcTransportTimeOutError, +} from './lib/jsonrpc-transport'; +import { + UnknownError, + NoAccountError, + CommunicationError, + InvalidAccountError, + NoDaemonError, +} from './errors'; + +import configureStore from './redux/store'; +import accountActions from './redux/account/actions'; +import connectionActions from './redux/connection/actions'; +import settingsActions from './redux/settings/actions'; + +import type { RpcCredentials } from './lib/rpc-address-file'; +import type { + DaemonRpcProtocol, + ConnectionObserver as DaemonConnectionObserver, +} from './lib/daemon-rpc'; +import type { ReduxStore } from './redux/store'; +import type { AccountToken, BackendState, RelaySettingsUpdate } from './lib/daemon-rpc'; import type { ConnectionState } from './redux/connection/reducers'; import type { TrayIconType } from './tray-icon-controller'; -import type { RpcCredentialsProvider, RpcCredentials } from './lib/backend'; -const initialState = null; -const memoryHistory = createMemoryHistory(); -const store = configureStore(initialState, memoryHistory); +export default class AppRenderer { + _daemonRpc: DaemonRpcProtocol = new DaemonRpc(); + _reconnectBackoff = new ReconnectionBackoff(); + _credentials: ?RpcCredentials; + _openConnectionObserver: ?DaemonConnectionObserver; + _closeConnectionObserver: ?DaemonConnectionObserver; + _memoryHistory = createMemoryHistory(); + _reduxStore: ReduxStore; + _reduxActions: *; + + constructor() { + const store = configureStore(null, this._memoryHistory); + const dispatch = store.dispatch; + + this._reduxStore = store; + this._reduxActions = { + account: bindActionCreators(accountActions, dispatch), + connection: bindActionCreators(connectionActions, dispatch), + settings: bindActionCreators(settingsActions, dispatch), + history: bindActionCreators({ push: pushHistory }, dispatch), + }; + + this._openConnectionObserver = this._daemonRpc.addOpenConnectionObserver(() => { + this._onOpenConnection(); + }); + + this._closeConnectionObserver = this._daemonRpc.addCloseConnectionObserver((error) => { + this._onCloseConnection(error); + }); + + this._setupReachability(); + + setShutdownHandler(async () => { + log.info('Executing a shutdown handler'); + try { + await this.disconnectTunnel(); + log.info('Disconnected the tunnel'); + } catch (e) { + log.error(`Failed to shutdown tunnel: ${e.message}`); + } + }); + + // disable pinch to zoom + webFrame.setVisualZoomLevelLimits(1, 1); + } + + dispose() { + const openConnectionObserver = this._openConnectionObserver; + const closeConnectionObserver = this._closeConnectionObserver; + + if (openConnectionObserver) { + openConnectionObserver.unsubscribe(); + this._openConnectionObserver = null; + } + + if (closeConnectionObserver) { + closeConnectionObserver.unsubscribe(); + this._closeConnectionObserver = null; + } + } + + renderView() { + return ( + <Provider store={this._reduxStore}> + <ConnectedRouter history={this._memoryHistory}> + {makeRoutes(this._reduxStore.getState, { app: this })} + </ConnectedRouter> + </Provider> + ); + } + + connect() { + this._connectToDaemon(); + } + + disconnect() { + this._daemonRpc.disconnect(); + } + + async login(accountToken: AccountToken) { + const actions = this._reduxActions; + actions.account.startLogin(accountToken); + + log.debug('Attempting to login'); + + try { + const accountData = await this._daemonRpc.getAccountData(accountToken); + await this._daemonRpc.setAccount(accountToken); + + actions.account.loginSuccessful(accountData.expiry); + + // Redirect the user after some time to allow for + // the 'Login Successful' screen to be visible + setTimeout(() => { + actions.history.push('/connect'); + log.debug('Autoconnecting...'); + this.connectTunnel(); + }, 1000); + } catch (error) { + log.error('Failed to log in,', error.message); + + actions.account.loginFailed(this._rpcErrorToBackendError(error)); + } + } + + async _autologin() { + const actions = this._reduxActions; + actions.account.startLogin(); + + log.debug('Attempting to log in automatically'); + + try { + const accountToken = await this._daemonRpc.getAccount(); + if (!accountToken) { + throw new NoAccountError(); + } + + log.debug(`The daemon had an account number stored: ${accountToken}`); + actions.account.startLogin(accountToken); + + const accountData = await this._daemonRpc.getAccountData(accountToken); + log.debug('The stored account number still exists:', accountData); + + actions.account.loginSuccessful(accountData.expiry); + actions.history.push('/connect'); + } catch (e) { + log.warn('Unable to autologin,', e.message); + + actions.account.autoLoginFailed(); + actions.history.push('/'); + + throw e; + } + } + + async logout() { + const actions = this._reduxActions; + + try { + await Promise.all([ + this.disconnectTunnel(), + this._daemonRpc.setAccount(null), + this._fetchAccountHistory(), + ]); + actions.account.loggedOut(); + actions.history.push('/'); + } catch (e) { + log.info('Failed to logout: ', e.message); + } + } + + async connectTunnel() { + const actions = this._reduxActions; + + try { + const currentState = await this._daemonRpc.getState(); + if (currentState.state === 'secured') { + log.debug('Refusing to connect as connection is already secured'); + actions.connection.connected(); + } else { + actions.connection.connecting(); + await this._daemonRpc.connectTunnel(); + } + } catch (error) { + actions.connection.disconnected(); + throw error; + } + } + + disconnectTunnel() { + return this._daemonRpc.disconnectTunnel(); + } + + updateRelaySettings(relaySettings: RelaySettingsUpdate) { + return this._daemonRpc.updateRelaySettings(relaySettings); + } + + async fetchRelaySettings() { + const actions = this._reduxActions; + const relaySettings = await this._daemonRpc.getRelaySettings(); + + log.debug('Got relay settings from daemon', JSON.stringify(relaySettings)); -class CredentialsProvider implements RpcCredentialsProvider { - request(): Promise<RpcCredentials> { + if (relaySettings.normal) { + const payload = {}; + const normal = relaySettings.normal; + const tunnel = normal.tunnel; + const location = normal.location; + + if (location === 'any') { + payload.location = 'any'; + } else { + payload.location = location.only; + } + + if (tunnel === 'any') { + payload.port = 'any'; + payload.protocol = 'any'; + } else { + const { port, protocol } = tunnel.only.openvpn; + payload.port = port === 'any' ? port : port.only; + payload.protocol = protocol === 'any' ? protocol : protocol.only; + } + + actions.settings.updateRelay({ + normal: payload, + }); + } else if (relaySettings.custom_tunnel_endpoint) { + const custom_tunnel_endpoint = relaySettings.custom_tunnel_endpoint; + const { + host, + tunnel: { + openvpn: { port, protocol }, + }, + } = custom_tunnel_endpoint; + + actions.settings.updateRelay({ + custom_tunnel_endpoint: { + host, + port, + protocol, + }, + }); + } + } + + async updateAccountExpiry() { + const actions = this._reduxActions; + try { + const accountToken = await this._daemonRpc.getAccount(); + if (!accountToken) { + throw new NoAccountError(); + } + const accountData = await this._daemonRpc.getAccountData(accountToken); + actions.account.updateAccountExpiry(accountData.expiry); + } catch (e) { + log.error(`Failed to update account expiry: ${e.message}`); + } + } + + async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { + await this._daemonRpc.removeAccountFromHistory(accountToken); + await this._fetchAccountHistory(); + } + + async _fetchAccountHistory(): Promise<void> { + const actions = this._reduxActions; + + const accountHistory = await this._daemonRpc.getAccountHistory(); + actions.account.updateAccountHistory(accountHistory); + } + + async _fetchRelayLocations() { + const actions = this._reduxActions; + const locations = await this._daemonRpc.getRelayLocations(); + + log.info('Got relay locations'); + + const storedLocations = locations.countries.map((country) => ({ + name: country.name, + code: country.code, + hasActiveRelays: country.cities.some((city) => city.has_active_relays), + cities: country.cities.map((city) => ({ + name: city.name, + code: city.code, + latitude: city.latitude, + longitude: city.longitude, + hasActiveRelays: city.has_active_relays, + })), + })); + + actions.settings.updateRelayLocations(storedLocations); + } + + async _fetchLocation() { + const actions = this._reduxActions; + const location = await this._daemonRpc.getLocation(); + + log.info('Got location from daemon'); + + const locationUpdate = { + ip: location.ip, + country: location.country, + city: location.city, + latitude: location.latitude, + longitude: location.longitude, + mullvadExitIp: location.mullvad_exit_ip, + }; + + actions.connection.newLocation(locationUpdate); + } + + async setAllowLan(allowLan: boolean) { + const actions = this._reduxActions; + await this._daemonRpc.setAllowLan(allowLan); + actions.settings.updateAllowLan(allowLan); + } + + async _fetchAllowLan() { + const actions = this._reduxActions; + const allowLan = await this._daemonRpc.getAllowLan(); + actions.settings.updateAllowLan(allowLan); + } + + async _fetchSecurityState() { + const securityState = await this._daemonRpc.getState(); + const connectionState = this._securityStateToConnectionState(securityState); + this._updateConnectionState(connectionState); + } + + async _connectToDaemon(): Promise<void> { + let credentials; + try { + credentials = await this._requestCredentials(); + } catch (error) { + log.error(`Cannot request the RPC credentials: ${error.message}`); + return; + } + + this._credentials = credentials; + this._daemonRpc.connect(credentials.connectionString); + } + + async _onOpenConnection() { + this._reconnectBackoff.reset(); + + // authenticate once connected + const credentials = this._credentials; + try { + if (!credentials) { + throw new Error('Credentials cannot be unset after connection is established.'); + } + await this._authenticate(credentials.sharedSecret); + } catch (error) { + log.error(`Cannot authenticate: ${error.message}`); + } + + // autologin + try { + await this._autologin(); + } catch (error) { + if (error instanceof NoAccountError) { + log.debug('No previously configured account set, showing window'); + ipcRenderer.send('show-window'); + } else { + log.error(`Failed to autologin: ${error.message}`); + } + } + + // make sure to re-subscribe to state notifications when connection is re-established. + try { + await this._subscribeStateListener(); + } catch (error) { + log.error(`Cannot subscribe for RPC notifications: ${error.message}`); + } + + // fetch initial state + try { + await this._fetchInitialState(); + } catch (error) { + log.error(`Cannot fetch initial state: ${error.message}`); + } + + // auto connect the tunnel + try { + await this.connectTunnel(); + } catch (error) { + log.error(`Cannot autoconnect the tunnel: ${error.message}`); + } + } + + async _onCloseConnection(error: ?Error) { + if (error) { + log.debug(`Lost connection to daemon: ${error.message}`); + + const recover = async () => { + try { + await this.connect(); + } catch (error) { + log.error(`Failed to reconnect: ${error.message}`); + } + }; + + this._reconnectBackoff.attempt(() => { + recover(); + }); + } + } + + _requestCredentials(): Promise<RpcCredentials> { return new Promise((resolve, _reject) => { ipcRenderer.once('daemon-connection-ready', (_event, credentials: RpcCredentials) => { resolve(credentials); @@ -30,64 +437,135 @@ class CredentialsProvider implements RpcCredentialsProvider { ipcRenderer.send('discover-daemon-connection'); }); } -} -const rpc = new DaemonRpc(); -const credentialsProvider = new CredentialsProvider(); -const backend = new Backend(store, rpc, credentialsProvider); -backend.connect(); + /** + * Start reachability monitoring for online/offline detection + * This is currently done via HTML5 APIs but will be replaced later + * with proper mullvad-daemon integration. + */ + _setupReachability() { + const actions = this._reduxActions; -setShutdownHandler(async () => { - log.info('Executing a shutdown handler'); + window.addEventListener('online', () => { + actions.connection.online(); + }); + window.addEventListener('offline', () => { + actions.connection.offline(); + }); - try { - await backend.disconnectTunnel(); - log.info('Disconnected the tunnel'); - } catch (e) { - log.error(`Failed to shutdown tunnel: ${e.message}`); + if (navigator.onLine) { + actions.connection.online(); + } else { + actions.connection.offline(); + } } -}); -/** - * Get tray icon type based on connection state - */ -const getIconType = (s: ConnectionState): TrayIconType => { - switch (s) { - case 'connected': - return 'secured'; - case 'connecting': - return 'securing'; - default: - return 'unsecured'; + async _subscribeStateListener() { + await this._daemonRpc.subscribeStateListener((newState, error) => { + if (error) { + log.error(`Received an error when processing the incoming state change: ${error.message}`); + } + + if (newState) { + const connectionState = this._securityStateToConnectionState(newState); + + log.debug( + `Got new state from daemon {state: ${newState.state}, target_state: ${ + newState.target_state + }}, translated to '${connectionState}'`, + ); + + this._updateConnectionState(connectionState); + this._refreshStateOnChange(); + } + }); } -}; -/** - * Update tray icon via IPC call - */ -const updateTrayIcon = () => { - const { connection } = store.getState(); + _fetchInitialState() { + return Promise.all([ + this._fetchSecurityState(), + this.fetchRelaySettings(), + this._fetchRelayLocations(), + this._fetchAllowLan(), + this._fetchLocation(), + this._fetchAccountHistory(), + ]); + } - // TODO: Only update the tray icon if the connection status changed - ipcRenderer.send('change-tray-icon', getIconType(connection.status)); -}; + _updateTrayIcon(connectionState: ConnectionState) { + const iconTypes: { [ConnectionState]: TrayIconType } = { + connected: 'secured', + connecting: 'securing', + }; + const type = iconTypes[connectionState] || 'unsecured'; -store.subscribe(updateTrayIcon); + ipcRenderer.send('change-tray-icon', type); + } -// force update tray -updateTrayIcon(); + async _refreshStateOnChange() { + try { + await this._fetchLocation(); + } catch (error) { + log.error(`Failed to fetch the location: ${error.message}`); + } + } -// disable smart pinch. -webFrame.setVisualZoomLevelLimits(1, 1); + _securityStateToConnectionState(backendState: BackendState): ConnectionState { + if (backendState.state === 'unsecured' && backendState.target_state === 'secured') { + return 'connecting'; + } else if (backendState.state === 'secured' && backendState.target_state === 'secured') { + return 'connected'; + } else if (backendState.target_state === 'unsecured') { + return 'disconnected'; + } + throw new Error('Unsupported state/target state combination: ' + JSON.stringify(backendState)); + } -export default class App extends Component { - render() { - return ( - <Provider store={store}> - <ConnectedRouter history={memoryHistory}> - {makeRoutes(store.getState, { backend })} - </ConnectedRouter> - </Provider> - ); + _updateConnectionState(connectionState: ConnectionState) { + const actions = this._reduxActions; + switch (connectionState) { + case 'connecting': + actions.connection.connecting(); + break; + case 'connected': + actions.connection.connected(); + break; + case 'disconnected': + actions.connection.disconnected(); + break; + } + + this._updateTrayIcon(connectionState); + } + + _rpcErrorToBackendError(e) { + if (e instanceof JsonRpcTransportRemoteError) { + switch (e.code) { + case -200: // Account doesn't exist + return new InvalidAccountError(); + case -32603: // Internal error + // We treat all internal backend errors as the user cannot reach + // api.mullvad.net. This is not always true of course, but it is + // true so often that we choose to disregard the other edge cases + // for now. + return new CommunicationError(); + } + } else if (e instanceof JsonRpcTransportTimeOutError) { + return new CommunicationError(); + } else if (e instanceof NoDaemonError) { + return e; + } + + return new UnknownError(e.message); + } + + async _authenticate(sharedSecret: string) { + try { + await this._daemonRpc.authenticate(sharedSecret); + log.info('Authenticated with backend'); + } catch (e) { + log.error(`Failed to authenticate with backend: ${e.message}`); + throw e; + } } } diff --git a/app/components/AccountInput.js b/app/components/AccountInput.js index 5a8fa47336..cb7cd258c5 100644 --- a/app/components/AccountInput.js +++ b/app/components/AccountInput.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; -import { formatAccount } from '../lib/formatters'; import { TextInput } from 'reactxp'; +import { formatAccount } from '../lib/formatters'; import { colors } from '../config'; // @TODO: move it into types.js @@ -66,10 +66,13 @@ export default class AccountInput extends React.Component<AccountInputProps, Acc } shouldComponentUpdate(nextProps: AccountInputProps, nextState: AccountInputState) { + const mergedProps = { ...this.props, ...nextProps }; + const hasPropChanges = Object.keys(mergedProps).some((key) => { + return this.props[key] !== nextProps[key]; + }); + return ( - this.props.value !== nextProps.value || - this.props.onEnter !== nextProps.onEnter || - this.props.onChange !== nextProps.onChange || + hasPropChanges || this.state.value !== nextState.value || this.state.selectionRange[0] !== nextState.selectionRange[0] || this.state.selectionRange[1] !== nextState.selectionRange[1] @@ -78,8 +81,7 @@ export default class AccountInput extends React.Component<AccountInputProps, Acc render() { const displayString = formatAccount(this.state.value || ''); - // eslint-disable-next-line no-unused-vars - const { value, onChange, onEnter, ...otherProps } = this.props; + const { value: _value, onChange: _onChange, onEnter: _onEnter, ...otherProps } = this.props; return ( <TextInput {...otherProps} diff --git a/app/components/Connect.js b/app/components/Connect.js index d14b768d44..79723e5672 100644 --- a/app/components/Connect.js +++ b/app/components/Connect.js @@ -9,7 +9,7 @@ import { TransparentButton, RedTransparentButton, GreenButton, Label } from './s import Accordion from './Accordion'; import styles from './ConnectStyles'; -import { NoCreditError, NoInternetError } from '../lib/backend'; +import { NoCreditError, NoInternetError } from '../errors'; import Map from './Map'; import type { HeaderBarStyle } from './HeaderBar'; diff --git a/app/components/Login.js b/app/components/Login.js index e70d84b7aa..355396a77c 100644 --- a/app/components/Login.js +++ b/app/components/Login.js @@ -352,7 +352,7 @@ export default class Login extends Component<Props, State> { onChange={this._onInputChange} onEnter={this._onLogin} value={this.props.accountToken || ''} - disabled={!this._shouldEnableAccountInput()} + editable={this._shouldEnableAccountInput()} autoFocus={true} ref={(ref) => (this._accountInput = ref)} testName="AccountInput" diff --git a/app/config.json b/app/config.json index fdbbfe7ae2..d634167048 100644 --- a/app/config.json +++ b/app/config.json @@ -22,10 +22,8 @@ "white20": "rgba(255, 255, 255, 0.2)", "blue20": "rgba(41, 77, 115, 0.2)", "blue40": "rgba(41, 77, 115, 0.4)", - "blue80": "rgba(41, 77, 115, 0.5)", "blue60": "rgba(41, 77, 115, 0.6)", "blue80": "rgba(41, 77, 115, 0.8)", - "blue80": "rgba(41, 77, 115, 0.9)", "red95": "rgba(208, 2, 27, 0.95)", "red40": "rgba(208, 2, 27, 0.40)", "red45": "rgba(208, 2, 27, 0.45)", diff --git a/app/containers/AccountPage.js b/app/containers/AccountPage.js index 1c54167b50..a8b54b30a3 100644 --- a/app/containers/AccountPage.js +++ b/app/containers/AccountPage.js @@ -4,7 +4,6 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { push } from 'react-router-redux'; import Account from '../components/Account'; -import accountActions from '../redux/account/actions'; import { links } from '../config'; import { openLink } from '../lib/platform'; @@ -16,14 +15,11 @@ const mapStateToProps = (state: ReduxState) => ({ accountExpiry: state.account.expiry, }); const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { - const { backend } = props; const { push: pushHistory } = bindActionCreators({ push }, dispatch); - const { logout } = bindActionCreators(accountActions, dispatch); - return { - updateAccountExpiry: () => backend.updateAccountExpiry(), + updateAccountExpiry: () => props.app.updateAccountExpiry(), onLogout: () => { - logout(backend); + props.app.logout(); }, onClose: () => { pushHistory('/settings'); diff --git a/app/containers/AdvancedSettingsPage.js b/app/containers/AdvancedSettingsPage.js index 0c1312b87f..357cafdfee 100644 --- a/app/containers/AdvancedSettingsPage.js +++ b/app/containers/AdvancedSettingsPage.js @@ -26,7 +26,6 @@ const mapStateToProps = (state: ReduxState) => { }; const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { - const { backend } = props; return { onClose: () => dispatch(push('/settings')), @@ -47,8 +46,8 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => .build(); try { - await backend.updateRelaySettings(relayUpdate); - await backend.fetchRelaySettings(); + await props.app.updateRelaySettings(relayUpdate); + await props.app.fetchRelaySettings(); } catch (e) { log.error('Failed to update relay settings', e.message); } diff --git a/app/containers/ConnectPage.js b/app/containers/ConnectPage.js index 21cc1a7133..7966a2fa04 100644 --- a/app/containers/ConnectPage.js +++ b/app/containers/ConnectPage.js @@ -57,7 +57,6 @@ const mapStateToProps = (state: ReduxState) => { const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { const { copyIPAddress } = bindActionCreators(connectActions, dispatch); const history = bindActionCreators({ push: pushHistory }, dispatch); - const { backend } = props; return { onSettings: () => { @@ -68,7 +67,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => }, onConnect: () => { try { - backend.connectTunnel(); + props.app.connectTunnel(); } catch (error) { log.error(`Failed to connect the tunnel: ${error.message}`); } @@ -78,7 +77,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => }, onDisconnect: () => { try { - backend.disconnectTunnel(); + props.app.disconnectTunnel(); } catch (error) { log.error(`Failed to disconnect the tunnel: ${error.message}`); } diff --git a/app/containers/LoginPage.js b/app/containers/LoginPage.js index fcf4dc8b7f..248c30ec1b 100644 --- a/app/containers/LoginPage.js +++ b/app/containers/LoginPage.js @@ -22,24 +22,20 @@ const mapStateToProps = (state: ReduxState) => { }; const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { const { push: pushHistory } = bindActionCreators({ push }, dispatch); - const { login, resetLoginError, updateAccountToken } = bindActionCreators( - accountActions, - dispatch, - ); - const { backend } = props; + const { resetLoginError, updateAccountToken } = bindActionCreators(accountActions, dispatch); return { openSettings: () => { pushHistory('/settings'); }, login: (account) => { - login(backend, account); + props.app.login(account); }, resetLoginError: () => { resetLoginError(); }, openExternalLink: (type) => openLink(links[type]), updateAccountToken: updateAccountToken, - removeAccountTokenFromHistory: (token) => backend.removeAccountFromHistory(token), + removeAccountTokenFromHistory: (token) => props.app.removeAccountFromHistory(token), }; }; diff --git a/app/containers/PreferencesPage.js b/app/containers/PreferencesPage.js index d4eb608038..6ee813209a 100644 --- a/app/containers/PreferencesPage.js +++ b/app/containers/PreferencesPage.js @@ -13,12 +13,11 @@ const mapStateToProps = (state: ReduxState) => ({ }); const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { - const { backend } = props; const { push: pushHistory } = bindActionCreators({ push }, dispatch); return { onClose: () => pushHistory('/settings'), onChangeAllowLan: (allowLan) => { - backend.setAllowLan(allowLan); + props.app.setAllowLan(allowLan); }, }; }; diff --git a/app/containers/SelectLocationPage.js b/app/containers/SelectLocationPage.js index 29b47c14a3..affe04dfc0 100644 --- a/app/containers/SelectLocationPage.js +++ b/app/containers/SelectLocationPage.js @@ -15,7 +15,6 @@ const mapStateToProps = (state: ReduxState) => ({ }); const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { const { push: pushHistory } = bindActionCreators({ push }, dispatch); - const { backend } = props; return { onClose: () => pushHistory('/connect'), onSelect: async (relayLocation) => { @@ -24,9 +23,9 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => .location.fromRaw(relayLocation) .build(); - await backend.updateRelaySettings(relayUpdate); - await backend.fetchRelaySettings(); - await backend.connectTunnel(); + await props.app.updateRelaySettings(relayUpdate); + await props.app.fetchRelaySettings(); + await props.app.connectTunnel(); pushHistory('/connect'); } catch (e) { diff --git a/app/errors.js b/app/errors.js new file mode 100644 index 0000000000..929fa52ab9 --- /dev/null +++ b/app/errors.js @@ -0,0 +1,61 @@ +export class NoCreditError extends Error { + constructor() { + super("Account doesn't have enough credit available for connection"); + } + + get userFriendlyTitle(): string { + return 'Out of time'; + } + + get userFriendlyMessage(): string { + return 'Buy more time, so you can continue using the internet securely'; + } +} + +export class NoInternetError extends Error { + constructor() { + super('Internet connectivity is currently unavailable'); + } + + get userFriendlyTitle(): string { + return 'Offline'; + } + + get userFriendlyMessage(): string { + return 'Your internet connection will be secured when you get back online'; + } +} + +export class NoDaemonError extends Error { + constructor() { + super('Could not connect to Mullvad daemon'); + } +} + +export class InvalidAccountError extends Error { + constructor() { + super('Invalid account number'); + } +} + +export class NoAccountError extends Error { + constructor() { + super('No account was set'); + } +} + +export class CommunicationError extends Error { + constructor() { + super('api.mullvad.net is blocked, please check your firewall'); + } +} + +export class UnknownError extends Error { + constructor(cause: string) { + super(`An unknown error occurred, ${cause}`); + } + + get userFriendlyTitle(): string { + return 'Something went wrong'; + } +} diff --git a/app/index.js b/app/index.js index 3f17282c34..39553123fc 100644 --- a/app/index.js +++ b/app/index.js @@ -1,7 +1,13 @@ -import React from 'react'; +// @flow + import RX from 'reactxp'; import App from './app'; +const app = new App(); +const view = app.renderView(); + +app.connect(); + RX.App.initialize(true, true); -RX.UserInterface.setMainView(<App />); +RX.UserInterface.setMainView(view); RX.UserInterface.useCustomScrollbars(true); diff --git a/app/lib/backend.js b/app/lib/backend.js deleted file mode 100644 index 3e128da0b1..0000000000 --- a/app/lib/backend.js +++ /dev/null @@ -1,631 +0,0 @@ -// @flow - -import { ipcRenderer } from 'electron'; -import { bindActionCreators } from 'redux'; -import { push as pushHistory } from 'react-router-redux'; -import { - RemoteError as JsonRpcTransportRemoteError, - TimeOutError as JsonRpcTransportTimeOutError, -} from './jsonrpc-transport'; -import accountActions from '../redux/account/actions'; -import connectionActions from '../redux/connection/actions'; -import settingsActions from '../redux/settings/actions'; -import { log } from '../lib/platform'; - -import type { RpcCredentials as OriginalRpcCredentials } from './rpc-address-file'; -import type { - DaemonRpcProtocol, - ConnectionObserver as DaemonConnectionObserver, -} from './daemon-rpc'; -import type { ReduxStore } from '../redux/store'; -import type { AccountToken, BackendState, RelaySettingsUpdate } from './daemon-rpc'; -import type { ConnectionState } from '../redux/connection/reducers'; - -export class NoCreditError extends Error { - constructor() { - super("Account doesn't have enough credit available for connection"); - } - - get userFriendlyTitle(): string { - return 'Out of time'; - } - - get userFriendlyMessage(): string { - return 'Buy more time, so you can continue using the internet securely'; - } -} - -export class NoInternetError extends Error { - constructor() { - super('Internet connectivity is currently unavailable'); - } - - get userFriendlyTitle(): string { - return 'Offline'; - } - - get userFriendlyMessage(): string { - return 'Your internet connection will be secured when you get back online'; - } -} - -export class NoDaemonError extends Error { - constructor() { - super('Could not connect to Mullvad daemon'); - } -} - -export class InvalidAccountError extends Error { - constructor() { - super('Invalid account number'); - } -} - -export class NoAccountError extends Error { - constructor() { - super('No account was set'); - } -} - -export class CommunicationError extends Error { - constructor() { - super('api.mullvad.net is blocked, please check your firewall'); - } -} - -export class UnknownError extends Error { - constructor(cause: string) { - super(`An unknown error occurred, ${cause}`); - } - - get userFriendlyTitle(): string { - return 'Something went wrong'; - } -} - -export class CredentialsRequestError extends Error { - _reason: Error; - - constructor(reason: Error) { - super('Failed to request the RPC credentials'); - this._reason = reason; - } - - get reason(): Error { - return this._reason; - } -} - -export type RpcCredentials = OriginalRpcCredentials; -export interface RpcCredentialsProvider { - request(): Promise<RpcCredentials>; -} - -/** - * Backend implementation - */ -export class Backend { - _daemonRpc: DaemonRpcProtocol; - _credentialsProvider: RpcCredentialsProvider; - _reconnectBackoff = new ReconnectionBackoff(); - _credentials: ?RpcCredentials; - _openConnectionObserver: ?DaemonConnectionObserver; - _closeConnectionObserver: ?DaemonConnectionObserver; - _reduxActions: *; - - constructor( - store: ReduxStore, - rpc: DaemonRpcProtocol, - credentialsProvider: RpcCredentialsProvider, - ) { - this._daemonRpc = rpc; - this._credentialsProvider = credentialsProvider; - - this._reduxActions = { - account: bindActionCreators(accountActions, store.dispatch), - connection: bindActionCreators(connectionActions, store.dispatch), - settings: bindActionCreators(settingsActions, store.dispatch), - history: bindActionCreators({ push: pushHistory }, store.dispatch), - }; - - this._openConnectionObserver = rpc.addOpenConnectionObserver(() => { - this._onOpenConnection(); - }); - - this._closeConnectionObserver = rpc.addCloseConnectionObserver((error) => { - this._onCloseConnection(error); - }); - - this._setupReachability(); - } - - dispose() { - const openConnectionObserver = this._openConnectionObserver; - const closeConnectionObserver = this._closeConnectionObserver; - - if (openConnectionObserver) { - openConnectionObserver.unsubscribe(); - this._openConnectionObserver = null; - } - - if (closeConnectionObserver) { - closeConnectionObserver.unsubscribe(); - this._closeConnectionObserver = null; - } - } - - connect() { - this._connectToDaemon(); - } - - disconnect() { - this._daemonRpc.disconnect(); - } - - async login(accountToken: AccountToken) { - const actions = this._reduxActions; - actions.account.startLogin(accountToken); - - log.debug('Attempting to login'); - - try { - const accountData = await this._daemonRpc.getAccountData(accountToken); - await this._daemonRpc.setAccount(accountToken); - - actions.account.loginSuccessful(accountData.expiry); - - // Redirect the user after some time to allow for - // the 'Login Successful' screen to be visible - setTimeout(() => { - actions.history.push('/connect'); - log.debug('Autoconnecting...'); - this.connectTunnel(); - }, 1000); - } catch (error) { - log.error('Failed to log in,', error.message); - - actions.account.loginFailed(this._rpcErrorToBackendError(error)); - } - } - - async autologin() { - const actions = this._reduxActions; - actions.account.startLogin(); - - log.debug('Attempting to log in automatically'); - - try { - const accountToken = await this._daemonRpc.getAccount(); - if (!accountToken) { - throw new NoAccountError(); - } - - log.debug(`The backend had an account number stored: ${accountToken}`); - actions.account.startLogin(accountToken); - - const accountData = await this._daemonRpc.getAccountData(accountToken); - log.debug('The stored account number still exists:', accountData); - - actions.account.loginSuccessful(accountData.expiry); - actions.history.push('/connect'); - } catch (e) { - log.warn('Unable to autologin,', e.message); - - actions.account.autoLoginFailed(); - actions.history.push('/'); - - throw e; - } - } - - async logout() { - const actions = this._reduxActions; - - try { - await Promise.all([ - this.disconnectTunnel(), - this._daemonRpc.setAccount(null), - this.fetchAccountHistory(), - ]); - actions.account.loggedOut(); - actions.history.push('/'); - } catch (e) { - log.info('Failed to logout: ', e.message); - } - } - - async connectTunnel() { - const actions = this._reduxActions; - - try { - const currentState = await this._daemonRpc.getState(); - if (currentState.state === 'secured') { - log.debug('Refusing to connect as connection is already secured'); - actions.connection.connected(); - } else { - actions.connection.connecting(); - await this._daemonRpc.connectTunnel(); - } - } catch (error) { - actions.connection.disconnected(); - throw error; - } - } - - disconnectTunnel() { - return this._daemonRpc.disconnectTunnel(); - } - - updateRelaySettings(relaySettings: RelaySettingsUpdate) { - return this._daemonRpc.updateRelaySettings(relaySettings); - } - - async fetchRelaySettings() { - const actions = this._reduxActions; - const relaySettings = await this._daemonRpc.getRelaySettings(); - - log.debug('Got relay settings from backend', JSON.stringify(relaySettings)); - - if (relaySettings.normal) { - const payload = {}; - const normal = relaySettings.normal; - const tunnel = normal.tunnel; - const location = normal.location; - - if (location === 'any') { - payload.location = 'any'; - } else { - payload.location = location.only; - } - - if (tunnel === 'any') { - payload.port = 'any'; - payload.protocol = 'any'; - } else { - const { port, protocol } = tunnel.only.openvpn; - payload.port = port === 'any' ? port : port.only; - payload.protocol = protocol === 'any' ? protocol : protocol.only; - } - - actions.settings.updateRelay({ - normal: payload, - }); - } else if (relaySettings.custom_tunnel_endpoint) { - const custom_tunnel_endpoint = relaySettings.custom_tunnel_endpoint; - const { - host, - tunnel: { - openvpn: { port, protocol }, - }, - } = custom_tunnel_endpoint; - - actions.settings.updateRelay({ - custom_tunnel_endpoint: { - host, - port, - protocol, - }, - }); - } - } - - async updateAccountExpiry() { - const actions = this._reduxActions; - try { - const accountToken = await this._daemonRpc.getAccount(); - if (!accountToken) { - throw new NoAccountError(); - } - const accountData = await this._daemonRpc.getAccountData(accountToken); - actions.account.updateAccountExpiry(accountData.expiry); - } catch (e) { - log.error(`Failed to update account expiry: ${e.message}`); - } - } - - async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { - await this._daemonRpc.removeAccountFromHistory(accountToken); - await this.fetchAccountHistory(); - } - - async fetchAccountHistory(): Promise<void> { - const actions = this._reduxActions; - - const accountHistory = await this._daemonRpc.getAccountHistory(); - actions.account.updateAccountHistory(accountHistory); - } - - async fetchRelayLocations() { - const actions = this._reduxActions; - const locations = await this._daemonRpc.getRelayLocations(); - - log.info('Got relay locations'); - - const storedLocations = locations.countries.map((country) => ({ - name: country.name, - code: country.code, - hasActiveRelays: country.cities.some((city) => city.has_active_relays), - cities: country.cities.map((city) => ({ - name: city.name, - code: city.code, - latitude: city.latitude, - longitude: city.longitude, - hasActiveRelays: city.has_active_relays, - })), - })); - - actions.settings.updateRelayLocations(storedLocations); - } - - async fetchLocation() { - const actions = this._reduxActions; - const location = await this._daemonRpc.getLocation(); - - log.info('Got location from daemon'); - - const locationUpdate = { - ip: location.ip, - country: location.country, - city: location.city, - latitude: location.latitude, - longitude: location.longitude, - mullvadExitIp: location.mullvad_exit_ip, - }; - - actions.connection.newLocation(locationUpdate); - } - - async setAllowLan(allowLan: boolean) { - const actions = this._reduxActions; - await this._daemonRpc.setAllowLan(allowLan); - actions.settings.updateAllowLan(allowLan); - } - - async fetchAllowLan() { - const actions = this._reduxActions; - const allowLan = await this._daemonRpc.getAllowLan(); - actions.settings.updateAllowLan(allowLan); - } - - async fetchSecurityState() { - const securityState = await this._daemonRpc.getState(); - const connectionState = this._securityStateToConnectionState(securityState); - this._updateConnectionState(connectionState); - } - - async _requestCredentials(): Promise<RpcCredentials> { - try { - return await this._credentialsProvider.request(); - } catch (providerError) { - throw new CredentialsRequestError(providerError); - } - } - - async _connectToDaemon(): Promise<void> { - let credentials; - try { - credentials = await this._requestCredentials(); - } catch (error) { - log.error(`Cannot request the RPC credentials: ${error.message}`); - return; - } - - this._credentials = credentials; - this._daemonRpc.connect(credentials.connectionString); - } - - async _onOpenConnection() { - this._reconnectBackoff.reset(); - - // authenticate once connected - const credentials = this._credentials; - try { - if (!credentials) { - throw new Error('Credentials cannot be unset after connection is established.'); - } - await this._authenticate(credentials.sharedSecret); - } catch (error) { - log.error(`Cannot authenticate: ${error.message}`); - } - - // autologin - try { - await this.autologin(); - } catch (error) { - if (error instanceof NoAccountError) { - log.debug('No previously configured account set, showing window'); - ipcRenderer.send('show-window'); - } else { - log.error(`Failed to autologin: ${error.message}`); - } - } - - // make sure to re-subscribe to state notifications when connection is re-established. - try { - await this._subscribeStateListener(); - } catch (error) { - log.error(`Cannot subscribe for RPC notifications: ${error.message}`); - } - - // fetch initial state - try { - await this._fetchInitialState(); - } catch (error) { - log.error(`Cannot fetch initial state: ${error.message}`); - } - - // auto connect the tunnel - try { - await this.connectTunnel(); - } catch (error) { - log.error(`Cannot autoconnect the tunnel: ${error.message}`); - } - } - - async _onCloseConnection(error: ?Error) { - if (error) { - log.debug(`Lost connection to daemon: ${error.message}`); - - const recover = async () => { - try { - await this.connect(); - } catch (error) { - log.error(`Failed to reconnect: ${error.message}`); - } - }; - - this._reconnectBackoff.attempt(() => { - recover(); - }); - } - } - - /** - * Start reachability monitoring for online/offline detection - * This is currently done via HTML5 APIs but will be replaced later - * with proper backend integration. - */ - _setupReachability() { - const actions = this._reduxActions; - - window.addEventListener('online', () => { - actions.connection.online(); - }); - window.addEventListener('offline', () => { - actions.connection.offline(); - }); - - if (navigator.onLine) { - actions.connection.online(); - } else { - actions.connection.offline(); - } - } - - async _subscribeStateListener() { - await this._daemonRpc.subscribeStateListener((newState, error) => { - if (error) { - log.error(`Received an error when processing the incoming state change: ${error.message}`); - } - - if (newState) { - const connectionState = this._securityStateToConnectionState(newState); - - log.debug( - `Got new state from backend {state: ${newState.state}, target_state: ${ - newState.target_state - }}, translated to '${connectionState}'`, - ); - - this._updateConnectionState(connectionState); - this._refreshStateOnChange(); - } - }); - } - - async _fetchInitialState() { - return Promise.all([ - this.fetchSecurityState(), - this.fetchRelaySettings(), - this.fetchRelayLocations(), - this.fetchAllowLan(), - this.fetchLocation(), - this.fetchAccountHistory(), - ]); - } - - async _refreshStateOnChange() { - try { - await this.fetchLocation(); - } catch (error) { - log.error(`Failed to fetch the location: ${error.message}`); - } - } - - _securityStateToConnectionState(backendState: BackendState): ConnectionState { - if (backendState.state === 'unsecured' && backendState.target_state === 'secured') { - return 'connecting'; - } else if (backendState.state === 'secured' && backendState.target_state === 'secured') { - return 'connected'; - } else if (backendState.target_state === 'unsecured') { - return 'disconnected'; - } - throw new Error('Unsupported state/target state combination: ' + JSON.stringify(backendState)); - } - - _updateConnectionState(connectionState: ConnectionState) { - const actions = this._reduxActions; - switch (connectionState) { - case 'connecting': - actions.connection.connecting(); - break; - case 'connected': - actions.connection.connected(); - break; - case 'disconnected': - actions.connection.disconnected(); - break; - } - } - - _rpcErrorToBackendError(e) { - if (e instanceof JsonRpcTransportRemoteError) { - switch (e.code) { - case -200: // Account doesn't exist - return new InvalidAccountError(); - case -32603: // Internal error - // We treat all internal backend errors as the user cannot reach - // api.mullvad.net. This is not always true of course, but it is - // true so often that we choose to disregard the other edge cases - // for now. - return new CommunicationError(); - } - } else if (e instanceof JsonRpcTransportTimeOutError) { - return new CommunicationError(); - } else if (e instanceof NoDaemonError) { - return e; - } - - return new UnknownError(e.message); - } - - async _authenticate(sharedSecret: string) { - try { - await this._daemonRpc.authenticate(sharedSecret); - log.info('Authenticated with backend'); - } catch (e) { - log.error(`Failed to authenticate with backend: ${e.message}`); - throw e; - } - } -} - -/* - * Used to calculate the time to wait before reconnecting - * the websocket. - * - * It uses a linear backoff function that goes from 500ms - * to 3000ms - */ -class ReconnectionBackoff { - _attempt: number; - - constructor() { - this._attempt = 0; - } - - attempt(handler: () => void) { - setTimeout(handler, this._getIncreasedBackoff()); - } - - reset() { - this._attempt = 0; - } - - _getIncreasedBackoff() { - if (this._attempt < 6) { - this._attempt++; - } - return this._attempt * 500; - } -} diff --git a/app/lib/reconnection-backoff.js b/app/lib/reconnection-backoff.js new file mode 100644 index 0000000000..1cd8203dac --- /dev/null +++ b/app/lib/reconnection-backoff.js @@ -0,0 +1,26 @@ +/* + * Used to calculate the time to wait before reconnecting to the daemon. + * It uses a linear backoff function that goes from 500ms to 3000ms. + */ +export default class ReconnectionBackoff { + _attempt: number; + + constructor() { + this._attempt = 0; + } + + attempt(handler: () => void) { + setTimeout(handler, this._getIncreasedBackoff()); + } + + reset() { + this._attempt = 0; + } + + _getIncreasedBackoff() { + if (this._attempt < 6) { + this._attempt++; + } + return this._attempt * 500; + } +} diff --git a/app/redux/account/actions.js b/app/redux/account/actions.js index df25c1e823..41fbfdee05 100644 --- a/app/redux/account/actions.js +++ b/app/redux/account/actions.js @@ -1,7 +1,6 @@ // @flow import type { AccountToken } from '../../lib/daemon-rpc'; -import type { Backend } from '../../lib/backend'; type StartLoginAction = { type: 'START_LOGIN', @@ -109,12 +108,7 @@ function updateAccountExpiry(expiry: string): UpdateAccountExpiryAction { }; } -const login = (backend: Backend, account: string) => () => backend.login(account); -const logout = (backend: Backend) => () => backend.logout(); - export default { - login, - logout, startLogin, loginSuccessful, loginFailed, diff --git a/app/routes.js b/app/routes.js index c48e03dac3..95ad24e876 100644 --- a/app/routes.js +++ b/app/routes.js @@ -15,10 +15,10 @@ import SelectLocationPage from './containers/SelectLocationPage'; import { getTransitionProps } from './transitions'; import type { ReduxGetState } from './redux/store'; -import type { Backend } from './lib/backend'; +import type App from './app'; export type SharedRouteProps = { - backend: Backend, + app: App, }; export default function makeRoutes( diff --git a/package.json b/package.json index e8ede62cc7..2128fbf2aa 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "react-router": "^4.2.0", "react-router-redux": "^5.0.0-alpha.9", "react-simple-maps": "^0.10.1", - "reactxp": "1.3.0-rc.6", + "reactxp": "^1.3.0", "redux": "^3.0.0", "redux-thunk": "^2.2.0", "uuid": "^3.0.1", @@ -86,14 +86,14 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" +"@types/lodash@4.14.110": + version "4.14.110" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.110.tgz#fb07498f84152947f30ea09d89207ca07123461e" + "@types/lodash@^4.14.64": version "4.14.91" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.91.tgz#794611b28056d16b5436059c6d800b39d573cd3a" -"@types/lodash@^4.14.80": - version "4.14.105" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.105.tgz#9fcc4627a1f98f8f8fce79ddb2bff4afd97e959b" - "@types/node@*": version "8.5.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.2.tgz#83b8103fa9a2c2e83d78f701a9aa7c9539739aa5" @@ -102,16 +102,16 @@ version "8.9.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.9.4.tgz#dfd327582a06c114eb6e0441fa3d6fab35edad48" -"@types/react-dom@^16.0.6": +"@types/react-dom@16.0.6": version "16.0.6" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c" dependencies: "@types/node" "*" "@types/react" "*" -"@types/react-native@^0.55.23": - version "0.55.25" - resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.55.25.tgz#68484f8c3f1ab0ccce21ea556dcbcf20140f0c63" +"@types/react-native@0.55.26": + version "0.55.26" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.55.26.tgz#a7150ca15e0de7e435cc66a1ca44f6b506c99e40" dependencies: "@types/react" "*" @@ -119,11 +119,9 @@ version "16.0.31" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.31.tgz#5285da62f3ac62b797f6d0729a1d6181f3180c3e" -"@types/react@^16.3.17": - version "16.3.17" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.17.tgz#d59d1a632570b0713946ed9c2949d994773633c5" - dependencies: - csstype "^2.2.0" +"@types/react@16.0.36": + version "16.0.36" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.36.tgz#ceb5639013bdb92a94147883052e69bb2c22c69b" abbrev@1: version "1.1.1" @@ -374,7 +372,7 @@ assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" -assert@^1.3.0: +assert@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" dependencies: @@ -2141,10 +2139,6 @@ css-what@2.1: version "2.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" -csstype@^2.2.0: - version "2.5.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7" - csurf@~1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/csurf/-/csurf-1.8.3.tgz#23f2a13bf1d8fce1d0c996588394442cba86a56a" @@ -4898,6 +4892,10 @@ lodash.templatesettings@^3.0.0: lodash._reinterpolate "^3.0.0" lodash.escape "^3.0.0" +lodash@4.17.10, lodash@^4.17.10, lodash@^4.17.4: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + lodash@^3.1.0, lodash@^3.10.1, lodash@^3.2.0, lodash@^3.3.1, lodash@^3.5.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -4910,10 +4908,6 @@ lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.2.1, lodash@^4.3.0, lo version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -lodash@^4.17.10, lodash@^4.17.4: - version "4.17.10" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" - log-symbols@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" @@ -6040,7 +6034,14 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0: +prop-types@15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" + dependencies: + loose-envify "^1.3.1" + object-assign "^4.1.1" + +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -6350,20 +6351,20 @@ react@^16.0.0: object-assign "^4.1.1" prop-types "^15.6.0" -reactxp@1.3.0-rc.6: - version "1.3.0-rc.6" - resolved "https://registry.yarnpkg.com/reactxp/-/reactxp-1.3.0-rc.6.tgz#11e68d2e450926b7b51d507300041912eb3dfc61" +reactxp@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reactxp/-/reactxp-1.3.0.tgz#ccb3859b7713ea0cb921fa3155c84f02b36b0f17" dependencies: - "@types/lodash" "^4.14.80" - "@types/react" "^16.3.17" - "@types/react-dom" "^16.0.6" - "@types/react-native" "^0.55.23" - assert "^1.3.0" - lodash "^4.17.4" - prop-types "^15.5.9" - rebound "^0.1.0" - subscribableevent "^1.0.0" - synctasks "^0.3.3" + "@types/lodash" "4.14.110" + "@types/react" "16.0.36" + "@types/react-dom" "16.0.6" + "@types/react-native" "0.55.26" + assert "1.4.1" + lodash "4.17.10" + prop-types "15.6.2" + rebound "0.1.0" + subscribableevent "1.0.0" + synctasks "0.3.3" read-all-stream@^3.0.0: version "3.1.0" @@ -6520,7 +6521,7 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" -rebound@^0.1.0: +rebound@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/rebound/-/rebound-0.1.0.tgz#0638c61a93666bb515a58a03e1cfb34021e88b72" @@ -7387,7 +7388,7 @@ strip-outer@^1.0.0: dependencies: escape-string-regexp "^1.0.2" -subscribableevent@^1.0.0: +subscribableevent@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/subscribableevent/-/subscribableevent-1.0.0.tgz#bde9500fa9009c7740c924109bac6119cd9898e6" dependencies: @@ -7443,7 +7444,7 @@ sync-exec@~0.6.x: version "0.6.2" resolved "https://registry.yarnpkg.com/sync-exec/-/sync-exec-0.6.2.tgz#717d22cc53f0ce1def5594362f3a89a2ebb91105" -synctasks@^0.3.3: +synctasks@0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/synctasks/-/synctasks-0.3.3.tgz#1e3dde423b39d28bc940fdb7698d8b4b7a741e77" |
