From cc50cd39291188866d458907464349675cef08e2 Mon Sep 17 00:00:00 2001 From: Alex Catarineu Date: Thu, 13 Feb 2020 13:24:33 +0100 Subject: [PATCH] Bug 28005: Implement .onion alias urlbar rewrites A custom HTTPS Everywhere update channel is installed, which provides rules for locally redirecting some memorable .tor.onion URLs to non-memorable .onion URLs. When these redirects occur, we also rewrite the URL in the urlbar to display the human-memorable hostname instead of the actual .onion. --- browser/actors/ClickHandlerChild.jsm | 20 ++ browser/actors/ContextMenuChild.jsm | 4 + browser/base/content/browser-places.js | 12 +- browser/base/content/browser.js | 37 +++- browser/base/content/nsContextMenu.js | 18 ++ browser/base/content/tabbrowser.js | 7 + browser/base/content/utilityOverlay.js | 2 + browser/components/BrowserGlue.jsm | 6 + .../onionservices/ExtensionMessaging.jsm | 86 ++++++++ .../onionservices/HttpsEverywhereControl.jsm | 119 +++++++++++ .../onionservices/OnionAliasStore.jsm | 199 ++++++++++++++++++ browser/components/onionservices/moz.build | 3 + browser/components/urlbar/UrlbarInput.jsm | 8 +- browser/modules/ContentClick.jsm | 1 + docshell/base/nsDocShell.cpp | 37 ++++ docshell/base/nsDocShell.h | 4 + docshell/base/nsDocShellLoadState.cpp | 4 + docshell/base/nsIDocShell.idl | 5 + docshell/base/nsIWebNavigation.idl | 5 + docshell/shistory/nsISHEntry.idl | 5 + docshell/shistory/nsSHEntry.cpp | 18 +- docshell/shistory/nsSHEntry.h | 1 + modules/libpref/init/StaticPrefList.h | 7 + netwerk/dns/effective_tld_names.dat | 2 + .../RemoteWebNavigation.jsm | 1 + .../content/widgets/browser-custom-element.js | 9 + toolkit/modules/RemoteWebProgress.jsm | 2 + toolkit/modules/WebProgressChild.jsm | 1 + .../modules/sessionstore/SessionHistory.jsm | 5 + 29 files changed, 619 insertions(+), 9 deletions(-) create mode 100644 browser/components/onionservices/ExtensionMessaging.jsm create mode 100644 browser/components/onionservices/HttpsEverywhereControl.jsm create mode 100644 browser/components/onionservices/OnionAliasStore.jsm diff --git a/browser/actors/ClickHandlerChild.jsm b/browser/actors/ClickHandlerChild.jsm index 4375be0067ac0..263a5fea053e9 100644 --- a/browser/actors/ClickHandlerChild.jsm +++ b/browser/actors/ClickHandlerChild.jsm @@ -156,6 +156,26 @@ class ClickHandlerChild extends ActorChild { json.originPrincipal = ownerDoc.nodePrincipal; json.triggeringPrincipal = ownerDoc.nodePrincipal; + // Check if the link needs to be opened with .tor.onion urlbar rewrites + // allowed. Only when the owner doc has allowOnionUrlbarRewrites = true + // and the same origin we should allow this. + json.allowOnionUrlbarRewrites = false; + if (this.mm.docShell.allowOnionUrlbarRewrites) { + const sm = Services.scriptSecurityManager; + try { + let targetURI = Services.io.newURI(href); + let isPrivateWin = + ownerDoc.nodePrincipal.originAttributes.privateBrowsingId > 0; + sm.checkSameOriginURI( + docshell.currentDocumentChannel.URI, + targetURI, + false, + isPrivateWin + ); + json.allowOnionUrlbarRewrites = true; + } catch (e) {} + } + // If a link element is clicked with middle button, user wants to open // the link somewhere rather than pasting clipboard content. Therefore, // when it's clicked with middle button, we should prevent multiple diff --git a/browser/actors/ContextMenuChild.jsm b/browser/actors/ContextMenuChild.jsm index 546f51e843d67..5915832eae7a2 100644 --- a/browser/actors/ContextMenuChild.jsm +++ b/browser/actors/ContextMenuChild.jsm @@ -543,6 +543,9 @@ class ContextMenuChild extends ActorChild { // The same-origin check will be done in nsContextMenu.openLinkInTab. let parentAllowsMixedContent = !!this.docShell.mixedContentChannel; + let parentAllowsOnionUrlbarRewrites = this.docShell + .allowOnionUrlbarRewrites; + // Get referrer attribute from clicked link and parse it let referrerAttrValue = Services.netUtils.parseAttributePolicyString( aEvent.composedTarget.getAttribute("referrerpolicy") @@ -659,6 +662,7 @@ class ContextMenuChild extends ActorChild { popupNodeSelectors, disableSetDesktopBg, parentAllowsMixedContent, + parentAllowsOnionUrlbarRewrites, }; Services.obs.notifyObservers( diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js index 6aa348c609a4f..a2dd38a975142 100755 --- a/browser/base/content/browser-places.js +++ b/browser/base/content/browser-places.js @@ -440,7 +440,8 @@ var PlacesCommandHook = { */ async bookmarkPage() { let browser = gBrowser.selectedBrowser; - let url = new URL(browser.currentURI.spec); + const uri = browser.currentOnionAliasURI || browser.currentURI; + let url = new URL(uri.spec); let info = await PlacesUtils.bookmarks.fetch({ url }); let isNewBookmark = !info; let showEditUI = !isNewBookmark || StarUI.showForNewBookmarks; @@ -544,7 +545,7 @@ var PlacesCommandHook = { tabs.forEach(tab => { let browser = tab.linkedBrowser; - let uri = browser.currentURI; + let uri = browser.currentOnionAliasURI || browser.currentURI; let title = browser.contentTitle || tab.label; let spec = uri.spec; if (!(spec in uniquePages)) { @@ -1618,14 +1619,17 @@ var BookmarkingUI = { }, onLocationChange: function BUI_onLocationChange() { - if (this._uri && gBrowser.currentURI.equals(this._uri)) { + const uri = + gBrowser.selectedBrowser.currentOnionAliasURI || gBrowser.currentURI; + if (this._uri && uri.equals(this._uri)) { return; } this.updateStarState(); }, updateStarState: function BUI_updateStarState() { - this._uri = gBrowser.currentURI; + this._uri = + gBrowser.selectedBrowser.currentOnionAliasURI || gBrowser.currentURI; this._itemGuids.clear(); let guids = new Set(); diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 410c8f62d0efd..67a1977fef6af 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -73,6 +73,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm", TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm", Translation: "resource:///modules/translation/Translation.jsm", + OnionAliasStore: "resource:///modules/OnionAliasStore.jsm", UITour: "resource:///modules/UITour.jsm", UpdateUtils: "resource://gre/modules/UpdateUtils.jsm", UrlbarInput: "resource:///modules/UrlbarInput.jsm", @@ -3336,7 +3337,10 @@ function URLBarSetURI(aURI, updatePopupNotifications) { // bar if the user has deleted the URL and we'd just put the same URL // back. See bug 304198. if (value === null) { - let uri = aURI || gBrowser.currentURI; + let uri = + aURI || + gBrowser.selectedBrowser.currentOnionAliasURI || + gBrowser.currentURI; // Strip off usernames and passwords for the location bar try { uri = Services.uriFixup.createExposableURI(uri); @@ -5898,11 +5902,24 @@ var XULBrowserWindow = { this.reloadCommand.removeAttribute("disabled"); } + // The onion memorable alias needs to be used in URLBarSetURI, but also in + // other parts of the code (like the bookmarks UI), so we save it. + if (gBrowser.selectedBrowser.allowOnionUrlbarRewrites) { + gBrowser.selectedBrowser.currentOnionAliasURI = OnionAliasStore.getShortURI( + aLocationURI + ); + } else { + gBrowser.selectedBrowser.currentOnionAliasURI = null; + } + // We want to update the popup visibility if we received this notification // via simulated locationchange events such as switching between tabs, however // if this is a document navigation then PopupNotifications will be updated // via TabsProgressListener.onLocationChange and we do not want it called twice - URLBarSetURI(aLocationURI, aIsSimulated); + URLBarSetURI( + gBrowser.selectedBrowser.currentOnionAliasURI || aLocationURI, + aIsSimulated + ); BookmarkingUI.onLocationChange(); @@ -7639,6 +7656,21 @@ function handleLinkClick(event, href, linkNode) { } catch (e) {} } + // Check if the link needs to be opened with .tor.onion urlbar rewrites + // allowed. Only when the owner doc has allowOnionUrlbarRewrites = true + // and the same origin we should allow this. + let persistAllowOnionUrlbarRewritesInChildTab = false; + if (where == "tab" && gBrowser.docShell.allowOnionUrlbarRewrites) { + const sm = Services.scriptSecurityManager; + try { + let tURI = makeURI(href); + let isPrivateWin = + doc.nodePrincipal.originAttributes.privateBrowsingId > 0; + sm.checkSameOriginURI(referrerURI, tURI, false, isPrivateWin); + persistAllowOnionUrlbarRewritesInChildTab = true; + } catch (e) {} + } + // first get document wide referrer policy, then // get referrer attribute from clicked link and parse it and // allow per element referrer to overrule the document wide referrer if enabled @@ -7671,6 +7703,7 @@ function handleLinkClick(event, href, linkNode) { triggeringPrincipal: doc.nodePrincipal, csp, frameOuterWindowID, + allowOnionUrlbarRewrites: persistAllowOnionUrlbarRewritesInChildTab, }; // The new tab/window must use the same userContextId diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js index e7106e5e8fc1a..55da2efb7782d 100644 --- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -93,6 +93,7 @@ function openContextMenu(aMessage) { disableSetDesktopBackground: data.disableSetDesktopBg, loginFillInfo: data.loginFillInfo, parentAllowsMixedContent: data.parentAllowsMixedContent, + parentAllowsOnionUrlbarRewrites: data.parentAllowsOnionUrlbarRewrites, userContextId: data.userContextId, webExtContextData: data.webExtContextData, }; @@ -1122,9 +1123,26 @@ nsContextMenu.prototype = { } catch (e) {} } + // Check if the link needs to be opened with .tor.onion urlbar rewrites + // allowed. Only when parent has allowOnionUrlbarRewrites = true + // and the same origin we should allow this. + let persistAllowOnionUrlbarRewrites = false; + + if (gContextMenuContentData.parentAllowsOnionUrlbarRewrites) { + const sm = Services.scriptSecurityManager; + try { + let targetURI = this.linkURI; + let isPrivateWin = + this.browser.contentPrincipal.originAttributes.privateBrowsingId > 0; + sm.checkSameOriginURI(referrerURI, targetURI, false, isPrivateWin); + persistAllowOnionUrlbarRewrites = true; + } catch (e) {} + } + let params = { allowMixedContent: persistAllowMixedContentInChildTab, userContextId: parseInt(event.target.getAttribute("data-usercontextid")), + allowOnionUrlbarRewrites: persistAllowOnionUrlbarRewrites, }; openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params)); diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js index 4dd07ff14e7bb..53c463fd3263c 100644 --- a/browser/base/content/tabbrowser.js +++ b/browser/base/content/tabbrowser.js @@ -1589,6 +1589,7 @@ var aRelatedToCurrent; var aAllowInheritPrincipal; var aAllowMixedContent; + var aAllowOnionUrlbarRewrites; var aSkipAnimation; var aForceNotRemote; var aPreferredRemoteType; @@ -1618,6 +1619,7 @@ aRelatedToCurrent = params.relatedToCurrent; aAllowInheritPrincipal = !!params.allowInheritPrincipal; aAllowMixedContent = params.allowMixedContent; + aAllowOnionUrlbarRewrites = params.allowOnionUrlbarRewrites; aSkipAnimation = params.skipAnimation; aForceNotRemote = params.forceNotRemote; aPreferredRemoteType = params.preferredRemoteType; @@ -1658,6 +1660,7 @@ relatedToCurrent: aRelatedToCurrent, skipAnimation: aSkipAnimation, allowMixedContent: aAllowMixedContent, + allowOnionUrlbarRewrites: aAllowOnionUrlbarRewrites, forceNotRemote: aForceNotRemote, createLazyBrowser: aCreateLazyBrowser, preferredRemoteType: aPreferredRemoteType, @@ -2530,6 +2533,7 @@ { allowInheritPrincipal, allowMixedContent, + allowOnionUrlbarRewrites, allowThirdPartyFixup, bulkOrderedOpen, charset, @@ -2902,6 +2906,9 @@ if (allowMixedContent) { flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT; } + if (allowOnionUrlbarRewrites) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES; + } if (!allowInheritPrincipal) { flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; } diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js index 9a1010b2afa9e..cc46b14561b47 100644 --- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -370,6 +370,7 @@ function openLinkIn(url, where, params) { var aRelatedToCurrent = params.relatedToCurrent; var aAllowInheritPrincipal = !!params.allowInheritPrincipal; var aAllowMixedContent = params.allowMixedContent; + var aAllowOnionUrlbarRewrites = params.allowOnionUrlbarRewrites; var aForceAllowDataURI = params.forceAllowDataURI; var aInBackground = params.inBackground; var aInitiatingDoc = params.initiatingDoc; @@ -680,6 +681,7 @@ function openLinkIn(url, where, params) { relatedToCurrent: aRelatedToCurrent, skipAnimation: aSkipTabAnimation, allowMixedContent: aAllowMixedContent, + allowOnionUrlbarRewrites: aAllowOnionUrlbarRewrites, userContextId: aUserContextId, originPrincipal: aPrincipal, triggeringPrincipal: aTriggeringPrincipal, diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index fd2cbfe1d64fd..7ae147953c0b3 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -502,6 +502,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { ShellService: "resource:///modules/ShellService.jsm", TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm", TabUnloader: "resource:///modules/TabUnloader.jsm", + OnionAliasStore: "resource:///modules/OnionAliasStore.jsm", UIState: "resource://services-sync/UIState.jsm", UITour: "resource:///modules/UITour.jsm", WebChannel: "resource://gre/modules/WebChannel.jsm", @@ -1849,6 +1850,7 @@ BrowserGlue.prototype = { Normandy.uninit(); RFPHelper.uninit(); + OnionAliasStore.uninit(); }, // Set up a listener to enable/disable the screenshots extension @@ -2119,6 +2121,10 @@ BrowserGlue.prototype = { RFPHelper.init(); }); + Services.tm.idleDispatchToMainThread(() => { + OnionAliasStore.init(); + }); + ChromeUtils.idleDispatch(() => { Blocklist.loadBlocklistAsync(); }); diff --git a/browser/components/onionservices/ExtensionMessaging.jsm b/browser/components/onionservices/ExtensionMessaging.jsm new file mode 100644 index 0000000000000..b5d69df93807e --- /dev/null +++ b/browser/components/onionservices/ExtensionMessaging.jsm @@ -0,0 +1,86 @@ +// Copyright (c) 2020, The Tor Project, Inc. + +"use strict"; + +const EXPORTED_SYMBOLS = ["ExtensionMessaging"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { ExtensionUtils } = ChromeUtils.import( + "resource://gre/modules/ExtensionUtils.jsm" +); +const { MessageChannel } = ChromeUtils.import( + "resource://gre/modules/MessageChannel.jsm" +); +const { AddonManager } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); + +class ExtensionMessaging { + constructor() { + this._callback = null; + this._handlers = new Map(); + this._messageManager = Services.cpmm; + } + + async sendMessage(msg, extensionId) { + this._init(); + + const addon = await AddonManager.getAddonByID(extensionId); + if (!addon) { + throw new Error(`extension '${extensionId} does not exist`); + } + await addon.startupPromise; + + const channelId = ExtensionUtils.getUniqueId(); + return new Promise((resolve, reject) => { + this._handlers.set(channelId, { resolve, reject }); + this._messageManager.sendAsyncMessage("MessageChannel:Messages", [ + { + messageName: "Extension:Message", + sender: { + id: extensionId, + extensionId, + }, + recipient: { extensionId }, + data: new StructuredCloneHolder(msg), + channelId, + responseType: MessageChannel.RESPONSE_FIRST, + }, + ]); + }); + } + + unload() { + if (this._callback) { + this._handlers.clear(); + this._messageManager.removeMessageListener( + "MessageChannel:Response", + this._callback + ); + this._callback = null; + } + } + + _onMessage({ data }) { + const channelId = data.messageName; + if (this._handlers.has(channelId)) { + const { resolve, reject } = this._handlers.get(channelId); + this._handlers.delete(channelId); + if (data.error) { + reject(new Error(data.error.message)); + } else { + resolve(data.value); + } + } + } + + _init() { + if (this._callback === null) { + this._callback = this._onMessage.bind(this); + this._messageManager.addMessageListener( + "MessageChannel:Response", + this._callback + ); + } + } +} diff --git a/browser/components/onionservices/HttpsEverywhereControl.jsm b/browser/components/onionservices/HttpsEverywhereControl.jsm new file mode 100644 index 0000000000000..60c3b5fca2821 --- /dev/null +++ b/browser/components/onionservices/HttpsEverywhereControl.jsm @@ -0,0 +1,119 @@ +// Copyright (c) 2020, The Tor Project, Inc. + +"use strict"; + +const EXPORTED_SYMBOLS = ["HttpsEverywhereControl"]; + +const { ExtensionMessaging } = ChromeUtils.import( + "resource:///modules/ExtensionMessaging.jsm" +); +const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm"); + +const EXTENSION_ID = "https-everywhere-eff@eff.org"; +const SECUREDROP_TOR_ONION_CHANNEL = { + name: "SecureDropTorOnion", + jwk: { + kty: "RSA", + e: "AQAB", + n: + "p10BbUVc5Xj2S_-MH3bACNBaISo_r9e3PVPyTTjsGsdg2qSXvqUO42fBtpFAy0zUzIGS83v4JjiRdvKJaZTIvbC8AcpymzdsTqujMm8RPTSy3hO_8mXzGa4DEsIB1uNLnUWRBKXvSGCmT9kFyxhTpkYqokNBzafVihTU34tN2Md1xFHnmZGqfYtPtbJLWAa5Z1M11EyR4lIyUxIiPTV9t1XstDbWr3iS83REJrGEFmjG1-BAgx8_lDUTa41799N2yYEhgZud7bL0M3ei8s5OERjiion5uANkUV3-s2QqUZjiVA-XR_HizXjciaUWNd683KqekpNOZ_0STh_UGwpcwU-KwG07QyiCrLrRpz8S_vH8CqGrrcWY3GSzYe9dp34jJdO65oA-G8tK6fMXtvTCFDZI6oNNaXJH71F5J0YbqO2ZqwKYc2WSi0gKVl2wd9roOVjaBmkJqvocntYuNM7t38fDEWHn5KUkmrTbiG68Cy56tDUfpKl3D9Uj4LaMvxJ1tKGvzQ4k_60odT7gIxu6DqYjXUHZpwPsSGBq3njaD7boe4CUXF2K7ViOc87BsKxRNCzDD8OklRjjXzOTOBH3PqFJ93CJ-4ECE5t9STU20aZ8E-2zKB8vjKyCySE4-kcIvBBsnkwVaJTPy9Ft1qYybo-soXEWVEZATANNWklBt8k", + }, + update_path_prefix: "https://securedrop.org/https-everywhere/", + scope: + "^https?:\\/\\/[a-z0-9-]+(?:\\.[a-z0-9-]+)*\\.securedrop\\.tor\\.onion\\/", + replaces_default_rulesets: false, +}; + +class HttpsEverywhereControl { + constructor() { + this._extensionMessaging = null; + } + + async _sendMessage(type, object) { + return this._extensionMessaging.sendMessage( + { + type, + object, + }, + EXTENSION_ID + ); + } + + static async wait(seconds = 1) { + return new Promise(resolve => setTimeout(resolve, seconds * 1000)); + } + + /** + * Installs the .tor.onion update channel in https-everywhere + */ + async installTorOnionUpdateChannel(retries = 5) { + this._init(); + + // TODO: https-everywhere store is initialized asynchronously, so sending a message + // immediately results in a `store.get is undefined` error. + // For now, let's wait a bit and retry a few times if there is an error, but perhaps + // we could suggest https-everywhere to send a message when that happens and listen + // for that here. + await HttpsEverywhereControl.wait(); + + try { + // TODO: we may want a way to "lock" this update channel, so that it cannot be modified + // by the user via UI, but I think this is not possible at the time of writing via + // the existing messages in https-everywhere. + await this._sendMessage( + "create_update_channel", + SECUREDROP_TOR_ONION_CHANNEL.name + ); + } catch (e) { + if (retries <= 0) { + throw new Error("Could not install SecureDropTorOnion update channel"); + } + await this.installTorOnionUpdateChannel(retries - 1); + return; + } + + await this._sendMessage( + "update_update_channel", + SECUREDROP_TOR_ONION_CHANNEL + ); + } + + /** + * Returns the .tor.onion rulesets available in https-everywhere + */ + async getTorOnionRules() { + return this._sendMessage("get_simple_rules_ending_with", ".tor.onion"); + } + + /** + * Returns the timestamp of the last .tor.onion update channel update. + */ + async getRulesetTimestamp() { + const rulesets = await this._sendMessage("get_ruleset_timestamps"); + const securedrop = + rulesets && + rulesets.find(([{ name }]) => name === SECUREDROP_TOR_ONION_CHANNEL.name); + if (securedrop) { + const [ + updateChannel, // This has the same structure as SECUREDROP_TOR_ONION_CHANNEL + lastUpdatedTimestamp, // An integer, 0 if the update channel was never updated + ] = securedrop; + void updateChannel; // Ignore eslint unused warning for ruleset + return lastUpdatedTimestamp; + } + return null; + } + + unload() { + if (this._extensionMessaging) { + this._extensionMessaging.unload(); + this._extensionMessaging = null; + } + } + + _init() { + if (!this._extensionMessaging) { + this._extensionMessaging = new ExtensionMessaging(); + } + } +} diff --git a/browser/components/onionservices/OnionAliasStore.jsm b/browser/components/onionservices/OnionAliasStore.jsm new file mode 100644 index 0000000000000..7e006e59490b2 --- /dev/null +++ b/browser/components/onionservices/OnionAliasStore.jsm @@ -0,0 +1,199 @@ +// Copyright (c) 2020, The Tor Project, Inc. + +"use strict"; + +const EXPORTED_SYMBOLS = ["OnionAliasStore"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { setTimeout, clearTimeout } = ChromeUtils.import( + "resource://gre/modules/Timer.jsm" +); +const { HttpsEverywhereControl } = ChromeUtils.import( + "resource:///modules/HttpsEverywhereControl.jsm" +); + +// Logger adapted from CustomizableUI.jsm +const kPrefOnionAliasDebug = "browser.onionalias.debug"; +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gDebuggingEnabled", + kPrefOnionAliasDebug, + false, + (pref, oldVal, newVal) => { + if (typeof log != "undefined") { + log.maxLogLevel = newVal ? "all" : "log"; + } + } +); +XPCOMUtils.defineLazyGetter(this, "log", () => { + let scope = {}; + ChromeUtils.import("resource://gre/modules/Console.jsm", scope); + let consoleOptions = { + maxLogLevel: gDebuggingEnabled ? "all" : "log", + prefix: "OnionAlias", + }; + return new scope.ConsoleAPI(consoleOptions); +}); + +function observe(topic, callback) { + let observer = { + observe(aSubject, aTopic, aData) { + if (topic === aTopic) { + callback(aSubject, aData); + } + }, + }; + Services.obs.addObserver(observer, topic); + return () => Services.obs.removeObserver(observer, topic); +} + +class _OnionAliasStore { + static get RULESET_CHECK_INTERVAL() { + return 1000 * 60; // 1 minute + } + + static get RULESET_CHECK_INTERVAL_FAST() { + return 1000 * 5; // 5 seconds + } + + constructor() { + this._onionMap = new Map(); + this._rulesetTimeout = null; + this._removeObserver = () => {}; + this._canLoadRules = false; + this._rulesetTimestamp = null; + } + + async _periodicRulesetCheck() { + // TODO: it would probably be preferable to listen to some message broadcasted by + // the https-everywhere extension when some update channel is updated, instead of + // polling every N seconds. + log.debug("Checking for new rules"); + const ts = await this.httpsEverywhereControl.getRulesetTimestamp(); + log.debug( + `Found ruleset timestamp ${ts}, current is ${this._rulesetTimestamp}` + ); + if (ts !== this._rulesetTimestamp) { + this._rulesetTimestamp = ts; + log.debug("New rules found, updating"); + // We clear the mappings even if we cannot load the rules from https-everywhere, + // since we cannot be sure if the stored mappings are correct anymore. + this._clear(); + if (this._canLoadRules) { + await this._loadRules(); + } + } + // If the timestamp is 0, that means the update channel was not yet updated, so + // we schedule a check soon. + this._rulesetTimeout = setTimeout( + () => this._periodicRulesetCheck(), + ts === 0 + ? _OnionAliasStore.RULESET_CHECK_INTERVAL_FAST + : _OnionAliasStore.RULESET_CHECK_INTERVAL + ); + } + + async init() { + this.httpsEverywhereControl = new HttpsEverywhereControl(); + + // Install update channel + await this.httpsEverywhereControl.installTorOnionUpdateChannel(); + + // Setup .tor.onion rule loading. + // The http observer is a fallback, and is removed in _loadRules() as soon as we are able + // to load some rules from HTTPS Everywhere. + this._loadHttpObserver(); + try { + await this.httpsEverywhereControl.getTorOnionRules(); + this._canLoadRules = true; + } catch (e) { + // Loading rules did not work, probably because "get_simple_rules_ending_with" is not yet + // working in https-everywhere. Use an http observer as a fallback for learning the rules. + log.debug("Could not load rules, using http observer as fallback"); + } + + // Setup checker for https-everywhere ruleset updates + this._periodicRulesetCheck(); + } + + /** + * Loads the .tor.onion mappings from https-everywhere. + */ + async _loadRules() { + const rules = await this.httpsEverywhereControl.getTorOnionRules(); + // Remove http observer if we are able to load some rules directly. + if (rules.length) { + this._removeObserver(); + this._removeObserver = () => {}; + } + this._clear(); + log.debug(`Loading ${rules.length} rules`, rules); + for (const rule of rules) { + // Here we are trusting that the securedrop ruleset follows some conventions so that we can + // assume there is a host mapping from `rule.host` to the hostname of the URL in `rule.to`. + try { + const url = new URL(rule.to); + const shortHost = rule.host; + const longHost = url.hostname; + this._addMapping(shortHost, longHost); + } catch (e) { + log.error("Could not process rule:", rule); + } + } + } + + /** + * Loads a http observer to listen for local redirects for populating + * the .tor.onion -> .onion mappings. Should only be used if we cannot ask https-everywhere + * directly for the mappings. + */ + _loadHttpObserver() { + this._removeObserver = observe("http-on-before-connect", channel => { + if ( + channel.isMainDocumentChannel && + channel.originalURI.host.endsWith(".tor.onion") + ) { + this._addMapping(channel.originalURI.host, channel.URI.host); + } + }); + } + + uninit() { + this._clear(); + this._removeObserver(); + this._removeObserver = () => {}; + if (this.httpsEverywhereControl) { + this.httpsEverywhereControl.unload(); + delete this.httpsEverywhereControl; + } + clearTimeout(this._rulesetTimeout); + this._rulesetTimeout = null; + this._rulesetTimestamp = null; + } + + _clear() { + this._onionMap.clear(); + } + + _addMapping(shortOnionHost, longOnionHost) { + this._onionMap.set(longOnionHost, shortOnionHost); + } + + getShortURI(onionURI) { + if ( + (onionURI.schemeIs("http") || onionURI.schemeIs("https")) && + this._onionMap.has(onionURI.host) + ) { + return onionURI + .mutate() + .setHost(this._onionMap.get(onionURI.host)) + .finalize(); + } + return null; + } +} + +let OnionAliasStore = new _OnionAliasStore(); diff --git a/browser/components/onionservices/moz.build b/browser/components/onionservices/moz.build index bf276c4a3c4cc..dfd664df434e4 100644 --- a/browser/components/onionservices/moz.build +++ b/browser/components/onionservices/moz.build @@ -1,6 +1,9 @@ JAR_MANIFESTS += ['jar.mn'] EXTRA_JS_MODULES += [ + 'ExtensionMessaging.jsm', + 'HttpsEverywhereControl.jsm', + 'OnionAliasStore.jsm', 'OnionLocationChild.jsm', 'OnionLocationParent.jsm', ] diff --git a/browser/components/urlbar/UrlbarInput.jsm b/browser/components/urlbar/UrlbarInput.jsm index 131d2ecc8ca88..8fd87bb8b026d 100644 --- a/browser/components/urlbar/UrlbarInput.jsm +++ b/browser/components/urlbar/UrlbarInput.jsm @@ -1022,7 +1022,13 @@ class UrlbarInput { } let uri; - if (this.getAttribute("pageproxystate") == "valid") { + // When we rewrite .onion to an alias, gBrowser.currentURI will be different than + // the URI displayed in the urlbar. We need to use the urlbar value to copy the + // alias instead of the actual .onion URI that is loaded. + if ( + this.getAttribute("pageproxystate") == "valid" && + !this.window.gBrowser.selectedBrowser.allowOnionUrlbarRewrites + ) { uri = this.window.gBrowser.currentURI; } else { // The value could be: diff --git a/browser/modules/ContentClick.jsm b/browser/modules/ContentClick.jsm index 767c678809952..872b06488c1a7 100644 --- a/browser/modules/ContentClick.jsm +++ b/browser/modules/ContentClick.jsm @@ -88,6 +88,7 @@ var ContentClick = { charset: browser.characterSet, referrerInfo: E10SUtils.deserializeReferrerInfo(json.referrerInfo), allowMixedContent: json.allowMixedContent, + allowOnionUrlbarRewrites: json.allowOnionUrlbarRewrites, isContentWindowPrivate: json.isContentWindowPrivate, originPrincipal: json.originPrincipal, triggeringPrincipal: json.triggeringPrincipal, diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index 7dcfd0234e06f..087f04ddc5b3c 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -6565,6 +6565,18 @@ void nsDocShell::OnRedirectStateChange(nsIChannel* aOldChannel, return; } + if (!mAllowOnionUrlbarRewrites) { + nsAutoCString oldHost; + nsAutoCString newHost; + if (NS_SUCCEEDED(oldURI->GetHost(oldHost)) && + StringEndsWith(oldHost, NS_LITERAL_CSTRING(".tor.onion")) && + NS_SUCCEEDED(newURI->GetHost(newHost)) && + StringEndsWith(newHost, NS_LITERAL_CSTRING(".onion")) && + !StringEndsWith(newHost, NS_LITERAL_CSTRING(".tor.onion"))) { + mAllowOnionUrlbarRewrites = true; + } + } + // Below a URI visit is saved (see AddURIVisit method doc). // The visit chain looks something like: // ... @@ -9769,6 +9781,10 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState, // We're making history navigation or a reload. Make sure our history ID // points to the same ID as SHEntry's docshell ID. mHistoryID = aLoadState->SHEntry()->DocshellID(); + // Loading from session history may not call DoURILoad(), so we set this + // flag here. + mAllowOnionUrlbarRewrites = aLoadState->HasLoadFlags( + INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES); } } @@ -10411,6 +10427,13 @@ nsresult nsDocShell::DoURILoad(nsDocShellLoadState* aLoadState, } } + mAllowOnionUrlbarRewrites = + aLoadState->HasLoadFlags( + INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES) || + (mAllowOnionUrlbarRewrites && GetCurrentDocChannel() && + NS_SUCCEEDED( + nsContentUtils::CheckSameOrigin(GetCurrentDocChannel(), channel))); + // hack nsCOMPtr httpChannel(do_QueryInterface(channel)); nsCOMPtr httpChannelInternal( @@ -11758,6 +11781,7 @@ nsresult nsDocShell::AddToSessionHistory(nsIURI* aURI, nsIChannel* aChannel, triggeringPrincipal, // Channel or provided principal principalToInherit, csp, mHistoryID, mDynamicallyCreated); + entry->SetAllowOnionUrlbarRewrites(mAllowOnionUrlbarRewrites); entry->SetOriginalURI(originalURI); entry->SetResultPrincipalURI(resultPrincipalURI); entry->SetLoadReplace(loadReplace); @@ -11938,6 +11962,10 @@ nsresult nsDocShell::LoadHistoryEntry(nsISHEntry* aEntry, uint32_t aLoadType) { srcdoc = VoidString(); } + if (aEntry->GetAllowOnionUrlbarRewrites()) { + flags |= INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES; + } + // If there is no valid triggeringPrincipal, we deny the load MOZ_ASSERT(triggeringPrincipal, "need a valid triggeringPrincipal to load from history"); @@ -13842,3 +13870,12 @@ nsDocShell::SetWatchedByDevtools(bool aWatched) { mWatchedByDevtools = aWatched; return NS_OK; } + +NS_IMETHODIMP +nsDocShell::GetAllowOnionUrlbarRewrites(bool* aAllowOnionUrlbarRewrites) { + NS_ENSURE_ARG(aAllowOnionUrlbarRewrites); + *aAllowOnionUrlbarRewrites = + StaticPrefs::browser_urlbar_onionRewrites_enabled() && + mAllowOnionUrlbarRewrites; + return NS_OK; +} diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h index 4a81946f7bf46..7de079c0903e9 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -153,6 +153,9 @@ class nsDocShell final : public nsDocLoader, // Whether the load was triggered by user interaction. INTERNAL_LOAD_FLAGS_IS_USER_TRIGGERED = 0x1000, + + // Whether rewriting the urlbar to a short .onion alias is allowed. + INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES = 0x2000, }; // Event type dispatched by RestorePresentation @@ -1235,6 +1238,7 @@ class nsDocShell final : public nsDocLoader, bool mCSSErrorReportingEnabled : 1; bool mAllowAuth : 1; bool mAllowKeywordFixup : 1; + bool mAllowOnionUrlbarRewrites : 1; bool mIsOffScreenBrowser : 1; bool mIsActive : 1; bool mDisableMetaRefreshWhenInactive : 1; diff --git a/docshell/base/nsDocShellLoadState.cpp b/docshell/base/nsDocShellLoadState.cpp index d2e21edceeda9..80143b49f3fa6 100644 --- a/docshell/base/nsDocShellLoadState.cpp +++ b/docshell/base/nsDocShellLoadState.cpp @@ -417,6 +417,10 @@ void nsDocShellLoadState::CalculateLoadURIFlags() { mLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; } + if (oldLoadFlags & nsIWebNavigation::LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES) { + mLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES; + } + if (oldLoadFlags & nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD) { mLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_FIRST_LOAD; } diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl index 542232892c5b0..cc0013e80c10e 100644 --- a/docshell/base/nsIDocShell.idl +++ b/docshell/base/nsIDocShell.idl @@ -1195,4 +1195,9 @@ interface nsIDocShell : nsIDocShellTreeItem * Whether developer tools are watching activity in this docshell. */ [infallible] attribute boolean watchedByDevtools; + + /** + * Whether rewriting the urlbar to a short .onion alias is allowed. + */ + [infallible] readonly attribute boolean allowOnionUrlbarRewrites; }; diff --git a/docshell/base/nsIWebNavigation.idl b/docshell/base/nsIWebNavigation.idl index cabae3f31d0fa..123129d2c2593 100644 --- a/docshell/base/nsIWebNavigation.idl +++ b/docshell/base/nsIWebNavigation.idl @@ -226,6 +226,11 @@ interface nsIWebNavigation : nsISupports */ const unsigned long LOAD_FLAGS_IS_REDIRECT = 0x800000; + /** + * Allow rewriting the urlbar to a short .onion alias. + */ + const unsigned long LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES = 0x1000000; + /** * Loads a given URI. This will give priority to loading the requested URI * in the object implementing this interface. If it can't be loaded here diff --git a/docshell/shistory/nsISHEntry.idl b/docshell/shistory/nsISHEntry.idl index 12dbf8172262c..f5863b532a284 100644 --- a/docshell/shistory/nsISHEntry.idl +++ b/docshell/shistory/nsISHEntry.idl @@ -229,6 +229,11 @@ interface nsISHEntry : nsISupports */ [infallible] attribute boolean persist; + /** + * Whether rewriting the urlbar to a short .onion alias is allowed. + */ + [infallible] attribute boolean allowOnionUrlbarRewrites; + /** * Set/Get the visual viewport scroll position if session history is * changed through anchor navigation or pushState. diff --git a/docshell/shistory/nsSHEntry.cpp b/docshell/shistory/nsSHEntry.cpp index e39b5888c157e..27f21a23d2bab 100644 --- a/docshell/shistory/nsSHEntry.cpp +++ b/docshell/shistory/nsSHEntry.cpp @@ -42,7 +42,8 @@ nsSHEntry::nsSHEntry() mIsSrcdocEntry(false), mScrollRestorationIsManual(false), mLoadedInThisProcess(false), - mPersist(true) {} + mPersist(true), + mAllowOnionUrlbarRewrites(false) {} nsSHEntry::nsSHEntry(const nsSHEntry& aOther) : mShared(aOther.mShared), @@ -68,7 +69,8 @@ nsSHEntry::nsSHEntry(const nsSHEntry& aOther) mIsSrcdocEntry(aOther.mIsSrcdocEntry), mScrollRestorationIsManual(false), mLoadedInThisProcess(aOther.mLoadedInThisProcess), - mPersist(aOther.mPersist) {} + mPersist(aOther.mPersist), + mAllowOnionUrlbarRewrites(aOther.mAllowOnionUrlbarRewrites) {} nsSHEntry::~nsSHEntry() { // Null out the mParent pointers on all our kids. @@ -855,3 +857,15 @@ nsSHEntry::SetPersist(bool aPersist) { mPersist = aPersist; return NS_OK; } + +NS_IMETHODIMP +nsSHEntry::GetAllowOnionUrlbarRewrites(bool* aAllowOnionUrlbarRewrites) { + *aAllowOnionUrlbarRewrites = mAllowOnionUrlbarRewrites; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetAllowOnionUrlbarRewrites(bool aAllowOnionUrlbarRewrites) { + mAllowOnionUrlbarRewrites = aAllowOnionUrlbarRewrites; + return NS_OK; +} diff --git a/docshell/shistory/nsSHEntry.h b/docshell/shistory/nsSHEntry.h index 4a9d7e466b221..aeb42b3077394 100644 --- a/docshell/shistory/nsSHEntry.h +++ b/docshell/shistory/nsSHEntry.h @@ -61,6 +61,7 @@ class nsSHEntry final : public nsISHEntry { bool mScrollRestorationIsManual; bool mLoadedInThisProcess; bool mPersist; + bool mAllowOnionUrlbarRewrites; }; #endif /* nsSHEntry_h */ diff --git a/modules/libpref/init/StaticPrefList.h b/modules/libpref/init/StaticPrefList.h index 158952041d3db..044ab5b707e91 100644 --- a/modules/libpref/init/StaticPrefList.h +++ b/modules/libpref/init/StaticPrefList.h @@ -2657,6 +2657,13 @@ VARCACHE_PREF( bool, false ) +// Whether rewriting the urlbar to a short .onion alias is allowed. +VARCACHE_PREF( + "browser.urlbar.onionRewrites.enabled", + browser_urlbar_onionRewrites_enabled, + bool, true +) + //--------------------------------------------------------------------------- // ChannelClassifier prefs //--------------------------------------------------------------------------- diff --git a/netwerk/dns/effective_tld_names.dat b/netwerk/dns/effective_tld_names.dat index 9b3c983d39341..59ac65531d95c 100644 --- a/netwerk/dns/effective_tld_names.dat +++ b/netwerk/dns/effective_tld_names.dat @@ -5497,6 +5497,8 @@ pro.om // onion : https://tools.ietf.org/html/rfc7686 onion +tor.onion +securedrop.tor.onion // org : https://en.wikipedia.org/wiki/.org org diff --git a/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm b/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm index 6d971c10180a6..76ce03dc68970 100644 --- a/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm +++ b/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm @@ -60,6 +60,7 @@ RemoteWebNavigation.prototype = { LOAD_FLAGS_ALLOW_POPUPS: 32768, LOAD_FLAGS_BYPASS_CLASSIFIER: 65536, LOAD_FLAGS_FORCE_ALLOW_COOKIES: 131072, + LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES: 16777216, STOP_NETWORK: 1, STOP_CONTENT: 2, diff --git a/toolkit/content/widgets/browser-custom-element.js b/toolkit/content/widgets/browser-custom-element.js index f31058012cbb3..d26c8f8f0d1dd 100644 --- a/toolkit/content/widgets/browser-custom-element.js +++ b/toolkit/content/widgets/browser-custom-element.js @@ -283,6 +283,8 @@ this._mayEnableCharacterEncodingMenu = null; + this._allowOnionUrlbarRewrites = false; + this._contentPrincipal = null; this._csp = null; @@ -677,6 +679,12 @@ : this.docShell.mayEnableCharacterEncodingMenu; } + get allowOnionUrlbarRewrites() { + return this.isRemoteBrowser + ? this._allowOnionUrlbarRewrites + : this.docShell.allowOnionUrlbarRewrites; + } + get contentPrincipal() { return this.isRemoteBrowser ? this._contentPrincipal @@ -1870,6 +1878,7 @@ "_textZoom", "_isSyntheticDocument", "_innerWindowID", + "_allowOnionUrlbarRewrites", ] ); } diff --git a/toolkit/modules/RemoteWebProgress.jsm b/toolkit/modules/RemoteWebProgress.jsm index 0f3c53a17a8c7..558c31515570d 100644 --- a/toolkit/modules/RemoteWebProgress.jsm +++ b/toolkit/modules/RemoteWebProgress.jsm @@ -281,6 +281,8 @@ class RemoteWebProgressManager { this._browser._isSyntheticDocument = json.synthetic; this._browser._innerWindowID = json.innerWindowID; this._browser._contentRequestContextID = json.requestContextID; + this._browser._allowOnionUrlbarRewrites = + json.allowOnionUrlbarRewrites; } this.onLocationChange(webProgress, request, location, flags); diff --git a/toolkit/modules/WebProgressChild.jsm b/toolkit/modules/WebProgressChild.jsm index 60a1aa88e8efc..26250e361bd4d 100644 --- a/toolkit/modules/WebProgressChild.jsm +++ b/toolkit/modules/WebProgressChild.jsm @@ -167,6 +167,7 @@ class WebProgressChild { json.requestContextID = this.mm.content.document.documentLoadGroup ? this.mm.content.document.documentLoadGroup.requestContextID : null; + json.allowOnionUrlbarRewrites = this.mm.docShell.allowOnionUrlbarRewrites; if (AppConstants.MOZ_CRASHREPORTER && CrashReporter.enabled) { let uri = aLocationURI; diff --git a/toolkit/modules/sessionstore/SessionHistory.jsm b/toolkit/modules/sessionstore/SessionHistory.jsm index e54ab3369cd8d..463f147a9ca4b 100644 --- a/toolkit/modules/sessionstore/SessionHistory.jsm +++ b/toolkit/modules/sessionstore/SessionHistory.jsm @@ -287,6 +287,7 @@ var SessionHistoryInternal = { } entry.persist = shEntry.persist; + entry.allowOnionUrlbarRewrites = shEntry.allowOnionUrlbarRewrites; return entry; }, @@ -559,6 +560,10 @@ var SessionHistoryInternal = { } } + if (entry.allowOnionUrlbarRewrites) { + shEntry.allowOnionUrlbarRewrites = entry.allowOnionUrlbarRewrites; + } + return shEntry; }, -- GitLab