From a25cc8c07f163ff38fffc1a978fa151015ca29a8 Mon Sep 17 00:00:00 2001 From: Taylor `Riastradh' Campbell Date: Mon, 8 Jan 2018 17:02:00 -0500 Subject: [PATCH] Tor MVP Support isolated_storage and tor_proxy requires https://github.com/brave/muon/pull/473 Use Session::IsOffTheRecord to detect private tab requires https://github.com/brave/muon/pull/473/commits/edad1b22c6c42fe72971a44dcb4ca147a1514201 Set Tor new identify fix #12997 requires https://github.com/brave/muon/pull/473/commits/7e052dd064e307a267e51202d6b7fab5b694c7fd Session::IsOffTheRecord for app/filtering Refresh page after getting new identity requires https://github.com/brave/muon/pull/473/commits/c3d6769ba628f4da20d9907dc44dfb6821036d03 implement switch Tor Private Tabs in about:newtab set isTor depending on tor private tab setting and tor availability close/re-open private tab when tor switch is toggled Recreate tor private tab at the same index it was previously at always enable new identity button for now Disable search suggestions in private mode for Tor Fix https://github.com/brave/browser-laptop/issues/13064 set ddg as default search engine in tor private tabs fix #13212 Disable webtorrent in tor private mode fix #13063 load favicons in Tor tabs as data: URLs fix #13065 Omit unlocked icon for HTTP onion sites Fix https://github.com/brave/browser-laptop/issues/12990 also fix unittests Test Plan: 1. go to http://3expgpdnrrzezf7r.onion/ in a private tab 2. no icon should be in the urlbar 3. go to https://3g2upl4pq6kufc4m.onion/ 4. you should see a lock icon disable webrtc in tor mode fix https://github.com/brave/browser-laptop/issues/13397 add note to shields panel about breakage in tor mode Tor binary path will be passed from browser-laptop (#13455) Disable widevine notification on Tor tabs needed for https://github.com/brave/browser-laptop/issues/13396 Test Plan: 1. go to https://shaka-player-demo.appspot.com in a tor private tab 2. you should not see a widevine notification disable plugins in Tor private tabs Workaround browser-context-created not being emitted for tor tabs Disable flash click to play on tor tabs TODO: figure out a way to disable widevine on a per-tab basis Test Plan: 1. ensure flash is installed and enabled in preferences 2. go to https://get.adobe.com/flashplayer/about/ 3. make sure there is no popup asking you to run flash 4. right click on the flash click-to-play element (looks like a puzzle piece) on the page. no context menu should appear. deny geolocation in tor mode fix https://github.com/brave/browser-laptop/issues/13447 implement new tor tabs mockup fix https://github.com/brave/browser-laptop/issues/12922 implement new Tor New Identity UX fix https://github.com/brave/browser-laptop/issues/13658 Test Plan: 1. open new private tab 2. go to check.torproject.org and open shields 3. click 'new circuit' button in shields 4. it should reload the page and show a new IP 5. open shields again, click the info circle next to 'new circuit' 5. it should open a FAQ page Bundling tor with Brave Set the tor socks port and data directory. (#13641) Pass the port and data directory down for tor to use depending on channel bind tor new circuit to hard refresh in tor tabs fix https://github.com/brave/browser-laptop/issues/13925 upgrade to muon 6.1.0 for tor API support --- .gitignore | 3 + .npmrc | 6 +- app/browser/api/ledger.js | 5 +- .../contentSettings/hostContentSettings.js | 10 ++- app/browser/reducers/aboutNewTabReducer.js | 18 +++- app/browser/reducers/tabsReducer.js | 21 ++++- app/browser/tabs.js | 32 ++++++- app/channel.js | 31 +++++++ app/common/commonMenu.js | 20 +++-- app/common/lib/suggestion.js | 12 ++- app/extensions.js | 18 +++- .../brave/content/scripts/favicon.js | 27 ++++++ app/extensions/brave/img/newtab/toricon.svg | 65 ++++++++++++++ .../brave/locales/en-US/bravery.properties | 6 +- .../brave/locales/en-US/newtab.properties | 11 ++- app/filtering.js | 39 ++++++++- app/renderer/components/main/braveryPanel.js | 51 +++++++++++ app/renderer/components/main/main.js | 3 +- .../components/navigation/urlBarIcon.js | 5 +- .../components/tabs/content/favIcon.js | 16 +++- app/renderer/rendererShortcutHandler.js | 8 +- app/renderer/rendererTabEvents.js | 20 +++++ docs/state.md | 3 +- js/about/aboutActions.js | 8 ++ js/about/newprivatetab.js | 86 +++++++++++++------ js/about/newtab.js | 1 + js/actions/appActions.js | 17 ++++ js/constants/appConfig.js | 6 +- js/constants/appConstants.js | 4 +- js/constants/config.js | 1 + js/constants/messages.js | 2 + js/constants/settings.js | 2 + js/flash.js | 3 + js/lib/request.js | 5 +- js/lib/urlutil.js | 14 +++ js/state/frameStateUtil.js | 14 +-- js/state/userPrefs.js | 13 ++- js/stores/appStore.js | 3 + js/stores/windowStore.js | 1 - package.json | 4 +- test/unit/about/newTabPageTest.js | 1 + .../renderer/reducers/urlBarReducerTest.js | 19 ++-- test/unit/lib/fakeElectron.js | 12 ++- test/unit/lib/urlutilTest.js | 19 ++++ test/unit/state/frameStateUtilTest.js | 18 ++++ tools/buildPackage.js | 9 ++ tools/cibuild.py | 2 +- tools/package_tor.js | 68 +++++++++++++++ 48 files changed, 689 insertions(+), 73 deletions(-) create mode 100644 app/extensions/brave/content/scripts/favicon.js create mode 100644 app/extensions/brave/img/newtab/toricon.svg create mode 100644 tools/package_tor.js diff --git a/.gitignore b/.gitignore index d2f75d658bc..5094e2a7213 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,6 @@ app/extensions/brave/content/scripts/sync.js # script used for signing for widevine signature_generator.py + +# binaries +app/extensions/bin diff --git a/.npmrc b/.npmrc index 7b805586b3d..9f140d16c9e 100644 --- a/.npmrc +++ b/.npmrc @@ -1,7 +1,7 @@ runtime = electron target_arch = x64 -brave_electron_version = 7.0.6 -chromedriver_version = 2.37 -target = v7.0.6 +brave_electron_version = 6.1.0 +chromedriver_version = 2.36 +target = v6.1.0 disturl=https://brave-laptop-binaries.s3.amazonaws.com/atom-shell/dist/ build_from_source = true diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index 8eadffa0505..75d7f3b6706 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -9,6 +9,7 @@ const format = require('date-fns/format') const Immutable = require('immutable') const electron = require('electron') const ipc = electron.ipcMain +const session = electron.session const path = require('path') const os = require('os') const qr = require('qr-image') @@ -863,7 +864,9 @@ const shouldTrackTab = (state, tabId) => { if (tabFromState == null) { tabFromState = pageDataState.getLastClosedTab(state, tabId) } - const isPrivate = !tabFromState.get('partition', '').startsWith('persist:') || tabFromState.get('incognito') + const partition = tabFromState.get('partition', '') + const ses = session.fromPartition(partition) + const isPrivate = (ses && ses.isOffTheRecord()) || tabFromState.get('incognito') return !isPrivate && !tabFromState.isEmpty() && ledgerUtil.shouldTrackView(tabFromState) } diff --git a/app/browser/contentSettings/hostContentSettings.js b/app/browser/contentSettings/hostContentSettings.js index 7cf137743b5..5a73ae5d15b 100644 --- a/app/browser/contentSettings/hostContentSettings.js +++ b/app/browser/contentSettings/hostContentSettings.js @@ -3,19 +3,27 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const { makeImmutable } = require('../../common/state/immutableUtil') +const appConfig = require('../../../js/constants/appConfig') let registeredCallbacks = [] let registeredSessions = {} let registeredPrivateSessions = {} +const blockContentSetting = { setting: 'block', primaryPattern: '*' } module.exports.setContentSettings = (contentSettings, incognito) => { contentSettings = makeImmutable(contentSettings) const partitions = incognito ? registeredPrivateSessions : registeredSessions for (let partition in partitions) { + let newContentSettings = contentSettings + if (partition === appConfig.tor.partition) { + // Do not allow plugins to be enabled in Tor contexts + newContentSettings = contentSettings.set('plugins', makeImmutable([blockContentSetting])) + } + const ses = partitions[partition] - contentSettings.forEach((settings, contentType) => { + newContentSettings.forEach((settings, contentType) => { ses.contentSettings.clearForOneType(contentType) settings.forEach((setting) => { module.exports.setContentSetting(ses, setting.get('primaryPattern'), setting.get('secondaryPattern'), diff --git a/app/browser/reducers/aboutNewTabReducer.js b/app/browser/reducers/aboutNewTabReducer.js index fa5fa549881..b80f2180919 100644 --- a/app/browser/reducers/aboutNewTabReducer.js +++ b/app/browser/reducers/aboutNewTabReducer.js @@ -14,9 +14,11 @@ const aboutNewTabReducer = (state, action) => { switch (action.actionType) { case appConstants.APP_SET_STATE: const useAlternativePrivateSearchEngine = getSetting(settings.USE_ALTERNATIVE_PRIVATE_SEARCH_ENGINE, state.get('settings')) + const torEnabled = getSetting(settings.USE_TOR_PRIVATE_TABS) state = aboutNewTabState.mergeDetails(state, { newTabPageDetail: { - useAlternativePrivateSearchEngine + useAlternativePrivateSearchEngine, + torEnabled } }) break @@ -36,6 +38,20 @@ const aboutNewTabReducer = (state, action) => { useAlternativePrivateSearchEngine: action.value } }) + } else if (action.key === settings.USE_TOR_PRIVATE_TABS) { + state = aboutNewTabState.mergeDetails(state, { + newTabPageDetail: { + torEnabled: action.value + } + }) + if (action.value === true) { + // Also enable DDG + state = aboutNewTabState.mergeDetails(state, { + newTabPageDetail: { + useAlternativePrivateSearchEngine: action.value + } + }) + } } } return state diff --git a/app/browser/reducers/tabsReducer.js b/app/browser/reducers/tabsReducer.js index e793afbbb88..06653904cb5 100644 --- a/app/browser/reducers/tabsReducer.js +++ b/app/browser/reducers/tabsReducer.js @@ -15,7 +15,7 @@ const tabState = require('../../common/state/tabState') const windowState = require('../../common/state/windowState') const siteSettings = require('../../../js/state/siteSettings') const siteSettingsState = require('../../common/state/siteSettingsState') -const {frameOptsFromFrame} = require('../../../js/state/frameStateUtil') +const {frameOptsFromFrame, isTor} = require('../../../js/state/frameStateUtil') const updateState = require('../../common/state/updateState') // Constants @@ -51,6 +51,10 @@ const getWebRTCPolicy = (state, tabId) => { return webrtcConstants.default } + if (isTor(tabValue)) { + return webrtcConstants.disableNonProxiedUdp + } + const allSiteSettings = siteSettingsState.getAllSiteSettings(state, tabValue.get('incognito') === true) const tabSiteSettings = siteSettings.getSiteSettingsForURL(allSiteSettings, tabValue.get('url')) @@ -254,6 +258,21 @@ const tabsReducer = (state, action, immutableAction) => { } }) break + case appConstants.APP_RECREATE_TOR_TAB: + { + const tabId = action.get('tabId') + tabs.create({ + url: 'about:newtab', + isPrivate: true, + windowId: tabState.getWindowId(state, tabId), + index: action.get('index'), + active: true, + isTor: action.get('torEnabled') + }, (tab) => { + appActions.tabCloseRequested(tabId) + }) + break + } case appConstants.APP_TAB_UPDATED: state = tabState.maybeCreateTab(state, action) // tabs.debugTabs(state) diff --git a/app/browser/tabs.js b/app/browser/tabs.js index 67ad488fce9..9399823125c 100644 --- a/app/browser/tabs.js +++ b/app/browser/tabs.js @@ -9,7 +9,7 @@ const { shouldDebugTabEvents } = require('../cmdLine') const tabState = require('../common/state/tabState') const {app, extensions, session, ipcMain} = require('electron') const {makeImmutable, makeJS} = require('../common/state/immutableUtil') -const {getTargetAboutUrl, getSourceAboutUrl, isSourceAboutUrl, newFrameUrl, isTargetAboutUrl, isIntermediateAboutPage, isTargetMagnetUrl, getSourceMagnetUrl} = require('../../js/lib/appUrlUtil') +const {getExtensionsPath, getTargetAboutUrl, getSourceAboutUrl, isSourceAboutUrl, newFrameUrl, isTargetAboutUrl, isIntermediateAboutPage, isTargetMagnetUrl, getSourceMagnetUrl} = require('../../js/lib/appUrlUtil') const {isURL, getUrlFromInput, toPDFJSLocation, getDefaultFaviconUrl, isHttpOrHttps, getLocationIfPDF} = require('../../js/lib/urlutil') const {isSessionPartition} = require('../../js/state/frameStateUtil') const {getOrigin} = require('../../js/lib/urlutil') @@ -28,7 +28,7 @@ const {newTabMode} = require('../common/constants/settingsEnums') const {tabCloseAction} = require('../common/constants/settingsEnums') const webContentsCache = require('./webContentsCache') const {FilterOptions} = require('ad-block') -const {isResourceEnabled} = require('../filtering') +const {isResourceEnabled, initPartition} = require('../filtering') const autofill = require('../autofill') const bookmarksState = require('../common/state/bookmarksState') const bookmarkFoldersState = require('../common/state/bookmarkFoldersState') @@ -38,6 +38,8 @@ const bookmarkOrderCache = require('../common/cache/bookmarkOrderCache') const ledgerState = require('../common/state/ledgerState') const {getWindow, notifyWindowWebContentsAdded} = require('./windows') const activeTabHistory = require('./activeTabHistory') +const path = require('path') +const {getTorSocksProxy} = require('../channel') let adBlockRegions let currentPartitionNumber = 0 @@ -101,6 +103,8 @@ const getPartition = (createProperties) => { let partition = session.defaultSession.partition if (createProperties.partition) { partition = createProperties.partition + } else if (createProperties.isTor) { + partition = appConfig.tor.partition } else if (createProperties.isPrivate) { partition = 'default' } else if (createProperties.isPartitioned) { @@ -114,6 +118,7 @@ const getPartition = (createProperties) => { const needsPartitionAssigned = (createProperties) => { return !createProperties.openerTabId || + createProperties.isTor || createProperties.isPrivate || createProperties.isPartitioned || createProperties.partitionNumber || @@ -511,6 +516,12 @@ const api = { index = newTabValue.get('index') } + const ses = session.fromPartition(newTab.session.partition) + let isPrivate + if (ses) { + isPrivate = ses.isOffTheRecord() + } + const frameOpts = { location, displayURL, @@ -519,6 +530,7 @@ const api = { active: !!newTabValue.get('active'), guestInstanceId: newTab.guestInstanceId, isPinned: !!newTabValue.get('pinned'), + isPrivate, openerTabId, disposition, index, @@ -1048,6 +1060,17 @@ const api = { if (isSessionPartition(createProperties.partition)) { createProperties.parent_partition = '' } + if (createProperties.isTor) { + // TODO(riastradh): Duplicate logic in app/filtering.js. + createProperties.isolated_storage = true + createProperties.parent_partition = '' + createProperties.tor_proxy = getTorSocksProxy() + if (process.platform === 'win32') { + createProperties.tor_path = path.join(getExtensionsPath('bin'), 'tor.exe') + } else { + createProperties.tor_path = path.join(getExtensionsPath('bin'), 'tor') + } + } } // Tabs are allowed to be initially discarded (unloaded) if they are regular tabs @@ -1069,6 +1092,11 @@ const api = { } extensions.createTab(createProperties, (tab) => { cb && cb(tab) + // XXX: Workaround for 'browser-context-created' not emitted for Tor + // browsing context + if (createProperties.isTor) { + initPartition(appConfig.tor.partition) + } }) } diff --git a/app/channel.js b/app/channel.js index e7c1241d6bb..f15dbe7ad8c 100644 --- a/app/channel.js +++ b/app/channel.js @@ -52,3 +52,34 @@ exports.getLinuxDesktopName = () => { } return desktopName } + +// getTorSocksProxy() +// +// Return the socks5:// `URL' for the Tor socks proxy we will +// configure the tor daemon to listen on and muon to connect to, +// depending on which channel we're using. This is provisional +// until we let the OS choose the port number as in +// , or +// until we add support for local sockets for SOCKS proxies as in +// . +// +exports.getTorSocksProxy = () => { + let portno + switch (channel) { + case 'dev': + case '': + default: + portno = 9250 + break + case 'beta': + portno = 9260 + break + case 'nightly': + portno = 9270 + break + case 'developer': + portno = 9280 + break + } + return `socks5://127.0.0.1:${portno}` +} diff --git a/app/common/commonMenu.js b/app/common/commonMenu.js index ede60939542..5ad784bf957 100644 --- a/app/common/commonMenu.js +++ b/app/common/commonMenu.js @@ -9,7 +9,7 @@ const messages = require('../../js/constants/messages') const locale = require('../../js/l10n') const settings = require('../../js/constants/settings') const {tabs} = require('../../js/constants/config') -const getSetting = require('../../js/settings').getSetting +const {getSetting} = require('../../js/settings') const communityURL = 'https://community.brave.com/' const isDarwin = process.platform === 'darwin' const electron = require('electron') @@ -77,10 +77,20 @@ module.exports.newPrivateTabMenuItem = () => { label: locale.translation('newPrivateTab'), accelerator: 'Shift+CmdOrCtrl+P', click: function (item, focusedWindow) { - ensureAtLeastOneWindow({ - url: 'about:newtab', - isPrivate: true - }) + // Check if Tor is available + const useTor = getSetting(settings.USE_TOR_PRIVATE_TABS) + if (useTor) { + ensureAtLeastOneWindow({ + url: 'about:newtab', + isPrivate: true, + isTor: true + }) + } else { + ensureAtLeastOneWindow({ + url: 'about:newtab', + isPrivate: true + }) + } } } } diff --git a/app/common/lib/suggestion.js b/app/common/lib/suggestion.js index e89a0e6cc9c..87c79b8178d 100644 --- a/app/common/lib/suggestion.js +++ b/app/common/lib/suggestion.js @@ -25,6 +25,14 @@ const sigmoid = (t) => { const ONE_DAY = 1000 * 60 * 60 * 24 +const searchSuggestionsEnabled = (state, tabId) => { + const frame = getFrameByTabId(state, tabId) + if (!frame || frame.get('isPrivate')) { + return false + } + return getSetting(settings.OFFER_SEARCH_SUGGESTIONS) +} + /* * Calculate the sorting priority for a history item based on number of * accesses and time since last access @@ -564,7 +572,7 @@ const getSearchSuggestions = (state, tabId, urlLocationLower) => { return new Promise((resolve, reject) => { const mapListToElements = getMapListToElements(urlLocationLower) let suggestionsList = Immutable.List() - if (getSetting(settings.OFFER_SEARCH_SUGGESTIONS)) { + if (searchSuggestionsEnabled(state, tabId)) { const searchResults = state.get('searchResults') const sortHandler = getSortForSearchSuggestions(urlLocationLower) if (searchResults) { @@ -630,7 +638,7 @@ const generateNewSearchXHRResults = debounce((state, windowId, tabId, input) => ? frameSearchDetail.get('autocomplete') : searchDetail.get('autocompleteURL') - const shouldDoSearchSuggestions = getSetting(settings.OFFER_SEARCH_SUGGESTIONS) && + const shouldDoSearchSuggestions = searchSuggestionsEnabled(state, tabId) && autocompleteURL && !isUrl(input) && input.length !== 0 diff --git a/app/extensions.js b/app/extensions.js index d7e7d7aa203..df4c9082a3e 100644 --- a/app/extensions.js +++ b/app/extensions.js @@ -128,6 +128,22 @@ let generateBraveManifest = () => { 'content/scripts/themeColor.js' ] }, + { + run_at: 'document_end', + all_frames: false, + matches: [''], + include_globs: [ + 'http://*/*', 'https://*/*' + ], + exclude_globs: [ + indexHTML, + getBraveExtUrl('about-*.html'), + getBraveExtUrl('about-*.html') + '#*' + ], + js: [ + 'content/scripts/favicon.js' + ] + }, { run_at: 'document_start', js: [ @@ -273,7 +289,7 @@ let generateTorrentManifest = () => { 48: 'img/webtorrent-48.png', 16: 'img/webtorrent-16.png' }, - incognito: 'split', + incognito: 'not_allowed', key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyWl+wMvL0wZX3JUs7GeZAvxMP+LWEh2bwMV1HyuBra/lGZIq3Fmh0+AFnvFPXz1NpQkbLS3QWyqhdIn/lepGwuc2ma0glPzzmieqwctUurMGSGManApGO1MkcbSPhb+R1mx8tMam5+wbme4WoW37PI3oATgOs2NvHYuP60qol3U7b/zB3IWuqtwtqKe2Q1xY17btvPuz148ygWWIHneedt0jwfr6Zp+CSLARB9Heq/jqGXV4dPSVZ5ebBHLQ452iZkHxS6fm4Z+IxjKdYs3HNj/s8xbfEZ2ydnArGdJ0lpSK9jkDGYyUBugq5Qp3FH6zV89WqBvoV1dqUmL9gxbHsQIDAQAB' } } diff --git a/app/extensions/brave/content/scripts/favicon.js b/app/extensions/brave/content/scripts/favicon.js new file mode 100644 index 00000000000..0e00ab0b2d4 --- /dev/null +++ b/app/extensions/brave/content/scripts/favicon.js @@ -0,0 +1,27 @@ +const getBase64FromImageUrl = (url) => { + return new Promise((resolve, reject) => { + const img = new window.Image() + img.onerror = function () { + reject(new Error('unable to load image')) + } + img.onload = function () { + const canvas = document.createElement('canvas') + canvas.width = this.naturalWidth + canvas.height = this.naturalHeight + canvas.getContext('2d') + .drawImage(this, 0, 0) + resolve(canvas.toDataURL('image/png')) + } + img.src = url + }) +} + +let faviconUrl = window.location.origin + '/favicon.ico' +const faviconNode = document.head.querySelector("link[rel='icon']") || document.head.querySelector("link[rel='shortcut icon']") +if (faviconNode) { + faviconUrl = faviconNode.getAttribute('href') || faviconUrl +} + +getBase64FromImageUrl(faviconUrl).then((data) => { + chrome.ipcRenderer.sendToHost('got-page-favicon', data) +}) diff --git a/app/extensions/brave/img/newtab/toricon.svg b/app/extensions/brave/img/newtab/toricon.svg new file mode 100644 index 00000000000..16e56c06fca --- /dev/null +++ b/app/extensions/brave/img/newtab/toricon.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/extensions/brave/locales/en-US/bravery.properties b/app/extensions/brave/locales/en-US/bravery.properties index dd85d0bd28a..ad4423e518e 100644 --- a/app/extensions/brave/locales/en-US/bravery.properties +++ b/app/extensions/brave/locales/en-US/bravery.properties @@ -13,7 +13,9 @@ blockAllCookies=Block all cookies blockAllFingerprinting=Block all fingerprinting blockPopups=Block Popups cookieControl=Cookie Control -editBraveryGlobalSettings=Edit default shield settings… +editBraveryGlobalSettings=Edit default shield settings… +newTorCircuit=New Tor circuit for this site… +braveryTorWarning=For your protection, some sites will not work fully in Tor mode. fingerprintingBlocked={[plural(blockedFingerprintCount)]} fingerprintingBlocked[one]=Fingerprinting Method Blocked fingerprintingBlocked[other]=Fingerprinting Methods Blocked @@ -28,4 +30,4 @@ safeBrowsing=Block Phishing / Malware scriptsBlockedNumber={[plural(blockedScriptCount)]} scriptsBlockedNumber[one]=Script Blocked scriptsBlockedNumber[other]=Scripts Blocked -showBraveAds=Show Brave Ads \ No newline at end of file +showBraveAds=Show Brave Ads diff --git a/app/extensions/brave/locales/en-US/newtab.properties b/app/extensions/brave/locales/en-US/newtab.properties index 8d6eebddd77..3cd9806f4fb 100644 --- a/app/extensions/brave/locales/en-US/newtab.properties +++ b/app/extensions/brave/locales/en-US/newtab.properties @@ -22,11 +22,14 @@ newTab=New Tab photoBy=Photo By pinTopSiteButton.title=Unpin page from top sites list preferencesPage.title=Go to Preferences page -privateTabText1=Private tabs are not logged in page history or counted in Brave Payments calculations. Private tabs and their cookies vanish when the browser is closed. -privateTabText2=File downloads, new bookmarks, and passwords can still be saved while using a private tab and will not be removed when the private tab is closed. -privateTabText3=Please note: Even though sites you visit in private tabs are not saved locally, they do not make you anonymous or invisible to your ISP, your employer, or to the sites you are visiting. +privateTabsMore=More about private tabs +privateTabText1=Whether or not you use Tor, private tabs are not logged in page history or counted in Brave Payments calculations. Private tabs and their cookies always vanish when the browser is closed. +privateTabText2=If you use private tabs without Tor, the sites you visit will be visible to your ISP or your employer. You won’t be anonymous, and sites will learn your public IP address. +privateTabText3=With Tor, Brave works hard to ensure that you’re extremely difficult to track online while providing a delightful browsing experience. But if your personal safety depends on remaining anonymous you may wish to use the Tor Browser from https://torproject.org instead. privateTabTitle=This is a Private Tab -privateTabSearchSectionTitle=Private search with +privateTabTorTitle=Make private tabs much more private with Tor +privateTabTorText1=Tor hides your IP address from the sites you visit, and hides the sites you visit from your ISP or your employer. Tor can slow down browsing. Some sites treat anonymous users differently, or might not work at all. Some sites might even ask you to prove you’re human. +privateTabSearchSectionTitle=Private search with DuckDuckGo privateTabSearchText1=With private search, Brave will use DuckDuckGo to answer your searches while you are in this private tab. DuckDuckGo is a search engine that does not track your search history, enabling you to search privately. removeBookmarkButton.title=Remove bookmark removeTopSiteButton.title=Remove page from Top Sites list diff --git a/app/filtering.js b/app/filtering.js index a3e47542925..9e2c55dbe91 100644 --- a/app/filtering.js +++ b/app/filtering.js @@ -9,12 +9,14 @@ const electron = require('electron') const session = electron.session const BrowserWindow = electron.BrowserWindow const webContents = electron.webContents +const webContentsCache = require('./browser/webContentsCache') const appActions = require('../js/actions/appActions') const appConfig = require('../js/constants/appConfig') const hostContentSettings = require('./browser/contentSettings/hostContentSettings') const downloadStates = require('../js/constants/downloadStates') const urlParse = require('./common/urlParse') const getSetting = require('../js/settings').getSetting +const {getExtensionsPath} = require('../js/lib/appUrlUtil') const appUrlUtil = require('../js/lib/appUrlUtil') const siteSettings = require('../js/state/siteSettings') const settings = require('../js/constants/settings') @@ -35,6 +37,7 @@ const ledgerUtil = require('./common/lib/ledgerUtil') const {cookieExceptions, isRefererException} = require('../js/data/siteHacks') const {getBraverySettingsCache, updateBraverySettingsCache} = require('./common/cache/braverySettingsCache') const {shouldDebugTabEvents} = require('./cmdLine') +const {getTorSocksProxy} = require('./channel') let appStore = null @@ -486,6 +489,9 @@ function registerPermissionHandler (session, partition) { if (!permissions[permission]) { console.warn('WARNING: got unregistered permission request', permission) response.push(false) + } else if (permission === 'geolocation' && partition === appConfig.tor.partition) { + // Never allow geolocation in Tor mode + response.push(false) } else if (isFullscreen && mainFrameOrigin && // The Torrent Viewer extension is always allowed to show fullscreen media mainFrameOrigin.startsWith('chrome-extension://' + config.torrentExtensionId)) { @@ -659,6 +665,19 @@ function registerForMagnetHandler (session) { } } +module.exports.setTorNewIdentity = (url, tabId) => { + const ses = session.fromPartition(appConfig.tor.partition) + if (!ses || !url) { + return + } + ses.setTorNewIdentity(url, () => { + const tab = webContentsCache.getWebContents(tabId) + if (tab && !tab.isDestroyed()) { + tab.reload(true) + } + }) +} + function initSession (ses, partition) { registeredSessions[partition] = ses ses.setEnableBrotli(true) @@ -666,6 +685,7 @@ function initSession (ses, partition) { } const initPartition = (partition) => { + const isTorPartition = partition === appConfig.tor.partition // Partitions can only be initialized once the app is ready if (!app.isReady()) { partitionsToInitialize.push(partition) @@ -675,6 +695,7 @@ const initPartition = (partition) => { return } initializedPartitions[partition] = true + let fns = [initSession, userPrefs.init, hostContentSettings.init, @@ -690,8 +711,20 @@ const initPartition = (partition) => { if (isSessionPartition(partition)) { options.parent_partition = '' } + if (isTorPartition) { + // TODO(riastradh): Duplicate logic in app/browser/tabs.js. + options.isolated_storage = true + options.parent_partition = '' + options.tor_proxy = getTorSocksProxy() + if (process.platform === 'win32') { + options.tor_path = path.join(getExtensionsPath('bin'), 'tor.exe') + } else { + options.tor_path = path.join(getExtensionsPath('bin'), 'tor') + } + } let ses = session.fromPartition(partition, options) + fns.forEach((fn) => { fn(ses, partition, module.exports.isPrivate(partition)) }) @@ -740,7 +773,11 @@ function shouldIgnoreUrl (details) { } module.exports.isPrivate = (partition) => { - return !partition.startsWith('persist:') + const ses = session.fromPartition(partition) + if (!ses) { + return false + } + return ses.isOffTheRecord() } module.exports.init = (state, action, store) => { diff --git a/app/renderer/components/main/braveryPanel.js b/app/renderer/components/main/braveryPanel.js index e8630c47eba..ac32ce5eb9c 100644 --- a/app/renderer/components/main/braveryPanel.js +++ b/app/renderer/components/main/braveryPanel.js @@ -57,8 +57,10 @@ class BraveryPanel extends React.Component { this.onToggleHTTPSE = this.onToggleSiteSetting.bind(this, 'httpsEverywhere') this.onToggleFp = this.onToggleSiteSetting.bind(this, 'fingerprintingProtection') this.onReload = this.onReload.bind(this) + this.onNewTorCircuit = this.onNewTorCircuit.bind(this) this.onEditGlobal = this.onEditGlobal.bind(this) this.onInfoClick = this.onInfoClick.bind(this) + this.onTorInfoClick = this.onTorInfoClick.bind(this) } onToggleAdsAndTracking (e) { @@ -104,6 +106,10 @@ class BraveryPanel extends React.Component { appActions.loadURLRequested(this.props.tabId, this.props.lastCommittedURL) } + onNewTorCircuit () { + appActions.setTorNewIdentity(this.props.tabId, this.props.lastCommittedURL) + } + onEditGlobal () { appActions.createTabRequested({ url: 'about:preferences#shields' @@ -117,6 +123,15 @@ class BraveryPanel extends React.Component { }) } + onTorInfoClick () { + this.onHide() + appActions.createTabRequested({ + url: config.torCircuitInfoUrl, + isPrivate: true, + isTor: true + }) + } + onHide () { windowActions.setBraveryPanelDetail() } @@ -232,6 +247,7 @@ class BraveryPanel extends React.Component { props.isBlockedScriptsShown = braveryPanelDetail.get('expandNoScript') props.isBlockingFingerprinting = props.blockedFingerprinting.size > 0 props.isFpShown = braveryPanelDetail.get('expandFp') + props.isTor = frameStateUtil.isTor(activeFrame) // used in other functions props.lastCommittedURL = lastCommittedURL @@ -623,6 +639,16 @@ class BraveryPanel extends React.Component { : null } + { + this.props.isTor + ?
+ : null + }
+ { + this.props.isTor + ?
+ + +
+ : null + } @@ -953,6 +997,9 @@ const styles = StyleSheet.create({ braveryPanel__body__footer__edit: { marginRight: '1rem' }, + braveryPanel__body__footer__tor: { + marginTop: '10px' + }, braveryPanel__body__footer__edit_clickable: { cursor: 'pointer' }, @@ -1013,6 +1060,10 @@ const styles = StyleSheet.create({ braveryPanel_compact__body__footer__edit: { marginBottom: editGlobalMarginBottom }, + braveryPanel_compact__body__footer__tor: { + maxWidth: '300px', + marginLeft: '4px' + }, // controlWrapper - Common braveryPanel__body__advanced__control: { diff --git a/app/renderer/components/main/main.js b/app/renderer/components/main/main.js index 326b07fd095..f59a251a31a 100644 --- a/app/renderer/components/main/main.js +++ b/app/renderer/components/main/main.js @@ -539,6 +539,7 @@ 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 props = {} // used in renderer @@ -559,7 +560,7 @@ class Main extends React.Component { !!currentWindow.get('braveryPanelDetail') props.showClearData = currentWindow.getIn(['ui', 'isClearBrowsingDataPanelVisible'], false) props.showImportData = currentWindow.has('importBrowserDataDetail') - props.showWidevine = currentWindow.getIn(['widevinePanelDetail', 'shown']) && !isLinux + props.showWidevine = currentWindow.getIn(['widevinePanelDetail', 'shown']) && !isLinux && !isTor props.showAutoFillAddress = currentWindow.has('autofillAddressDetail') props.showAutoFillCC = currentWindow.has('autofillCreditCardDetail') props.showLogin = !!loginRequiredDetails diff --git a/app/renderer/components/navigation/urlBarIcon.js b/app/renderer/components/navigation/urlBarIcon.js index 90f2ae8935c..e67bf582f8f 100644 --- a/app/renderer/components/navigation/urlBarIcon.js +++ b/app/renderer/components/navigation/urlBarIcon.js @@ -28,7 +28,7 @@ const dndData = require('../../../../js/dndData') const UrlUtil = require('../../../../js/lib/urlutil') const frameStateUtil = require('../../../../js/state/frameStateUtil') const {isSourceAboutUrl} = require('../../../../js/lib/appUrlUtil') -const {isPotentialPhishingUrl} = require('../../../../js/lib/urlutil') +const {isPotentialPhishingUrl, isOnionUrl} = require('../../../../js/lib/urlutil') class UrlBarIcon extends React.Component { constructor (props) { @@ -92,6 +92,7 @@ class UrlBarIcon extends React.Component { props.activeTabShowingMessageBox = tabState.isShowingMessageBox(state, activeTabId) props.isAboutPage = isSourceAboutUrl(props.location) && props.location !== 'about:newtab' props.isPotentialPhishingUrl = isPotentialPhishingUrl(props.location) + props.isOnionUrl = isOnionUrl(props.location) // used in other functions props.title = activeFrame.get('title', '') @@ -131,6 +132,8 @@ class UrlBarIcon extends React.Component { icon = iconTestId = 'isSecure' isExtendedSecure = this.props.isSecureWithEVCert + } else if (this.props.isOnionUrl) { + iconTestId = 'isInsecureOnion' } else if (this.props.isSecure === 1) { icon = iconTestId = 'isInsecure' diff --git a/app/renderer/components/tabs/content/favIcon.js b/app/renderer/components/tabs/content/favIcon.js index d06bd13bb31..da5bc1ad359 100644 --- a/app/renderer/components/tabs/content/favIcon.js +++ b/app/renderer/components/tabs/content/favIcon.js @@ -23,6 +23,17 @@ const globalStyles = require('../../styles/global') const {theme} = require('../../styles/theme') const {opacityIncreaseElementKeyframes} = require('../../styles/animations') +const isLocalFavicon = (favicon) => { + if (!favicon) { + return true + } + favicon = favicon.toLowerCase() + return favicon.startsWith('data:') || + favicon.startsWith('chrome:') || + favicon.startsWith('chrome-extension://') || + favicon.startsWith('file://') +} + class Favicon extends React.Component { constructor (props) { super(props) @@ -33,9 +44,11 @@ class Favicon extends React.Component { const currentWindow = state.get('currentWindow') const tabId = ownProps.tabId const frameKey = frameStateUtil.getFrameKeyByTabId(currentWindow, tabId) + const frame = frameStateUtil.getFrameByKey(currentWindow, frameKey) const props = {} props.isPinned = tabState.isTabPinned(state, tabId) + props.isTor = frameStateUtil.isTor(frame) props.favicon = faviconState.getFavicon(currentWindow, frameKey) props.showIcon = faviconState.showFavicon(currentWindow, frameKey) props.tabLoading = faviconState.showLoadingIcon(currentWindow, frameKey) @@ -90,7 +103,8 @@ class Favicon extends React.Component { const themeLight = this.props.tabIconColor === 'white' const instanceStyles = { } - if (this.props.favicon) { + if (this.props.favicon && (!this.props.isTor || isLocalFavicon(this.props.favicon))) { + // Ensure that remote favicons do not load in Tor mode instanceStyles['--faviconsrc'] = `url(${this.props.favicon})` } diff --git a/app/renderer/rendererShortcutHandler.js b/app/renderer/rendererShortcutHandler.js index da374918208..b35fa34e477 100644 --- a/app/renderer/rendererShortcutHandler.js +++ b/app/renderer/rendererShortcutHandler.js @@ -148,8 +148,14 @@ function handleShortcut (frameKey, shortcut, e, args) { break } case 'clean-reload': { + const frame = frameStateUtil.getFrameByKey(windowStore.state, frameKey) const tabId = frameStateUtil.getTabIdByFrameKey(windowStore.state, frameKey) - tabActions.reload(tabId, true) + if (frameStateUtil.isTor(frame)) { + // set new tor circuit + appActions.setTorNewIdentity(tabId, frame.get('location')) + } else { + tabActions.reload(tabId, true) + } break } case 'save': { diff --git a/app/renderer/rendererTabEvents.js b/app/renderer/rendererTabEvents.js index 20fc109010a..27b8b003771 100644 --- a/app/renderer/rendererTabEvents.js +++ b/app/renderer/rendererTabEvents.js @@ -110,6 +110,10 @@ const api = module.exports = { break } case 'page-favicon-updated': { + if (frameStateUtil.isTor(frame)) { + // This will be set as a data: URL by the page content script + break + } if (e.favicons && e.favicons.length > 0 && // Favicon changes lead to recalculation of top site data so only fire @@ -347,6 +351,22 @@ function handleTabIpcMessage (tabId, frame, e) { method = () => windowActions.setContextMenuDetail() break } + case messages.RECREATE_TOR_TAB: { + const tab = getTab(tabId) + method = (torEnabled) => { + appActions.recreateTorTab(torEnabled, tabId, + tab ? tab.get('index') : undefined) + } + break + } + case messages.GOT_PAGE_FAVICON: { + method = (dataUrl) => { + if (frameStateUtil.isTor(frame)) { + windowActions.setFavicon(frame, dataUrl) + } + } + break + } } method.apply(null, e.args) } diff --git a/docs/state.md b/docs/state.md index 87151dae4c4..5cc294b9639 100644 --- a/docs/state.md +++ b/docs/state.md @@ -33,7 +33,8 @@ AppStore ignoredTopSites: [string], // list of ignored sites pinnedTopSites: [string], // list of pinned sites to be used on gridLayout. Defaults to 1 Brave-related site; see data/newTabData.js => pinnedTopSites sites: [string], // list of sites to be used on gridLayout. Defaults to 6 Brave-related sites; see data/newTabData.js => topSites - updatedStamp: number // timestamp for when the data was last updated + updatedStamp: number, // timestamp for when the data was last updated + torEnabled: boolean, // whether Tor private tabs is enabled }, preferences: { backupNotifyCount: number, // number of times user has been reminded to backup wallet diff --git a/js/about/aboutActions.js b/js/about/aboutActions.js index fe511ca4072..0511923a12f 100644 --- a/js/about/aboutActions.js +++ b/js/about/aboutActions.js @@ -194,6 +194,14 @@ const aboutActions = { ipc.sendToHost(messages.CONTEXT_MENU_OPENED, nodeProps, contextMenuType) }, + /** + * Emitted when tor tab setting is changed + * @param {boolean} torEnabled - Whether tor is enabled for that tab + */ + recreateTorTab: function (torEnabled) { + ipc.sendToHost(messages.RECREATE_TOR_TAB, torEnabled) + }, + downloadRevealed: function (downloadId) { aboutActions.dispatchAction({ actionType: appConstants.APP_DOWNLOAD_REVEALED, diff --git a/js/about/newprivatetab.js b/js/about/newprivatetab.js index 6f40256c3ef..f7f2c6de374 100644 --- a/js/about/newprivatetab.js +++ b/js/about/newprivatetab.js @@ -6,6 +6,7 @@ const React = require('react') const {StyleSheet, css} = require('aphrodite') const privateTabIcon = require('../../app/extensions/brave/img/newtab/private_tab_pagearea_icon.svg') const ddgIcon = require('../../app/extensions/brave/img/newtab/private_tab_pagearea_ddgicon.svg') +const torIcon = require('../../app/extensions/brave/img/newtab/toricon.svg') const globalStyles = require('../../app/renderer/components/styles/global') const { theme } = require('../../app/renderer/components/styles/theme') const {SettingCheckbox} = require('../../app/renderer/components/common/settings') @@ -18,17 +19,28 @@ const aboutActions = require('./aboutActions') require('../../less/about/newtab.less') const useAlternativePrivateSearchEngineDataKeys = ['newTabDetail', 'useAlternativePrivateSearchEngine'] +const torEnabled = ['newTabDetail', 'torEnabled'] class NewPrivateTab extends React.Component { onChangePrivateSearch (e) { aboutActions.changeSetting(settings.USE_ALTERNATIVE_PRIVATE_SEARCH_ENGINE, e.target.value) } + onChangeTor (e) { + aboutActions.changeSetting(settings.USE_TOR_PRIVATE_TABS, e.target.value) + aboutActions.recreateTorTab(e.target.value) + } + onClickPrivateSearchTitle () { const newSettingValue = !this.props.newTabData.getIn(useAlternativePrivateSearchEngineDataKeys) aboutActions.changeSetting(settings.USE_ALTERNATIVE_PRIVATE_SEARCH_ENGINE, newSettingValue) } + onClickTorTitle () { + const newSettingValue = !this.props.newTabData.getIn(torEnabled) + aboutActions.changeSetting(settings.USE_TOR_PRIVATE_TABS, newSettingValue) + } + render () { if (!this.props.newTabData) { return null @@ -39,15 +51,19 @@ class NewPrivateTab extends React.Component {
-

-

-

{ this.props.newTabData.hasIn(useAlternativePrivateSearchEngineDataKeys) &&

+ DuckDuckGo logo + +

+ +

+

+ -

- - DuckDuckGo -

- DuckDuckGo logo
-

+

+ } + { +
+
+ Tor logo + +

+ +

+

+ + +

}

+
+

+

+

+

+

} } @@ -113,13 +149,12 @@ const styles = StyleSheet.create({ }, section_privateTab: { - margin: '20px 0 0 0' + margin: '0 0 10px 70px' }, wrapper: { fontFamily: globalStyles.typography.body.family, display: 'flex', - alignSelf: 'center', maxWidth: '780px', [atBreakpointIconGutter]: { @@ -129,7 +164,6 @@ const styles = StyleSheet.create({ textWrapper: { fontFamily: 'inherit', - marginLeft: '25px', marginBottom: 0, [atBreakpointIconGutter]: { padding: '14px 0', @@ -172,10 +206,10 @@ const styles = StyleSheet.create({ }, text: { - lineHeight: '1.4', - fontSize: '18px', + lineHeight: '1.5', + fontSize: '17px', color: globalStyles.color.alphaWhite, - maxWidth: '544px', + maxWidth: '800px', fontFamily: 'inherit', ':not(:last-of-type)': { paddingBottom: '20px' @@ -187,11 +221,6 @@ const styles = StyleSheet.create({ lineHeight: '1.5' }, - text_thirdPartyNote: { - fontStyle: 'italic', - fontSize: '15px' - }, - text_sectionTitle: { fontFamily: globalStyles.typography.display.family, fontSize: 'var(--private-tab-section-title-font-size)', @@ -206,7 +235,10 @@ const styles = StyleSheet.create({ }, privateSearch: { - marginTop: '40px' + border: 'solid 2px', + borderRadius: '10px', + padding: '20px', + marginBottom: '10px' }, privateSearch__setting: { @@ -216,17 +248,23 @@ const styles = StyleSheet.create({ }, privateSearch__ddgImage: { - width: 'auto', - height: 'var(--private-tab-section-title-logo-height)' + width: '82px', + marginRight: '20px' + }, + + privateSearch__torImage: { + width: '70px', + marginRight: '14px' }, privateSearch__switch: { - marginRight: '14px', + marginLeft: '14px', padding: 0, cursor: 'pointer' }, privateSearch__title: { + maxWidth: '800px', whiteSpace: 'nowrap', marginRight: '18px', display: 'flex', diff --git a/js/about/newtab.js b/js/about/newtab.js index c35689bfaf8..8f549e40f19 100644 --- a/js/about/newtab.js +++ b/js/about/newtab.js @@ -261,6 +261,7 @@ class NewTabPage extends React.Component { return
} + // TODO: use this.props.isIncognito when muon supports it for tor tabs if (this.props.isIncognito) { return } diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 6ac932cf4eb..f33b5b7e4ac 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -2050,6 +2050,23 @@ const appActions = { index, windowId }) + }, + + setTorNewIdentity: function (tabId, url) { + dispatch({ + actionType: appConstants.APP_SET_TOR_NEW_IDENTITY, + tabId, + url + }) + }, + + recreateTorTab: function (torEnabled, tabId, index) { + dispatch({ + actionType: appConstants.APP_RECREATE_TOR_TAB, + torEnabled, + tabId, + index + }) } } diff --git a/js/constants/appConfig.js b/js/constants/appConfig.js index a825d192838..8c0e1079257 100644 --- a/js/constants/appConfig.js +++ b/js/constants/appConfig.js @@ -105,6 +105,9 @@ module.exports = { delayNotificationTryPayments: 1000 * 60 * 60 * 24 * 10, // 10 days (from firstRunTimestamp) defaultContributionAmount: 7.5 }, + tor: { + partition: 'persist:tor' + }, updates: { // Check for front end updates every hour appUpdateCheckFrequency: 1000 * 60 * 60, @@ -148,7 +151,7 @@ module.exports = { 'general.spellcheck-languages': Immutable.fromJS(['en-US']), 'search.default-search-engine': 'Google', 'search.offer-search-suggestions': false, // false by default for privacy reasons - 'search.use-alternate-private-search-engine': false, // use true for DDG search in Private Tab + 'search.use-alternate-private-search-engine': true, // true for DDG search in Private Tab since Tor is enabled 'tabs.switch-to-new-tabs': false, 'tabs.paint-tabs': true, 'tabs.tabs-per-page': 20, @@ -171,6 +174,7 @@ module.exports = { 'security.autoplay.media': autoplayOption.ALWAYS_ALLOW, 'security.flash.installed': false, 'security.site-isolation-enabled': false, + 'tor.private-tabs.enabled': true, 'shields.blocked-count-badge': true, 'shields.compact-bravery-panel': false, // sync diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index 29834ffdb9c..f5c20e16dbe 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -208,7 +208,9 @@ const appConstants = { APP_ADD_PUBLISHER_TO_LEDGER: _, APP_ON_WALLET_DELETE: _, APP_ON_WALLET_PROPERTIES_ERROR: _, - APP_ON_PUBLISHER_TOGGLE_UPDATE: _ + APP_ON_PUBLISHER_TOGGLE_UPDATE: _, + APP_SET_TOR_NEW_IDENTITY: _, + APP_RECREATE_TOR_TAB: _ } module.exports = mapValuesByKeys(appConstants) diff --git a/js/constants/config.js b/js/constants/config.js index c16d3aa3207..f570748c211 100644 --- a/js/constants/config.js +++ b/js/constants/config.js @@ -19,6 +19,7 @@ module.exports = { zoomLevels: [-3.75, -3.35, -2.5, -1.65, -1.25, -0.5, -0.25, 0, 0.25, 0.5, 1.25, 2.5, 3.75, 5, 7.5, 10, 15, 20] }, fingerprintingInfoUrl: 'https://github.com/brave/browser-laptop/wiki/Fingerprinting-Protection-Mode', + torCircuitInfoUrl: 'https://github.com/brave/browser-laptop/wiki/Brave-Tor-Support#what-does-the-new-tor-circuit-for-this-site-button-do', maxClosedFrames: 100, menu: { // History -> Recently closed frame list diff --git a/js/constants/messages.js b/js/constants/messages.js index 66f80436c47..fd4f7836c62 100644 --- a/js/constants/messages.js +++ b/js/constants/messages.js @@ -76,6 +76,8 @@ const messages = { DETACH: _, PASSWORD_DETAILS_UPDATED: _, /** @arg {Object} passwords app state */ PASSWORD_SITE_DETAILS_UPDATED: _, /** @arg {Object} passwords app state */ + RECREATE_TOR_TAB: _, /** @arg {boolean} torEnabled */ + GOT_PAGE_FAVICON: _, /** @arg {string} dataUrl */ // Init INITIALIZE_WINDOW: _, // Session restore diff --git a/js/constants/settings.js b/js/constants/settings.js index 6b37f57b5d6..4e9a92535f2 100644 --- a/js/constants/settings.js +++ b/js/constants/settings.js @@ -102,6 +102,8 @@ const settings = { // Debug settings DEBUG_ALLOW_MANUAL_TAB_DISCARD: 'debug.manual-tab-discard.enabled', DEBUG_VERBOSE_TAB_INFO: 'debug.verbose-tab-info.enabled', + // Tor settings + USE_TOR_PRIVATE_TABS: 'tor.private-tabs.enabled', // DEPRECATED settings // DO NOT REMOVE OR CHANGE THESE VALUES diff --git a/js/flash.js b/js/flash.js index 3d044269ac2..e98932a25ef 100644 --- a/js/flash.js +++ b/js/flash.js @@ -163,6 +163,9 @@ module.exports.onFlashContextMenu = (state, tabId) => { if (!tab) { return } + if (tabState.isIncognito(state, tabId) && getSetting(settings.USE_TOR_PRIVATE_TABS)) { + return + } const flashMenu = Menu.buildFromTemplate(flashMenuTemplateInit(state, tabId)) flashMenu.popup(tab) diff --git a/js/lib/request.js b/js/lib/request.js index 0bbd21f1f1d..32903b14c0e 100644 --- a/js/lib/request.js +++ b/js/lib/request.js @@ -31,11 +31,12 @@ const getDefaultSession = () => { * Depends on there being a loaded browser window available. * @param {Object|string} options - options object or URL to load * @param {function.} callback + * @param {Object=} session - muon session to use if not the default */ -module.exports.request = (options, callback) => { +module.exports.request = (options, callback, session) => { var params var responseType = options.responseType || 'text' - var defaultSession = getDefaultSession() + var defaultSession = session || getDefaultSession() if (!defaultSession) return callback(new Error('Request failed, no session available')) diff --git a/js/lib/urlutil.js b/js/lib/urlutil.js index bc53690a232..8bb72f71642 100644 --- a/js/lib/urlutil.js +++ b/js/lib/urlutil.js @@ -498,6 +498,20 @@ const UrlUtil = { return url .replace(/((#?\/?)|(\/#?))$/, '') // remove trailing # and / .trim() // remove whitespaces + }, + + /** + * Whether a site is a Tor Hidden Service .onion URL + * @param {string} url + * @return {boolean} + */ + isOnionUrl: (url) => { + if (typeof url !== 'string') { return false } + const hostname = urlParse(url).hostname + if (!hostname) { + return false + } + return hostname.endsWith('.onion') } } diff --git a/js/state/frameStateUtil.js b/js/state/frameStateUtil.js index f8069d97dc7..43d50001fb3 100644 --- a/js/state/frameStateUtil.js +++ b/js/state/frameStateUtil.js @@ -6,6 +6,7 @@ const Immutable = require('immutable') // Constants const config = require('../constants/config') +const appConfig = require('../constants/appConfig') const settings = require('../constants/settings') // Actions @@ -299,10 +300,6 @@ function getPartitionNumber (partition) { return Number((matches && matches[1]) || 0) } -function isPrivatePartition (partition) { - return partition && !partition.startsWith('persist:') -} - function isSessionPartition (partition) { return partition && partition.startsWith('persist:partition-') } @@ -487,6 +484,13 @@ const isFirstFrameKeyInTabPage = (state, frameKey) => { return firstFrame && firstFrame.get('key') === frameKey } +/** + * Check if frame or tab object is associated with a tor private tab + */ +function isTor (frame) { + return !!(frame && frame.get('partition') === appConfig.tor.partition) +} + const getTabPageIndex = (state) => { const tabPageIndex = state.getIn(['ui', 'tabs', 'tabPageIndex'], 0) const previewTabPageIndex = state.getIn(['ui', 'tabs', 'previewTabPageIndex']) @@ -771,7 +775,6 @@ module.exports = { getHistory, isFrameKeyPinned, getNonPinnedFrameCount, - isPrivatePartition, isSessionPartition, getFrames, getFrameKeys, @@ -808,6 +811,7 @@ module.exports = { onFindBarHide, getTotalBlocks, isPinned, + isTor, isFirstFrameKeyInTabPage, getTabPageIndex, updateTabPageIndex, diff --git a/js/state/userPrefs.js b/js/state/userPrefs.js index 13f1aa10254..0c99d14438f 100644 --- a/js/state/userPrefs.js +++ b/js/state/userPrefs.js @@ -2,9 +2,12 @@ * 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/. */ +const appConfig = require('../constants/appConfig') + let registeredCallbacks = [] let registeredSessions = {} let registeredPrivateSessions = {} +const blockContentSetting = { setting: 'block', primaryPattern: '*' } // TODO(bridiver) move this to electron so we can call a simpler api const setUserPrefType = (ses, path, value) => { @@ -54,8 +57,16 @@ module.exports.setUserPref = (path, value, incognito = false) => { const partitions = incognito ? registeredPrivateSessions : registeredSessions for (let partition in partitions) { + let newValue = value + if (partition === appConfig.tor.partition && path === 'content_settings' && value) { + newValue = Object.assign({}, value, { + flashEnabled: [blockContentSetting], + flashAllowed: [blockContentSetting], + plugins: [blockContentSetting] + }) + } const ses = partitions[partition] - setUserPrefType(ses, path, value) + setUserPrefType(ses, path, newValue) ses.webRequest.handleBehaviorChanged() } } diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 3104175102a..19635933df8 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -401,6 +401,9 @@ 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()) diff --git a/js/stores/windowStore.js b/js/stores/windowStore.js index c9da2edcceb..88c7a4a544d 100644 --- a/js/stores/windowStore.js +++ b/js/stores/windowStore.js @@ -129,7 +129,6 @@ const newFrame = (state, frameOpts) => { } if (frameOpts.partition) { - frameOpts.isPrivate = frameStateUtil.isPrivatePartition(frameOpts.partition) if (frameStateUtil.isSessionPartition(frameOpts.partition)) { frameOpts.partitionNumber = frameStateUtil.getPartitionNumber(frameOpts.partition) } diff --git a/package.json b/package.json index afb50071364..20b025aaf44 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "flow": "flow; test $? -eq 0 -o $? -eq 2", "lint": "standard --verbose | snazzy", "lint-standard": "standard", - "postinstall": "npm run download-sync-client && webpack", + "package-tor": "node ./tools/package_tor.js", + "postinstall": "npm run package-tor && npm run download-sync-client && webpack", "pre-push-tests": "if [ -z $NO_PUSH_TESTS ]; then npm run unittest; fi", "start-log": "node ./tools/start.js --user-data-dir-name=brave-development --enable-logging=stderr --v=1 --enable-extension-activity-logging --enable-sandbox-logging --enable-dcheck", "start": "node ./tools/start.js --user-data-dir-name=brave-development --enable-logging --v=0 --enable-extension-activity-logging --enable-sandbox-logging --enable-dcheck", @@ -196,6 +197,7 @@ "standard": "10.0.3", "style-loader": "~0.19.0", "uglifyjs-webpack-plugin": "^1.0.0", + "unzip": "^0.1.11", "uuid": "^3.0.1", "webdriverio": "4.7.1", "webpack": "^3.8.1", diff --git a/test/unit/about/newTabPageTest.js b/test/unit/about/newTabPageTest.js index 03afa5876fa..a507e539502 100644 --- a/test/unit/about/newTabPageTest.js +++ b/test/unit/about/newTabPageTest.js @@ -30,6 +30,7 @@ describe('NewTab component unit tests', function () { mockery.registerMock('../../fonts') mockery.registerMock('../../app/extensions/brave/img/newtab/private_tab_pagearea_icon.svg') mockery.registerMock('../../app/extensions/brave/img/newtab/private_tab_pagearea_ddgicon.svg') + mockery.registerMock('../../app/extensions/brave/img/newtab/toricon.svg') randomSpy = sinon.spy(randomWrapper, 'random') mockery.registerMock('../../app/common/lib/randomUtil', randomWrapper) window.chrome = fakeElectron diff --git a/test/unit/app/renderer/reducers/urlBarReducerTest.js b/test/unit/app/renderer/reducers/urlBarReducerTest.js index 674cdfe60a4..b6c7405ac8f 100644 --- a/test/unit/app/renderer/reducers/urlBarReducerTest.js +++ b/test/unit/app/renderer/reducers/urlBarReducerTest.js @@ -12,6 +12,8 @@ const tabActionConsts = require('../../../../../app/common/constants/tabAction') require('../../../braveUnit') +let getSettingsValue = true + const windowState = Immutable.fromJS({ activeFrameKey: 2, searchDetail: {}, @@ -111,6 +113,9 @@ describe('urlBarReducer', function () { mockery.registerMock('electron', fakeElectron) mockery.registerMock('../../../js/stores/appStoreRenderer', fakeAppStoreRenderer) mockery.registerMock('./stores/appStoreRenderer', fakeAppStoreRenderer) + mockery.registerMock('../../../js/settings', { getSetting: (settingKey) => { + return getSettingsValue + }}) urlBarReducer = require('../../../../../app/renderer/reducers/urlBarReducer') }) after(function () { @@ -215,11 +220,6 @@ describe('urlBarReducer', function () { }) mockery.registerMock('electron', fakeElectron) mockery.registerMock('../../../js/stores/appStoreRenderer', fakeAppStoreRenderer) - mockery.registerMock('../../../js/settings', { getSetting: (settingKey, settingsCollection, value) => { - switch (settingKey) { - default: return true - } - }}) this.suggestionClickHandlers = { navigateSiteClickHandler: sinon.mock() } @@ -264,10 +264,17 @@ describe('urlBarReducer', function () { const hasCustomSearchProvider = newState.hasIn(['frames', 1, 'navbar', 'urlbar', 'searchDetail']) assert.equal(hasCustomSearchProvider, false) }) - it('searches using default search provider in Private Tabs', () => { + it('searches using custom search provider in Private Tabs with Tor enabled', () => { + const newState = urlBarReducer(stateWithPrivateTab, action) + const hasCustomSearchProvider = newState.hasIn(['frames', 1, 'navbar', 'urlbar', 'searchDetail']) + assert.equal(hasCustomSearchProvider, true) + }) + it('searches using default search provider in Private Tabs without Tor enabled', () => { + getSettingsValue = false const newState = urlBarReducer(stateWithPrivateTab, action) const hasCustomSearchProvider = newState.hasIn(['frames', 1, 'navbar', 'urlbar', 'searchDetail']) assert.equal(hasCustomSearchProvider, false) + getSettingsValue = true }) it('searches using Private Search provider in Private Tabs, with relevant setting value', () => { diff --git a/test/unit/lib/fakeElectron.js b/test/unit/lib/fakeElectron.js index 19d1664d535..e7af97252c6 100644 --- a/test/unit/lib/fakeElectron.js +++ b/test/unit/lib/fakeElectron.js @@ -81,7 +81,17 @@ const fakeElectron = { } } }, - fromPartition: () => {} + fromPartition: function (partition) { + if (!partition.startsWith('persist:') || partition === 'tor') { + return { + isOffTheRecord: function () { return true } + } + } else { + return { + isOffTheRecord: function () { return false } + } + } + } }, extensions: { createTab: function () {} diff --git a/test/unit/lib/urlutilTest.js b/test/unit/lib/urlutilTest.js index ded9fc557a1..1c5406f48e7 100644 --- a/test/unit/lib/urlutilTest.js +++ b/test/unit/lib/urlutilTest.js @@ -519,4 +519,23 @@ describe('urlutil', function () { assert.equal(result, 'http://www.test.com/test.pdf') }) }) + + describe('isOnionUrl', function () { + it('null url', function () { + const result = urlUtil.isOnionUrl(null) + assert.equal(result, false) + }) + it('regular url', function () { + const result = urlUtil.isOnionUrl('http://bing.com') + assert.equal(result, false) + }) + it('onion url', function () { + const result = urlUtil.isOnionUrl('https://facebookcorewwwi.onion/') + assert.equal(result, true) + }) + it('weird onion url', function () { + const result = urlUtil.isOnionUrl('hTtpS://ABCDEF.onioN/?test=1#abc') + assert.equal(result, true) + }) + }) }) diff --git a/test/unit/state/frameStateUtilTest.js b/test/unit/state/frameStateUtilTest.js index 10f7fe6cf4e..d5bcc40818b 100644 --- a/test/unit/state/frameStateUtilTest.js +++ b/test/unit/state/frameStateUtilTest.js @@ -492,4 +492,22 @@ describe('frameStateUtil', function () { assert.equal(result, true) }) }) + + describe('isTor', function () { + const frame1 = Immutable.Map({partition: 'persist:tor'}) + const frame2 = Immutable.Map({partition: ''}) + const frame3 = Immutable.Map({foobar: false}) + it('null frame case', function () { + assert.equal(frameStateUtil.isTor(null), false) + }) + it('other frame case', function () { + assert.equal(frameStateUtil.isTor(frame3), false) + }) + it('regular frame case', function () { + assert.equal(frameStateUtil.isTor(frame2), false) + }) + it('tor frame case', function () { + assert.equal(frameStateUtil.isTor(frame1), true) + }) + }) }) diff --git a/tools/buildPackage.js b/tools/buildPackage.js index 2978ebf147e..924b1a477b4 100644 --- a/tools/buildPackage.js +++ b/tools/buildPackage.js @@ -158,6 +158,15 @@ if (isLinux) { cmds.push(`"node_modules/rcedit/bin/rcedit.exe" ./${appName}-win32-` + arch + `/${appName}.exe --set-version-string "SquirrelAwareVersion" "1"`) } +// Verify tor binaries and bundle with Brave +var torPath +if (isDarwin) { + torPath = path.join(buildDir, `${appName}.app`, 'Contents', 'Resources', 'extensions', 'bin') +} else { + torPath = path.join(buildDir, 'resources', 'extensions', 'bin') +} +cmds.push('npm run package-tor ' + torPath) + if (isDarwin) { const macAppName = `${appName}.app` cmds.push('mkdirp ' + path.join(buildDir, macAppName, 'Contents', 'Resources', 'app.asar.unpacked', 'node_modules', 'node-anonize2-relic-emscripten')) diff --git a/tools/cibuild.py b/tools/cibuild.py index f3c8318038d..f4cba60310a 100755 --- a/tools/cibuild.py +++ b/tools/cibuild.py @@ -4,7 +4,7 @@ import subprocess import sys import os.path -MUON_VERSION = '7.0.6' +MUON_VERSION = '6.1.0' CHROMEDRIVER_VERSION = '2.37' SOURCE_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) TARGET_ARCH= os.environ['TARGET_ARCH'] if os.environ.has_key('TARGET_ARCH') else 'x64' diff --git a/tools/package_tor.js b/tools/package_tor.js new file mode 100644 index 00000000000..4eb3023fe67 --- /dev/null +++ b/tools/package_tor.js @@ -0,0 +1,68 @@ +var execute = require('./lib/execute') +const path = require('path') +const fs = require('fs') +const unzip = require('unzip') + +var cmds = [] + +const isWindows = process.platform === 'win32' +const isDarwin = process.platform === 'darwin' +const isLinux = process.platform === 'linux' + +const torS3Prefix = 'https://s3.us-east-2.amazonaws.com/demo-tor-binaries/' +var torPath = process.argv.slice(2)[0] // npm run package-tor torPath +if (torPath === undefined) { + torPath = path.join('app', 'extensions', 'bin') +} + +var torVersion = '0.3.2.10' +var braveVersion = '1' +var torURL = torS3Prefix + 'tor-' + torVersion + '-' + process.platform + '-brave-' + braveVersion + +if (isWindows) { + torURL += '.zip' +} + +// mkdir -p doesn't work well with Windows +if (!fs.existsSync(torPath)) { + fs.mkdirSync(torPath) +} + +var sha512Tor +if (isDarwin) { + sha512Tor = 'af09b7f31fcb3996db3883dff0c20187fdb9653fb5d207dc7e8d3d8cd9de90f2908e47f4414c03b5633a94183425e0c9804316ece353f8174ec4897559932a4e' +} else if (isLinux) { + sha512Tor = '8e78aa316aef74b6c68983cc3a82ec77e0805001faa1bc0bcf6458227af56929cdf9bd6d48626026aba5a48c82a71eaab15588a6c744b02c2e0be990e73e034b' +} else { + sha512Tor = '0fbd88d590069fdc243fcdcc84b8aa10caa921fc11d7e776334f0cbd5b0095b21046adfd291be61dc414c232bc1ff22e7e8142b0af1d20e71154f8de66be83ab' +} + +// Windows adds " " to the file, in mac/linux " " preserves the spaces between +// sha and file path +cmds.push('curl -o ' + path.join(torPath, 'tor') + ' ' + torURL) +if (isWindows) { + cmds.push('echo ' + sha512Tor + ' ' + path.join(torPath, 'tor') + '> tor.hash') +} else { + cmds.push('echo "' + sha512Tor + ' ' + path.join(torPath, 'tor') + '" > tor.hash') +} + +if (isDarwin) { + cmds.push('shasum -a 512 -c tor.hash') +} else { + cmds.push('sha512sum -c tor.hash') +} + +if (!isWindows) { + cmds.push('chmod +x ' + path.join(torPath, 'tor')) +} +cmds.push('rm -f tor.hash') +execute(cmds, '', (err) => { + if (err) { + console.error('package tor failed', err) + process.exit(1) + } + if (isWindows) { + fs.createReadStream(path.join(torPath, 'tor')).pipe(unzip.Extract({ path: torPath })) + } + console.log('done') +})