commit 24c2e6869ae1c319543d9cd913f6280d4e595b98 parent 96d9c18343a6247004de4069478106e0113b5f41 Author: Alex Catarineu <acat@torproject.org> Date: Thu, 5 Mar 2020 22:16:39 +0100 TB 21952: Implement Onion-Location Whenever a valid Onion-Location HTTP header (or corresponding HTML <meta> http-equiv attribute) is found in a document load, we either redirect to it (if the user opted-in via preference) or notify the presence of an onionsite alternative with a badge in the urlbar. Diffstat:
17 files changed, 363 insertions(+), 2 deletions(-)
diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js @@ -252,6 +252,9 @@ var gBrowserInit = { // Init the OnionAuthPrompt OnionAuthPrompt.init(); + // Init the Onion Location pill + OnionLocationParent.init(document); + gTorCircuitPanel.init(); // Certain kinds of automigration rely on this notification to complete diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js @@ -52,6 +52,7 @@ ChromeUtils.defineESModuleGetters(this, { NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", nsContextMenu: "chrome://browser/content/nsContextMenu.sys.mjs", + OnionLocationParent: "resource:///modules/OnionLocationParent.sys.mjs", OpenInTabsUtils: "moz-src:///browser/components/tabbrowser/OpenInTabsUtils.sys.mjs", OpenSearchManager: @@ -2345,6 +2346,7 @@ var XULBrowserWindow = { CFRPageActions.updatePageActions(gBrowser.selectedBrowser); AboutReaderParent.updateReaderButton(gBrowser.selectedBrowser); + OnionLocationParent.updateOnionLocationBadge(gBrowser.selectedBrowser); TranslationsParent.onLocationChange(gBrowser.selectedBrowser); PictureInPicture.updateUrlbarToggle(gBrowser.selectedBrowser); @@ -2931,6 +2933,16 @@ var CombinedStopReload = { var TabsProgressListener = { onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + // Clear OnionLocation UI + if ( + aStateFlags & Ci.nsIWebProgressListener.STATE_START && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && + aRequest && + aWebProgress.isTopLevel + ) { + OnionLocationParent.onStateChange(aBrowser); + } + // Collect telemetry data about tab load times. if ( aWebProgress.isTopLevel && diff --git a/browser/base/content/browser.js.globals b/browser/base/content/browser.js.globals @@ -250,5 +250,6 @@ "TorConnectParent", "gTorConnectUrlbarButton", "gTorConnectTitlebarStatus", - "OnionAuthPrompt" + "OnionAuthPrompt", + "OnionLocationParent" ] diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml @@ -440,6 +440,8 @@ hidden="true"> <label id="tor-connect-urlbar-button-label"/> </hbox> + +#include ../../components/onionservices/content/onionlocation-urlbar.inc.xhtml </hbox> </html:moz-urlbar> <toolbartabstop/> diff --git a/browser/base/content/popup-notifications.inc.xhtml b/browser/base/content/popup-notifications.inc.xhtml @@ -13,6 +13,15 @@ role="alert" remote="true"/> + <!-- Shown when visiting first web page with an onion-available header. + - This allows us to add some extra content to the popup. + - See tor-browser#21952 and tor-browser#41341 --> + <popupnotification id="onion-location-notification" hidden="true"> + <popupnotificationcontent orient="vertical"> + <description id="onion-location-body-text"/> + </popupnotificationcontent> + </popupnotification> + <popupnotification id="webRTC-shareDevices-notification" hidden="true" descriptionid="webRTC-shareDevices-notification-description"> <popupnotificationcontent id="webRTC-selectCamera" orient="vertical"> diff --git a/browser/components/DesktopActorRegistry.sys.mjs b/browser/components/DesktopActorRegistry.sys.mjs @@ -551,6 +551,19 @@ let JSWINDOWACTORS = { // LinkPreviewParent.sys.mjs and LinkPreviewChild.sys.mjs are missing. // tor-browser#44045. + OnionLocation: { + parent: { + esModuleURI: "resource:///modules/OnionLocationParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///modules/OnionLocationChild.sys.mjs", + events: { + pageshow: { mozSystemGroup: true }, + }, + }, + messageManagerGroups: ["browsers"], + }, + PageAssist: { parent: { esModuleURI: "resource:///actors/PageAssistParent.sys.mjs", diff --git a/browser/components/onionservices/OnionLocationChild.sys.mjs b/browser/components/onionservices/OnionLocationChild.sys.mjs @@ -0,0 +1,45 @@ +// Copyright (c) 2020, The Tor Project, Inc. + +/** + * This class contains the child part of Onion Location. + */ +export class OnionLocationChild extends JSWindowActorChild { + handleEvent(event) { + this.onPageShow(event); + } + + onPageShow(event) { + if (event.target != this.document) { + return; + } + const onionLocationURI = this.document.onionLocationURI; + if (onionLocationURI) { + this.sendAsyncMessage("OnionLocation:Set"); + } + } + + receiveMessage(aMessage) { + if (aMessage.name == "OnionLocation:Refresh") { + const doc = this.document; + const docShell = this.docShell; + let onionLocationURI = doc.onionLocationURI; + const refreshURI = docShell.QueryInterface(Ci.nsIRefreshURI); + if (onionLocationURI && refreshURI) { + const docUrl = URL.parse(doc.URL); + let onionUrl = URL.parse(onionLocationURI.asciiSpec); + // Keep consistent with Location + if (!onionUrl?.hash && docUrl?.hash) { + onionUrl.hash = docUrl.hash; + onionLocationURI = Services.io.newURI(onionUrl?.toString() || ""); + } + refreshURI.refreshURI( + onionLocationURI, + doc.nodePrincipal, + 0, + false, + true + ); + } + } + } +} diff --git a/browser/components/onionservices/OnionLocationParent.sys.mjs b/browser/components/onionservices/OnionLocationParent.sys.mjs @@ -0,0 +1,172 @@ +// Copyright (c) 2020, The Tor Project, Inc. + +import { TorStrings } from "resource://gre/modules/TorStrings.sys.mjs"; + +// Prefs + +// We keep the "prioritizeonions" name, even if obsolete, in order to +// prevent the notification from being shown again to upgrading users. +const NOTIFICATION_PREF = "privacy.prioritizeonions.showNotification"; + +// Element IDs +const ONIONLOCATION_BOX_ID = "onion-location-box"; +const ONIONLOCATION_LABEL_ID = "onion-label"; + +// Notification IDs +const NOTIFICATION_ID = "onion-location"; +const NOTIFICATION_ANCHOR_ID = "onion-location-box"; + +// Strings +const STRING_ONION_AVAILABLE = TorStrings.onionLocation.onionAvailable; +const NOTIFICATION_CANCEL_LABEL = TorStrings.onionLocation.notNow; +const NOTIFICATION_CANCEL_ACCESSKEY = TorStrings.onionLocation.notNowAccessKey; +const NOTIFICATION_OK_LABEL = TorStrings.onionLocation.loadOnion; +const NOTIFICATION_OK_ACCESSKEY = TorStrings.onionLocation.loadOnionAccessKey; +const NOTIFICATION_TITLE = TorStrings.onionLocation.tryThis; +const NOTIFICATION_DESCRIPTION = TorStrings.onionLocation.description; +const NOTIFICATION_LEARN_MORE_URL = + TorStrings.onionLocation.learnMoreURLNotification; + +/** + * This class contains the parent part of Onion Location. + */ +export class OnionLocationParent extends JSWindowActorParent { + // Listeners are added in BrowserGlue.jsm + receiveMessage(aMsg) { + switch (aMsg.name) { + case "OnionLocation:Set": { + let browser = this.browsingContext.embedderElement; + OnionLocationParent.setOnionLocation(browser); + break; + } + } + } + + static init(document) { + document + .getElementById(ONIONLOCATION_BOX_ID) + .addEventListener("click", event => this.buttonClick(event)); + } + + static buttonClick(event) { + if (event.button !== 0) { + return; + } + const win = event.target.ownerGlobal; + if (win.gBrowser) { + const browser = win.gBrowser.selectedBrowser; + OnionLocationParent.redirect(browser); + } + } + + static redirect(browser) { + let windowGlobal = browser.browsingContext.currentWindowGlobal; + let actor = windowGlobal.getActor("OnionLocation"); + if (actor) { + actor.sendAsyncMessage("OnionLocation:Refresh", {}); + OnionLocationParent.setDisabled(browser); + } + } + + static onStateChange(browser) { + delete browser._onionLocation; + OnionLocationParent.hideNotification(browser); + } + + static setOnionLocation(browser) { + browser._onionLocation = true; + let tabBrowser = browser.getTabBrowser(); + if (tabBrowser && browser === tabBrowser.selectedBrowser) { + OnionLocationParent.updateOnionLocationBadge(browser); + } + } + + static hideNotification(browser) { + const win = browser.ownerGlobal; + if (browser._onionLocationPrompt) { + win.PopupNotifications.remove(browser._onionLocationPrompt); + } + } + + static showNotification(browser) { + const mustShow = Services.prefs.getBoolPref(NOTIFICATION_PREF, true); + if (!mustShow) { + return; + } + + const win = browser.ownerGlobal; + Services.prefs.setBoolPref(NOTIFICATION_PREF, false); + + const mainAction = { + label: NOTIFICATION_OK_LABEL, + accessKey: NOTIFICATION_OK_ACCESSKEY, + callback() { + OnionLocationParent.redirect(browser); + }, + }; + + const cancelAction = { + label: NOTIFICATION_CANCEL_LABEL, + accessKey: NOTIFICATION_CANCEL_ACCESSKEY, + callback: () => {}, + }; + + win.document.getElementById("onion-location-body-text").textContent = + NOTIFICATION_DESCRIPTION; + + const options = { + autofocus: true, + persistent: true, + removeOnDismissal: false, + eventCallback(aTopic) { + if (aTopic === "removed") { + delete browser._onionLocationPrompt; + } + }, + learnMoreURL: NOTIFICATION_LEARN_MORE_URL, + hideClose: true, + popupOptions: { + position: "bottomright topright", + }, + }; + + // A hacky way of setting the popup anchor outside the usual url bar icon + // box, similar to CRF and addons. + // See: https://searchfox.org/mozilla-esr115/rev/7962d6b7b17ee105ad64ab7906af2b9179f6e3d2/toolkit/modules/PopupNotifications.sys.mjs#46 + browser[NOTIFICATION_ANCHOR_ID + "popupnotificationanchor"] = + win.document.getElementById(NOTIFICATION_ANCHOR_ID); + + browser._onionLocationPrompt = win.PopupNotifications.show( + browser, + NOTIFICATION_ID, + NOTIFICATION_TITLE, + NOTIFICATION_ANCHOR_ID, + mainAction, + [cancelAction], + options + ); + } + + static setEnabled(browser) { + const win = browser.ownerGlobal; + const label = win.document.getElementById(ONIONLOCATION_LABEL_ID); + label.textContent = STRING_ONION_AVAILABLE; + const elem = win.document.getElementById(ONIONLOCATION_BOX_ID); + elem.removeAttribute("hidden"); + } + + static setDisabled(browser) { + const win = browser.ownerGlobal; + const elem = win.document.getElementById(ONIONLOCATION_BOX_ID); + elem.setAttribute("hidden", true); + } + + static updateOnionLocationBadge(browser) { + if (browser._onionLocation) { + OnionLocationParent.setEnabled(browser); + OnionLocationParent.showNotification(browser); + } else { + OnionLocationParent.setDisabled(browser); + } + } +} diff --git a/browser/components/onionservices/content/onionlocation-urlbar.inc.xhtml b/browser/components/onionservices/content/onionlocation-urlbar.inc.xhtml @@ -0,0 +1,9 @@ +# Copyright (c) 2020, The Tor Project, Inc. + +<hbox id="onion-location-box" + class="tor-button tor-urlbar-button" + role="button" + hidden="true"> + <image id="onion-location-button" role="presentation"/> + <label id="onion-label"/> +</hbox> diff --git a/browser/components/onionservices/content/onionlocation.css b/browser/components/onionservices/content/onionlocation.css @@ -0,0 +1,22 @@ +/* Copyright (c) 2020, The Tor Project, Inc. */ + +#onion-location-button { + list-style-image: url(chrome://global/skin/icons/onion-site.svg); + -moz-context-properties: fill; + fill: currentColor; +} + +#tor-connect-urlbar-button:not([hidden]) ~ #onion-location-box { + /* Hide this button whilst the "Connect" button is shown. tor-browser#43406. + * This should only make a difference when the tor process dies mid-session. + * NOTE: We do not attempt to re-assign focus since the user should already + * have had their attention disrupted when the tor process dies mid-session. + * NOTE: If we hide this whilst the onion location popup is shown, the popup + * will instead become anchored to the site identity (padlock) button instead, + * but will return to the #onion-location-box once the "Connect" button is + * hidden again. In principle we could explicitly call + * PopupNotifications.anchorVisibilityChange() but mozilla code already has + * plenty of entry points to update the anchor position for us. + * Moreover, this scenario is expected to be a very rare. */ + display: none; +} diff --git a/browser/components/onionservices/jar.mn b/browser/components/onionservices/jar.mn @@ -5,3 +5,4 @@ browser.jar: content/browser/onionservices/onionservices.css (content/onionservices.css) content/browser/onionservices/savedKeysDialog.js (content/savedKeysDialog.js) content/browser/onionservices/savedKeysDialog.xhtml (content/savedKeysDialog.xhtml) + skin/classic/browser/onionlocation.css (content/onionlocation.css) diff --git a/browser/components/onionservices/moz.build b/browser/components/onionservices/moz.build @@ -1 +1,6 @@ JAR_MANIFESTS += ["jar.mn"] + +EXTRA_JS_MODULES += [ + "OnionLocationChild.sys.mjs", + "OnionLocationParent.sys.mjs", +] diff --git a/browser/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css @@ -31,6 +31,7 @@ @import url("chrome://browser/skin/formautofill-notification.css"); @import url("chrome://global/skin/tor-colors.css"); @import url("chrome://browser/skin/tor-urlbar-button.css"); +@import url("chrome://browser/skin/onionlocation.css"); :root, body { diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp @@ -3094,6 +3094,7 @@ void Document::ResetToURI(nsIURI* aURI, nsILoadGroup* aLoadGroup, // mDocumentURI. mDocumentBaseURI = nullptr; mChromeXHRDocBaseURI = nullptr; + mOnionLocationURI = nullptr; if (aLoadGroup) { nsCOMPtr<nsIInterfaceRequestor> callbacks; @@ -7337,6 +7338,57 @@ void Document::GetHeaderData(nsAtom* aHeaderField, nsAString& aData) const { } } +static bool IsValidOnionLocation(nsIURI* aDocumentURI, + nsIURI* aOnionLocationURI) { + if (!aDocumentURI || !aOnionLocationURI) { + return false; + } + + // Current URI + nsAutoCString host; + if (!aDocumentURI->SchemeIs("https")) { + return false; + } + NS_ENSURE_SUCCESS(aDocumentURI->GetAsciiHost(host), false); + if (StringEndsWith(host, ".onion"_ns)) { + // Already in the .onion site + return false; + } + + // Target URI + if (!aOnionLocationURI->SchemeIs("http") && + !aOnionLocationURI->SchemeIs("https")) { + return false; + } + nsCOMPtr<nsIEffectiveTLDService> eTLDService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + if (!eTLDService) { + NS_ENSURE_SUCCESS(aOnionLocationURI->GetAsciiHost(host), false); + // This should not happen, but in the unlikely case, still check if it is a + // .onion and in case allow it. + return StringEndsWith(host, ".onion"_ns); + } + NS_ENSURE_SUCCESS(eTLDService->GetBaseDomain(aOnionLocationURI, 0, host), + false); + if (!StringEndsWith(host, ".onion"_ns)) { + return false; + } + + // Ignore v2 + if (host.Length() == 22) { + const char* cur = host.BeginWriting(); + // We have already checked that it ends by ".onion" + const char* end = host.EndWriting() - 6; + bool base32 = true; + for (; cur < end && base32; ++cur) { + base32 = isalpha(*cur) || ('2' <= *cur && *cur <= '7'); + } + return !base32; + } + + return true; +} + void Document::SetHeaderData(nsAtom* aHeaderField, const nsAString& aData) { if (!aHeaderField) { NS_ERROR("null headerField"); @@ -7427,6 +7479,14 @@ void Document::SetHeaderData(nsAtom* aHeaderField, const nsAString& aData) { if (aHeaderField == nsGkAtoms::handheldFriendly) { mViewportType = Unknown; } + + if (aHeaderField == nsGkAtoms::headerOnionLocation && !aData.IsEmpty()) { + nsCOMPtr<nsIURI> onionURI; + if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(onionURI), aData)) && + IsValidOnionLocation(Document::GetDocumentURI(), onionURI)) { + mOnionLocationURI = onionURI; + } + } } void Document::SetEarlyHints( @@ -11766,7 +11826,7 @@ void Document::RetrieveRelevantHeaders(nsIChannel* aChannel) { static const char* const headers[] = { "default-style", "content-style-type", "content-language", "content-disposition", "refresh", "x-dns-prefetch-control", - "x-frame-options", "origin-trial", + "x-frame-options", "origin-trial", "onion-location", // add more http headers if you need // XXXbz don't add content-location support without reading bug // 238654 and its dependencies/dups first. diff --git a/dom/base/Document.h b/dom/base/Document.h @@ -3589,6 +3589,7 @@ class Document : public nsINode, void ReleaseCapture() const; void MozSetImageElement(const nsAString& aImageElementId, Element* aElement); nsIURI* GetDocumentURIObject() const; + nsIURI* GetOnionLocationURI() const { return mOnionLocationURI; } // Not const because all the fullscreen goop is not const const char* GetFullscreenError(CallerType); bool FullscreenEnabled(CallerType aCallerType) { @@ -4812,6 +4813,7 @@ class Document : public nsINode, nsCOMPtr<nsIURI> mChromeXHRDocURI; nsCOMPtr<nsIURI> mDocumentBaseURI; nsCOMPtr<nsIURI> mChromeXHRDocBaseURI; + nsCOMPtr<nsIURI> mOnionLocationURI; // A lazily-constructed URL data for style system to resolve URL values. RefPtr<URLExtraData> mCachedURLData; diff --git a/dom/webidl/Document.webidl b/dom/webidl/Document.webidl @@ -755,6 +755,9 @@ partial interface Document { // context which isn't in bfcache. [ChromeOnly] boolean isActive(); + // Allow chrome JS to know whether a document has a valid Onion-Location + // that we could redirect to. + [ChromeOnly] readonly attribute URI? onionLocationURI; }; Document includes NonElementParentNode; diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py @@ -861,6 +861,7 @@ STATIC_ATOMS = [ Atom("oninputsourceschange", "oninputsourceschange"), Atom("oninstall", "oninstall"), Atom("oninvalid", "oninvalid"), + Atom("headerOnionLocation", "onion-location"), Atom("onkeydown", "onkeydown"), Atom("onkeypress", "onkeypress"), Atom("onkeyup", "onkeyup"),