From 242b3ed50bc502f8da8b6542b30c7f8beaedc61f Mon Sep 17 00:00:00 2001 From: yan Date: Wed, 6 Jun 2018 15:29:17 -0700 Subject: [PATCH] Add tor loading progress UI Fix https://github.com/brave/browser-laptop/issues/14043 Test Plan: Success case: 1. start brave 2. immediately open a tor private tab 3. you should briefly see a loading message in the URLbar 4. once loading is finished, you should be able to type in the URLbar Fail case: 1. start an HTTP listener on port 9250 (so that Tor cannot bind to it. this causes Tor to not be able to connect). 2. start brave 3. open tor tab and wait for 20s 4. you should see an error message pop up 5. the button and link in the error message should work as expected --- app/browser/reducers/torReducer.js | 28 ++++++++ .../brave/locales/en-US/app.properties | 9 ++- app/filtering.js | 47 +++++++++++--- app/locale.js | 4 ++ app/renderer/components/main/main.js | 7 +- app/renderer/components/main/siteInfo.js | 64 ++++++++++++++++++- app/renderer/components/navigation/urlBar.js | 31 ++++++++- app/sessionStore.js | 3 + docs/state.md | 4 ++ js/actions/appActions.js | 28 +++++++- js/constants/appConstants.js | 3 + js/stores/appStore.js | 12 ++-- 12 files changed, 218 insertions(+), 22 deletions(-) create mode 100644 app/browser/reducers/torReducer.js diff --git a/app/browser/reducers/torReducer.js b/app/browser/reducers/torReducer.js new file mode 100644 index 00000000000..f32405f59f4 --- /dev/null +++ b/app/browser/reducers/torReducer.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict' + +const filtering = require('../../filtering') +const appConstants = require('../../../js/constants/appConstants') + +const torReducer = (state, action) => { + switch (action.actionType) { + case appConstants.APP_SET_TOR_NEW_IDENTITY: + filtering.setTorNewIdentity(action.url, action.tabId) + break + case appConstants.APP_ON_TOR_INIT_ERROR: + state = state.setIn(['tor', 'initializationError'], action.message) + break + case appConstants.APP_ON_TOR_INIT_SUCCESS: + state = state.setIn(['tor', 'initializationError'], false).setIn(['tor', 'percentInitialized'], null) + break + case appConstants.APP_ON_TOR_INIT_PERCENTAGE: + state = state.setIn(['tor', 'percentInitialized'], action.percentage) + break + } + return state +} + +module.exports = torReducer diff --git a/app/extensions/brave/locales/en-US/app.properties b/app/extensions/brave/locales/en-US/app.properties index e0266b502d5..b331f4c4a00 100644 --- a/app/extensions/brave/locales/en-US/app.properties +++ b/app/extensions/brave/locales/en-US/app.properties @@ -239,6 +239,11 @@ tabsSuggestionTitle=Tabs topSiteSuggestionTitle=Top Site torrentBlockedInTor=For your privacy, torrents are blocked in private tabs when Tor is enabled. torrentWarningOk=Ok +torConnectionError=Unable to connect to the Tor network +torConnectionErrorInfo=Brave could not make a connection to the Tor network. Disable Tor to continue private browsing without Tor protection. +torConnectionErrorDisable=Disable Tor +torConnectionErrorRetry=To retry connecting to the Tor network, +torConnectionErrorRestart=click here to restart Brave. turnOffNotifications=Turn off notifications unknownError=Oops, something went wrong. unmuteTab=Unmute tab @@ -257,7 +262,9 @@ updateNow=Update updateOops=Oops! updateRequiresRelaunch=Requires a quick relaunch… updateViewLog=View log -urlbar.placeholder=Enter a URL or search term +urlbarPlaceholder=Enter a URL or search term +urlbarPlaceholderTorSuccess=Successfully connected to the Tor network! +urlbarPlaceholderTorProgress=Connecting to the Tor network urlCopied=URL copied to clipboard useBrave=Use Brave verifiedPublisher.title=This is a verified publisher. Click to enable this publisher for payments diff --git a/app/filtering.js b/app/filtering.js index 98929f87098..4de55aace65 100644 --- a/app/filtering.js +++ b/app/filtering.js @@ -726,7 +726,11 @@ const initPartition = (partition) => { options.parent_partition = '' } if (isTorPartition) { - setupTor() + try { + setupTor() + } catch (e) { + appActions.onTorInitError(`Could not start Tor: ${e}`) + } // TODO(riastradh): Duplicate logic in app/browser/tabs.js. options.isolated_storage = true options.parent_partition = '' @@ -756,40 +760,63 @@ const initPartition = (partition) => { module.exports.initPartition = initPartition function setupTor () { + let torInitialized = null + // If Tor has not successfully initialized or thrown an error within 20s, + // assume it's broken. + setTimeout(() => { + if (torInitialized === null) { + appActions.onTorInitError(`Tor could not start.`) + } + }, 20000) // Set up the tor daemon watcher. (NOTE: We don't actually start // the tor daemon here; that happens in C++ code. But we do talk to // its control socket.) const torDaemon = new tor.TorDaemon() torDaemon.setup((err) => { if (err) { - console.log(`tor: failed to make directories: ${err}`) + appActions.onTorInitError(`Tor failed to make directories: ${err}`) + torInitialized = false return } - torDaemon.on('exit', () => console.log('tor: daemon exited')) + torDaemon.on('exit', () => { + appActions.onTorInitError('The Tor process has stopped.') + torInitialized = false + }) torDaemon.on('launch', (socksAddr) => { console.log(`tor: daemon listens on ${socksAddr}`) const bootstrapped = (err, progress) => { - // TODO(riastradh): Visually update a progress bar! if (err) { - console.log(`tor: bootstrap error: ${err}`) + appActions.onTorInitError(`Tor bootstrap error: ${err}`) + torInitialized = false return } - console.log(`tor: bootstrapped ${progress}%`) + appActions.onTorInitPercentage(progress) } const circuitEstablished = (err, ok) => { if (ok) { - console.log(`tor: ready`) + console.log('Tor ready!') + appActions.onTorInitSuccess() + torInitialized = true } else { - console.log(err ? `tor: not ready: ${err}` : `tor: not ready`) + if (err) { + appActions.onTorInitError(`Tor not ready: ${err}`) + torInitialized = false + } else { + // Simply log the error but don't show error UI since Tor might + // finish opening a circuit. + console.log('tor still not ready') + } } } torDaemon.onBootstrap(bootstrapped, (err) => { if (err) { - console.log(`tor: error subscribing to bootstrap: ${err}`) + appActions.onTorInitError(`Tor error bootstrapping: ${err}`) + torInitialized = false } torDaemon.onCircuitEstablished(circuitEstablished, (err) => { if (err) { - console.log(`tor: error subscribing to circuit ready: ${err}`) + appActions.onTorInitError(`Tor error opening a circuit: ${err}`) + torInitialized = false } }) }) diff --git a/app/locale.js b/app/locale.js index c9e5abed254..c97c3991e59 100644 --- a/app/locale.js +++ b/app/locale.js @@ -213,6 +213,10 @@ var rendererIdentifiers = function () { 'smartphoneTitle', 'updateLater', 'updateHello', + 'urlbarPlaceholder', + 'urlbarPlaceholderTorSuccess', + 'urlbarPlaceholderTorProgress', + 'torConnectionError', // notifications 'notificationPasswordWithUserName', 'notificationUpdatePasswordWithUserName', diff --git a/app/renderer/components/main/main.js b/app/renderer/components/main/main.js index 326b07fd095..48bfe5a08d5 100644 --- a/app/renderer/components/main/main.js +++ b/app/renderer/components/main/main.js @@ -539,6 +539,8 @@ class Main extends React.Component { const widevinePanelDetail = currentWindow.get('widevinePanelDetail', Immutable.Map()) const loginRequiredDetails = basicAuthState.getLoginRequiredDetail(state, activeTabId) const focused = isFocused(state) + const isTor = frameStateUtil.isTor(activeFrame) + const torConnectionError = state.getIn(['tor', 'initializationError']) const props = {} // used in renderer @@ -553,8 +555,9 @@ class Main extends React.Component { props.captionButtonsVisible = isWindows props.showContextMenu = currentWindow.has('contextMenuDetail') props.showPopupWindow = currentWindow.has('popupWindowDetail') - props.showSiteInfo = currentWindow.getIn(['ui', 'siteInfo', 'isVisible']) && - !isSourceAboutUrl(activeFrame.get('location')) + props.showSiteInfo = (currentWindow.getIn(['ui', 'siteInfo', 'isVisible']) && + !isSourceAboutUrl(activeFrame.get('location'))) || + (torConnectionError && isTor) props.showBravery = shieldState.braveShieldsEnabled(activeFrame) && !!currentWindow.get('braveryPanelDetail') props.showClearData = currentWindow.getIn(['ui', 'isClearBrowsingDataPanelVisible'], false) diff --git a/app/renderer/components/main/siteInfo.js b/app/renderer/components/main/siteInfo.js index 5fe9ac28acb..8fd8264fbf6 100644 --- a/app/renderer/components/main/siteInfo.js +++ b/app/renderer/components/main/siteInfo.js @@ -30,12 +30,16 @@ const urlUtil = require('../../../../js/lib/urlutil') const globalStyles = require('../styles/global') const commonStyles = require('../styles/commonStyles') +// Constants +const settings = require('../../../../js/constants/settings') + class SiteInfo extends React.Component { constructor (props) { super(props) this.onAllowRunInsecureContent = this.onAllowRunInsecureContent.bind(this) this.onDenyRunInsecureContent = this.onDenyRunInsecureContent.bind(this) this.onViewCertificate = this.onViewCertificate.bind(this) + this.onDisableTor = this.onDisableTor.bind(this) } onAllowRunInsecureContent () { @@ -61,7 +65,20 @@ class SiteInfo extends React.Component { windowActions.setSiteInfoVisible(false) } + onDisableTor () { + appActions.changeSetting(settings.USE_TOR_PRIVATE_TABS, false) + appActions.recreateTorTab(false, this.props.activeTabId, + this.props.activeTabIndex) + } + + onRestart () { + appActions.shuttingDown(true) + } + get secureIcon () { + if (this.props.torConnectionError) { + return
+ } if (this.props.isFullySecured) { // fully secure return
@@ -149,7 +166,26 @@ class SiteInfo extends React.Component { site: this.props.location } - if (this.props.maybePhishingLocation) { + if (this.props.torConnectionError) { + // Log the error for advanced users to debug + console.log('Tor connection error:', this.props.torConnectionError) + return
+
+
+
+
+
+
+
+
+ } else if (this.props.maybePhishingLocation) { return
@@ -232,10 +268,12 @@ class SiteInfo extends React.Component { props.secureConnection = isSecure === true props.partiallySecureConnection = isSecure === 1 props.certErrorConnection = isSecure === 2 + props.torConnectionError = frameStateUtil.isTor(activeFrame) && state.getIn(['tor', 'initializationError']) // used in other function props.isPrivate = activeFrame.get('isPrivate') props.activeTabId = activeFrame.get('tabId', tabState.TAB_ID_NONE) + props.activeTabIndex = frameStateUtil.getIndexByTabId(currentWindow, props.activeTabId) return props } @@ -287,12 +325,36 @@ const styles = StyleSheet.create({ margin: `${globalStyles.spacing.dialogInsideMargin} 0 0 ${globalStyles.spacing.dialogInsideMargin}` }, + connectionInfo__header: { + color: globalStyles.color.braveOrange, + fontSize: '1rem' + }, + connectionInfo__viewCertificateButton: { display: 'flex', justifyContent: 'flex-end', marginTop: globalStyles.spacing.dialogInsideMargin }, + torConnectionInfo: { + marginTop: '15px', + marginBottom: '20px' + }, + + torBody: { + paddingBottom: '15px', + lineHeight: '1.5em' + }, + + torFooter: { + lineHeight: '1.5em' + }, + + link: { + color: globalStyles.color.braveOrange, + cursor: 'pointer' + }, + siteInfo: { maxHeight: '300px', maxWidth: '400px', diff --git a/app/renderer/components/navigation/urlBar.js b/app/renderer/components/navigation/urlBar.js index ac8551a92f9..2b7d69774a1 100644 --- a/app/renderer/components/navigation/urlBar.js +++ b/app/renderer/components/navigation/urlBar.js @@ -41,6 +41,7 @@ const {normalizeLocation, getNormalizedSuggestion} = require('../../../common/li const isDarwin = require('../../../common/lib/platformUtil').isDarwin() const publisherUtil = require('../../../common/lib/publisherUtil') const historyUtil = require('../../../common/lib/historyUtil') +const locale = require('../../../../js/l10n') // Icons const iconNoScript = require('../../../../img/url-bar-no-script.svg') @@ -421,6 +422,23 @@ class UrlBar extends React.Component { } } + get placeholderValue () { + if (this.props.isTor) { + if (this.props.torInitializationError) { + return `${locale.translation('torConnectionError')}.` + } else if (this.props.torPercentInitialized) { + // Don't show 100% since it sometimes gets stuck at 100% + const percentInitialized = this.props.torPercentInitialized === '100' ? '99' : this.props.torPercentInitialized + return `${locale.translation('urlbarPlaceholderTorProgress')}: ${percentInitialized}%...` + } else if (this.props.torInitializationError === false) { + return locale.translation('urlbarPlaceholderTorSuccess') + } else { + return `${locale.translation('urlbarPlaceholderTorProgress')}...` + } + } + return locale.translation('urlbarPlaceholder') + } + get titleValue () { // For about:newtab we don't want the top of the browser saying New Tab // Instead just show "Brave" @@ -468,6 +486,7 @@ class UrlBar extends React.Component { const selectedIndex = urlbar.getIn(['suggestions', 'selectedIndex']) const allSiteSettings = siteSettingsState.getAllSiteSettings(state, activeFrameIsPrivate) const braverySettings = siteSettings.getSiteSettingsForURL(allSiteSettings, location) + const isTor = frameStateUtil.isTor(activeFrame) // TODO(bridiver) - these definitely needs a helpers const publisherKey = ledgerState.getLocationProp(state, baseUrl, 'publisher') @@ -493,6 +512,9 @@ class UrlBar extends React.Component { props.loading = activeFrame.get('loading') props.navigationProgressPercent = tabState.getNavigationProgressPercent(state, activeTabId) + props.isTor = isTor + props.torPercentInitialized = state.getIn(['tor', 'percentInitialized']) + props.torInitializationError = state.getIn(['tor', 'initializationError']) props.showDisplayTime = !props.titleMode && props.displayURL === location props.showNoScriptInfo = enableNoScript && scriptsBlocked && scriptsBlocked.size props.evCert = activeFrame.getIn(['security', 'evCert']) @@ -528,6 +550,12 @@ class UrlBar extends React.Component { return {this.props.evCert} } + get shouldDisable () { + return (this.props.displayURL === undefined && this.loadTime === '') || + (this.props.isTor && + (this.props.torPercentInitialized || this.props.torInitializationError !== false)) + } + setUrlBarRef (ref) { this.urlBarRef = ref } @@ -567,7 +595,7 @@ class UrlBar extends React.Component {
: { immutableData = immutableData.set('notifications', Immutable.List()) // Delete temp site settings immutableData = immutableData.set('temporarySiteSettings', Immutable.Map()) + // Delete Tor init state + immutableData = immutableData.set('tor', Immutable.Map()) if (immutableData.getIn(['settings', settings.CHECK_DEFAULT_ON_STARTUP]) === true) { // Delete defaultBrowserCheckComplete state since this is checked on startup @@ -1135,6 +1137,7 @@ module.exports.defaultAppState = () => { passwords: [], notifications: [], temporarySiteSettings: {}, + tor: {}, autofill: { addresses: { guid: [], diff --git a/docs/state.md b/docs/state.md index 5cc294b9639..f5526cbda13 100644 --- a/docs/state.md +++ b/docs/state.md @@ -608,6 +608,10 @@ AppStore // Same as siteSettings but never gets written to disk // XXX: This was intended for Private Browsing but is currently unused. }, + tor: { + percentInitialized: number, // percentage initialized + initializationError: string|boolean, // error message. false means successfully initialized. + }, updates: { lastCheckTimestamp: boolean, metadata: { diff --git a/js/actions/appActions.js b/js/actions/appActions.js index f33b5b7e4ac..ea8d8b98015 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -203,7 +203,7 @@ const appActions = { }) }, - /** + /** * Dispatches a message to the store to indicate that the webview entered full screen mode. * * @param {Object} tabId - Tab id of the frame to put in full screen @@ -830,10 +830,12 @@ const appActions = { /** * Dispatches a message when the app is shutting down. + * @param {boolean} restart - whether to restart after shutdown */ - shuttingDown: function () { + shuttingDown: function (restart) { dispatch({ - actionType: appConstants.APP_SHUTTING_DOWN + actionType: appConstants.APP_SHUTTING_DOWN, + restart }) }, @@ -2052,6 +2054,26 @@ const appActions = { }) }, + onTorInitError: function (message) { + dispatch({ + actionType: appConstants.APP_ON_TOR_INIT_ERROR, + message + }) + }, + + onTorInitSuccess: function () { + dispatch({ + actionType: appConstants.APP_ON_TOR_INIT_SUCCESS + }) + }, + + onTorInitPercentage: function (percentage) { + dispatch({ + actionType: appConstants.APP_ON_TOR_INIT_PERCENTAGE, + percentage + }) + }, + setTorNewIdentity: function (tabId, url) { dispatch({ actionType: appConstants.APP_SET_TOR_NEW_IDENTITY, diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index f5c20e16dbe..4f8ecaa5d69 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -209,6 +209,9 @@ const appConstants = { APP_ON_WALLET_DELETE: _, APP_ON_WALLET_PROPERTIES_ERROR: _, APP_ON_PUBLISHER_TOGGLE_UPDATE: _, + APP_ON_TOR_INIT_ERROR: _, + APP_ON_TOR_INIT_SUCCESS: _, + APP_ON_TOR_INIT_PERCENTAGE: _, APP_SET_TOR_NEW_IDENTITY: _, APP_RECREATE_TOR_TAB: _ } diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 19635933df8..27e38f97b2b 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -233,6 +233,7 @@ const handleAppAction = (action) => { require('../../app/browser/reducers/aboutNewTabReducer'), require('../../app/browser/reducers/braverySettingsReducer'), require('../../app/browser/reducers/siteSettingsReducer'), + require('../../app/browser/reducers/torReducer'), require('../../app/browser/reducers/pageDataReducer'), ledgerReducer, require('../../app/browser/menu') @@ -279,7 +280,13 @@ const handleAppAction = (action) => { calculateTopSites(true, true) break case appConstants.APP_SHUTTING_DOWN: - appDispatcher.shutdown() + if (action.restart) { + const args = process.argv.slice(1) + args.push('--relaunch') + app.relaunch({args}) + } else { + appDispatcher.shutdown() + } app.quit() break case appConstants.APP_SET_DATA_FILE_ETAG: @@ -401,9 +408,6 @@ const handleAppAction = (action) => { } } break - case appConstants.APP_SET_TOR_NEW_IDENTITY: - filtering.setTorNewIdentity(action.url, action.tabId) - break case appConstants.APP_ON_CLEAR_BROWSING_DATA: const defaults = appState.get('clearBrowsingDataDefaults') const temp = appState.get('tempClearBrowsingData', Immutable.Map())