Commit 5af0b360 authored by Alex Catarineu's avatar Alex Catarineu Committed by Matthew Finkel
Browse files

Bug 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.
parent 862c67af
......@@ -49,6 +49,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
NetUtil: "resource://gre/modules/NetUtil.jsm",
NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.jsm",
OnionLocationParent: "resource:///modules/OnionLocationParent.jsm",
PageActions: "resource:///modules/PageActions.jsm",
PageThumbs: "resource://gre/modules/PageThumbs.jsm",
PanelMultiView: "resource:///modules/PanelMultiView.jsm",
......@@ -5293,6 +5294,7 @@ var XULBrowserWindow = {
Services.obs.notifyObservers(null, "touchbar-location-change", location);
UpdateBackForwardCommands(gBrowser.webNavigation);
AboutReaderParent.updateReaderButton(gBrowser.selectedBrowser);
OnionLocationParent.updateOnionLocationBadge(gBrowser.selectedBrowser);
if (!gMultiProcessBrowser) {
// Bug 1108553 - Cannot rotate images with e10s
......@@ -5785,6 +5787,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 &&
......
......@@ -2028,6 +2028,9 @@
onclick="FullZoom.reset(); FullZoom.resetScalingZoom();"
tooltip="dynamic-shortcut-tooltip"
hidden="true"/>
#include ../../components/onionservices/content/onionlocation-urlbar.inc.xhtml
<box id="pageActionSeparator" class="urlbar-page-action"/>
<image id="pageActionButton"
class="urlbar-icon urlbar-page-action"
......
......@@ -543,6 +543,19 @@ let JSWINDOWACTORS = {
allFrames: true,
},
OnionLocation: {
parent: {
moduleURI: "resource:///modules/OnionLocationParent.jsm",
},
child: {
moduleURI: "resource:///modules/OnionLocationChild.jsm",
events: {
pageshow: { mozSystemGroup: true },
},
},
messageManagerGroups: ["browsers"],
},
PageInfo: {
child: {
moduleURI: "resource:///actors/PageInfoChild.jsm",
......
// Copyright (c) 2020, The Tor Project, Inc.
"use strict";
var EXPORTED_SYMBOLS = ["OnionLocationChild"];
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;
const onionLocationURI = doc.onionLocationURI;
const refreshURI = docShell.QueryInterface(Ci.nsIRefreshURI);
if (onionLocationURI && refreshURI) {
refreshURI.refreshURI(
onionLocationURI,
doc.nodePrincipal,
0,
false,
true
);
}
}
}
}
// Copyright (c) 2020, The Tor Project, Inc.
"use strict";
var EXPORTED_SYMBOLS = ["OnionLocationParent"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
// Prefs
const NOTIFICATION_PREF = "privacy.prioritizeonions.showNotification";
const PRIORITIZE_ONIONS_PREF = "privacy.prioritizeonions.enabled";
// Element IDs
const ONIONLOCATION_BOX_ID = "onion-location-box";
const ONIONLOCATION_BUTTON_ID = "onion-location-button";
const ONIONLOCATION_LABEL_ID = "onion-label";
// Notification IDs
const NOTIFICATION_ID = "onion-location";
const NOTIFICATION_ANCHOR_ID = "onionlocation";
// 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.alwaysPrioritize;
const NOTIFICATION_OK_ACCESSKEY =
TorStrings.onionLocation.alwaysPrioritizeAccessKey;
const NOTIFICATION_TITLE = TorStrings.onionLocation.tryThis;
const NOTIFICATION_DESCRIPTION = TorStrings.onionLocation.description;
const NOTIFICATION_LEARN_MORE_URL = TorStrings.onionLocation.learnMoreURL;
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 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() {
Services.prefs.setBoolPref(PRIORITIZE_ONIONS_PREF, true);
OnionLocationParent.redirect(browser);
win.openPreferences("privacy-onionservices");
},
};
const cancelAction = {
label: NOTIFICATION_CANCEL_LABEL,
accessKey: NOTIFICATION_CANCEL_ACCESSKEY,
callback: () => {},
};
const options = {
autofocus: true,
persistent: true,
removeOnDismissal: false,
eventCallback(aTopic) {
if (aTopic === "removed") {
delete browser._onionLocationPrompt;
delete browser.onionpopupnotificationanchor;
}
},
learnMoreURL: NOTIFICATION_LEARN_MORE_URL,
displayURI: {
hostPort: NOTIFICATION_TITLE, // This is hacky, but allows us to have a title without extra markup/css.
},
hideClose: true,
popupIconClass: "onionlocation-notification-icon",
};
// A hacky way of setting the popup anchor outside the usual url bar icon box
// onionlocationpopupnotificationanchor comes from `${ANCHOR_ID}popupnotificationanchor`
// From https://searchfox.org/mozilla-esr68/rev/080f9ed47742644d2ff84f7aa0b10aea5c44301a/browser/components/newtab/lib/CFRPageActions.jsm#488
browser.onionlocationpopupnotificationanchor = win.document.getElementById(
ONIONLOCATION_BUTTON_ID
);
browser._onionLocationPrompt = win.PopupNotifications.show(
browser,
NOTIFICATION_ID,
NOTIFICATION_DESCRIPTION,
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);
}
}
}
/* Copyright (c) 2020, The Tor Project, Inc. */
.onionlocation-notification-icon {
display: none;
}
\ No newline at end of file
/* Copyright (c) 2020, The Tor Project, Inc. */
#onion-location-button {
list-style-image: url(chrome://browser/content/onionservices/onionlocation.svg);
}
#onion-location-box {
border-radius: 3px;
background-color: #6200A4;
padding-left: 5px;
padding-right: 5px;
color: white;
-moz-context-properties: fill;
fill: white;
}
#onion-location-box:hover {
background-color: #0060DF !important;
}
toolbar[brighttext] #onion-location-box {
background-color: #9400ff;
}
toolbar[brighttext] #onion-location-box:hover {
background-color: #0060DF !important;
}
# Copyright (c) 2020, The Tor Project, Inc.
<hbox id="onion-location-box"
class="urlbar-icon-wrapper urlbar-page-action"
role="button"
hidden="true"
onclick="OnionLocationParent.buttonClick(event);">
<image id="onion-location-button" role="presentation"/>
<hbox id="onion-label-container"><label id="onion-label"/></hbox>
</hbox>
<svg width="16" height="16" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="m8.016411 14.54499v-0.969784c3.071908-0.0089 5.559239-2.501304 5.559239-5.575429 0-3.073903-2.487331-5.566336-5.559239-5.575206v-0.9697843c3.607473 0.00909 6.528802 2.935521 6.528802 6.544991 0 3.609691-2.921329 6.536342-6.528802 6.545213zm0-3.394356c1.732661-0.0091 3.135111-1.415756 3.135111-3.150857 0-1.734878-1.402451-3.141542-3.135111-3.150634v-0.9695626c2.268448 0.00887 4.104895 1.849753 4.104895 4.120197 0 2.270666-1.836447 4.111549-4.104895 4.120419zm0-4.846926c0.9294227 0.00887 1.680545 0.7644289 1.680545 1.696069 0 0.9318627-0.7511226 1.687421-1.680545 1.696291zm-8.016411 1.696069c0 4.418473 3.581527 8.000222 8 8.000222 4.418251 0 8-3.581749 8-8.000222 0-4.418251-3.581749-7.999778-8-7.999778-4.418473 0-8 3.581527-8 7.999778z" />
</svg>
\ No newline at end of file
# Copyright (c) 2020, The Tor Project, Inc.
<groupbox id="onionServicesGroup" data-category="panePrivacy" data-subcategory="onionservices" hidden="true">
<label><html:h2 id="onionServicesTitle"></html:h2></label>
<label><label class="tail-with-learn-more" id="prioritizeOnionsDesc"></label><label
class="learnMore" is="text-link" id="onionServicesLearnMore"></label></label>
<radiogroup id="prioritizeOnionsRadioGroup" aria-labelledby="prioritizeOnionsDesc" preference="privacy.prioritizeonions.enabled">
<radio id="onionServicesRadioAlways" value="true"/>
<radio id="onionServicesRadioAsk" value="false"/>
</radiogroup>
</groupbox>
// Copyright (c) 2020, The Tor Project, Inc.
"use strict";
ChromeUtils.defineModuleGetter(
this,
"TorStrings",
"resource:///modules/TorStrings.jsm"
);
const OnionLocationPreferences = {
init() {
document.getElementById("onionServicesTitle").textContent =
TorStrings.onionLocation.onionServicesTitle;
document.getElementById("prioritizeOnionsDesc").textContent =
TorStrings.onionLocation.prioritizeOnionsDescription;
const learnMore = document.getElementById("onionServicesLearnMore");
learnMore.textContent = TorStrings.onionLocation.learnMore;
learnMore.href = TorStrings.onionLocation.learnMoreURL;
document.getElementById("onionServicesRadioAlways").label =
TorStrings.onionLocation.always;
document.getElementById("onionServicesRadioAsk").label =
TorStrings.onionLocation.askEverytime;
},
};
Object.defineProperty(this, "OnionLocationPreferences", {
value: OnionLocationPreferences,
enumerable: true,
writable: false,
});
......@@ -7,3 +7,5 @@ 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)
content/browser/onionservices/onionlocationPreferences.js (content/onionlocationPreferences.js)
content/browser/onionservices/onionlocation.svg (content/onionlocation.svg)
......@@ -4,4 +4,6 @@ EXTRA_JS_MODULES += [
"ExtensionMessaging.jsm",
"HttpsEverywhereControl.jsm",
"OnionAliasStore.jsm",
"OnionLocationChild.jsm",
"OnionLocationParent.jsm",
]
......@@ -14,6 +14,8 @@
<html:h1 data-l10n-id="privacy-header"/>
</hbox>
#include ../onionservices/content/onionlocationPreferences.inc.xhtml
<!-- Tracking / Content Blocking -->
<groupbox id="trackingGroup" data-category="panePrivacy" hidden="true" aria-describedby="contentBlockingDescription">
<label id="contentBlockingHeader"><html:h2 data-l10n-id="content-blocking-enhanced-tracking-protection"/></label>
......
......@@ -93,6 +93,12 @@ XPCOMUtils.defineLazyScriptGetter(
"chrome://browser/content/securitylevel/securityLevel.js"
);
XPCOMUtils.defineLazyScriptGetter(
this,
["OnionLocationPreferences"],
"chrome://browser/content/onionservices/onionlocationPreferences.js"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"listManager",
......@@ -169,6 +175,9 @@ Preferences.addAll([
// Do not track
{ id: "privacy.donottrackheader.enabled", type: "bool" },
// Onion Location
{ id: "privacy.prioritizeonions.enabled", type: "bool" },
// Media
{ id: "media.autoplay.default", type: "int" },
......@@ -330,6 +339,13 @@ var gPrivacyPane = {
window.addEventListener("unload", unload);
},
/**
* Show the OnionLocation preferences UI
*/
_initOnionLocation() {
OnionLocationPreferences.init();
},
/**
* Whether the prompt to restart Firefox should appear when changing the autostart pref.
*/
......@@ -531,6 +547,7 @@ var gPrivacyPane = {
this._initTrackingProtectionExtensionControl();
OnionServicesAuthPreferences.init();
this._initSecurityLevel();
this._initOnionLocation();
Services.telemetry.setEventRecordingEnabled("pwmgr", true);
......
......@@ -427,3 +427,5 @@ html|*#webRTC-previewVideo {
background: #FFE900 url(chrome://browser/skin/notification-icons/update.svg) no-repeat center;
border-radius: 50%;
}
%include ../../components/onionservices/content/onionlocation-notification-icons.css
\ No newline at end of file
......@@ -948,3 +948,5 @@
.searchbar-search-button:hover:not([addengines=true]) > .searchbar-search-icon-overlay:-moz-locale-dir(rtl) {
margin-inline: -26px 20px;
}
%include ../../components/onionservices/content/onionlocation-urlbar.css
......@@ -2790,6 +2790,7 @@ void Document::ResetToURI(nsIURI* aURI, nsILoadGroup* aLoadGroup,
// mDocumentURI.
mDocumentBaseURI = nullptr;
mChromeXHRDocBaseURI = nullptr;
mOnionLocationURI = nullptr;
 
// Check if the current document is the top-level DevTools document.
// For inner DevTools frames, mIsDevToolsDocument will be set when
......@@ -6304,6 +6305,22 @@ void Document::GetHeaderData(nsAtom* aHeaderField, nsAString& aData) const {
}
}
 
static bool IsValidOnionLocation(nsIURI* aDocumentURI,
nsIURI* aOnionLocationURI) {
bool isHttpish;
nsAutoCString host;
return aDocumentURI && aOnionLocationURI &&
NS_SUCCEEDED(aDocumentURI->SchemeIs("https", &isHttpish)) &&
isHttpish && NS_SUCCEEDED(aDocumentURI->GetAsciiHost(host)) &&
!StringEndsWith(host, ".onion"_ns) &&
((NS_SUCCEEDED(aOnionLocationURI->SchemeIs("http", &isHttpish)) &&
isHttpish) ||
(NS_SUCCEEDED(aOnionLocationURI->SchemeIs("https", &isHttpish)) &&
isHttpish)) &&
NS_SUCCEEDED(aOnionLocationURI->GetAsciiHost(host)) &&
StringEndsWith(host, ".onion"_ns);
}
void Document::SetHeaderData(nsAtom* aHeaderField, const nsAString& aData) {
if (!aHeaderField) {
NS_ERROR("null headerField");
......@@ -6378,6 +6395,21 @@ void Document::SetHeaderData(nsAtom* aHeaderField, const nsAString& aData) {
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)) {
if (StaticPrefs::privacy_prioritizeonions_enabled()) {
nsCOMPtr<nsIRefreshURI> refresher(mDocumentContainer);
if (refresher) {
refresher->RefreshURI(onionURI, NodePrincipal(), 0, false, true);
}
} else {
mOnionLocationURI = onionURI;
}
}
}
}
 
void Document::TryChannelCharset(nsIChannel* aChannel, int32_t& aCharsetSource,
......@@ -10440,7 +10472,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",
"x-frame-options", "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.
......
......@@ -3390,6 +3390,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) {
......@@ -4316,6 +4317,7 @@ class Document : public nsINode,
nsCOMPtr<nsIURI> mChromeXHRDocURI;
nsCOMPtr<nsIURI> mDocumentBaseURI;
nsCOMPtr<nsIURI> mChromeXHRDocBaseURI;
nsCOMPtr<nsIURI> mOnionLocationURI;
// The base domain of the document for third-party checks.
nsCString mBaseDomain;
......
......@@ -702,3 +702,12 @@ partial interface Document {
[ChromeOnly, Pure]
readonly attribute nsIPermissionDelegateHandler permDelegateHandler;
};
/**
* Extension to allows chrome JS to know whether the document has a valid
* Onion-Location that we could redirect to.
*/
partial interface Document {
[ChromeOnly] readonly attribute URI? onionLocationURI;
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment