tor-commits
Threads by month
- ----- 2025 -----
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
November 2021
- 18 participants
- 1045 discussions

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 27604: Fix addon issues when moving TB directory
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit 17f78a434fa03745da2229889bc05d14807a8c71
Author: Alex Catarineu <acat(a)torproject.org>
Date: Wed Oct 30 10:44:48 2019 +0100
Bug 27604: Fix addon issues when moving TB directory
---
toolkit/mozapps/extensions/internal/XPIProvider.jsm | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
index 7b6c904aad3f..04d57a42348e 100644
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -475,7 +475,7 @@ class XPIState {
// Builds prior to be 1512436 did not include the rootURI property.
// If we're updating from such a build, add that property now.
- if (!("rootURI" in this) && this.file) {
+ if (this.file) {
this.rootURI = getURIForResourceInFile(this.file, "").spec;
}
@@ -488,7 +488,10 @@ class XPIState {
saved.currentModifiedTime != this.lastModifiedTime
) {
this.lastModifiedTime = saved.currentModifiedTime;
- } else if (saved.currentModifiedTime === null) {
+ } else if (
+ saved.currentModifiedTime === null &&
+ (!this.file || !this.file.exists())
+ ) {
this.missing = true;
}
}
@@ -1449,6 +1452,7 @@ var XPIStates = {
if (shouldRestoreLocationData && oldState[loc.name]) {
loc.restore(oldState[loc.name]);
+ changed = changed || loc.path != oldState[loc.name].path;
}
changed = changed || loc.changed;
1
0

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 32092: Fix Tor Browser Support link in preferences
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit 4d9c0a23c3572616a9ebdf5d015d868d0b63281b
Author: Alex Catarineu <acat(a)torproject.org>
Date: Tue Oct 15 22:54:10 2019 +0200
Bug 32092: Fix Tor Browser Support link in preferences
---
browser/components/preferences/preferences.js | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
index a89fddd0306d..ce338584142e 100644
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
@@ -166,10 +166,7 @@ function init_all() {
gotoPref().then(() => {
let helpButton = document.getElementById("helpButton");
- let helpUrl =
- Services.urlFormatter.formatURLPref("app.support.baseURL") +
- "preferences";
- helpButton.setAttribute("href", helpUrl);
+ helpButton.setAttribute("href", "https://support.torproject.org/tbb");
document.getElementById("addonsButton").addEventListener("click", e => {
if (e.button >= 2) {
1
0

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 31607: App menu items stop working on macOS
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit f3a14f3930fbd43ee72923ae96b6ecad3f2ced5a
Author: Kathy Brade <brade(a)pearlcrescent.com>
Date: Thu Oct 3 10:53:43 2019 -0400
Bug 31607: App menu items stop working on macOS
Avoid re-creating the hidden window, since this causes the nsMenuBarX
object that is associated with the app menu to be freed (which in
turn causes all of the app menu items to stop working).
More detail: There should only be one hidden window.
XREMain::XRE_mainRun() contains an explicit call to create the
hidden window and that is the normal path by which it is created.
However, when Tor Launcher's wizard/progress window is opened during
startup, a hidden window is created earlier as a side effect of
calls to nsAppShellService::GetHiddenWindow(). Then, when
XREMain::XRE_mainRun() creates its hidden window, the original one
is freed which also causes the app menu's nsMenuBarX object which
is associated with that window to be destroyed. When that happens,
the menuGroupOwner property within each Cocoa menu items's MenuItemInfo
object is cleared. This breaks the link that is necessary for
NativeMenuItemTarget's menuItemHit method to dispatch a menu item
event.
---
xpfe/appshell/nsAppShellService.cpp | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/xpfe/appshell/nsAppShellService.cpp b/xpfe/appshell/nsAppShellService.cpp
index ed7c2227f4d4..e56cbfb77ca9 100644
--- a/xpfe/appshell/nsAppShellService.cpp
+++ b/xpfe/appshell/nsAppShellService.cpp
@@ -93,6 +93,10 @@ void nsAppShellService::EnsureHiddenWindow() {
NS_IMETHODIMP
nsAppShellService::CreateHiddenWindow() {
+ if (mHiddenWindow) {
+ return NS_OK;
+ }
+
if (!XRE_IsParentProcess()) {
return NS_ERROR_NOT_IMPLEMENTED;
}
1
0

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#tor
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit 699778599a86cbb7b60059680c07340acd65c315
Author: Richard Pospesel <richard(a)torproject.org>
Date: Mon Sep 16 15:25:39 2019 -0700
Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#tor
This patch adds a new about:preferences#tor page which allows modifying
bridge, proxy, and firewall settings from within Tor Browser. All of the
functionality present in tor-launcher's Network Configuration panel is
present:
- Setting built-in bridges
- Requesting bridges from BridgeDB via moat
- Using user-provided bridges
- Configuring SOCKS4, SOCKS5, and HTTP/HTTPS proxies
- Setting firewall ports
- Viewing and Copying Tor's logs
- The Networking Settings in General preferences has been removed
---
browser/components/moz.build | 1 +
browser/components/preferences/main.inc.xhtml | 54 --
browser/components/preferences/main.js | 14 -
browser/components/preferences/preferences.js | 9 +
browser/components/preferences/preferences.xhtml | 5 +
browser/components/preferences/privacy.js | 1 +
.../torpreferences/content/parseFunctions.jsm | 89 +++
.../torpreferences/content/requestBridgeDialog.jsm | 204 +++++
.../content/requestBridgeDialog.xhtml | 35 +
.../torpreferences/content/torBridgeSettings.jsm | 325 ++++++++
.../torpreferences/content/torCategory.inc.xhtml | 9 +
.../torpreferences/content/torFirewallSettings.jsm | 72 ++
.../torpreferences/content/torLogDialog.jsm | 66 ++
.../torpreferences/content/torLogDialog.xhtml | 23 +
.../components/torpreferences/content/torPane.js | 857 +++++++++++++++++++++
.../torpreferences/content/torPane.xhtml | 123 +++
.../torpreferences/content/torPreferences.css | 77 ++
.../torpreferences/content/torPreferencesIcon.svg | 5 +
.../torpreferences/content/torProxySettings.jsm | 245 ++++++
browser/components/torpreferences/jar.mn | 14 +
browser/components/torpreferences/moz.build | 1 +
browser/modules/BridgeDB.jsm | 110 +++
browser/modules/TorProtocolService.jsm | 323 ++++++++
browser/modules/moz.build | 2 +
24 files changed, 2596 insertions(+), 68 deletions(-)
diff --git a/browser/components/moz.build b/browser/components/moz.build
index 57ec3c51c5e9..b409974a965c 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -54,6 +54,7 @@ DIRS += [
"syncedtabs",
"uitour",
"urlbar",
+ "torpreferences",
"translation",
]
diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml
index a89b89f723a8..594711e61474 100644
--- a/browser/components/preferences/main.inc.xhtml
+++ b/browser/components/preferences/main.inc.xhtml
@@ -671,58 +671,4 @@
<label id="cfrFeaturesLearnMore" class="learnMore" data-l10n-id="browsing-cfr-recommendations-learn-more" is="text-link"/>
</hbox>
</groupbox>
-
-<hbox id="networkProxyCategory"
- class="subcategory"
- hidden="true"
- data-category="paneGeneral">
- <html:h1 data-l10n-id="network-settings-title"/>
-</hbox>
-
-<!-- Network Settings-->
-<groupbox id="connectionGroup" data-category="paneGeneral" hidden="true">
- <label class="search-header" hidden="true"><html:h2 data-l10n-id="network-settings-title"/></label>
-
- <hbox align="center">
- <hbox align="center" flex="1">
- <description id="connectionSettingsDescription" control="connectionSettings"/>
- <spacer width="5"/>
- <label id="connectionSettingsLearnMore" class="learnMore" is="text-link"
- data-l10n-id="network-proxy-connection-learn-more">
- </label>
- <separator orient="vertical"/>
- </hbox>
-
- <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
- <hbox>
- <button id="connectionSettings"
- is="highlightable-button"
- class="accessory-button"
- data-l10n-id="network-proxy-connection-settings"
- searchkeywords="doh trr"
- search-l10n-ids="
- connection-window.title,
- connection-proxy-option-no.label,
- connection-proxy-option-auto.label,
- connection-proxy-option-system.label,
- connection-proxy-option-manual.label,
- connection-proxy-http,
- connection-proxy-https,
- connection-proxy-http-port,
- connection-proxy-socks,
- connection-proxy-socks4,
- connection-proxy-socks5,
- connection-proxy-noproxy,
- connection-proxy-noproxy-desc,
- connection-proxy-https-sharing.label,
- connection-proxy-autotype.label,
- connection-proxy-reload.label,
- connection-proxy-autologin.label,
- connection-proxy-socks-remote-dns.label,
- connection-dns-over-https.label,
- connection-dns-over-https-url-custom.label,
- " />
- </hbox>
- </hbox>
-</groupbox>
</html:template>
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
index 2a6ba4a3d8e4..501ba9144a31 100644
--- a/browser/components/preferences/main.js
+++ b/browser/components/preferences/main.js
@@ -368,15 +368,6 @@ var gMainPane = {
});
this.updatePerformanceSettingsBox({ duringChangeEvent: false });
this.displayUseSystemLocale();
- let connectionSettingsLink = document.getElementById(
- "connectionSettingsLearnMore"
- );
- let connectionSettingsUrl =
- Services.urlFormatter.formatURLPref("app.support.baseURL") +
- "prefs-connection-settings";
- connectionSettingsLink.setAttribute("href", connectionSettingsUrl);
- this.updateProxySettingsUI();
- initializeProxyUI(gMainPane);
if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
gMainPane.initBrowserLocale();
@@ -510,11 +501,6 @@ var gMainPane = {
"change",
gMainPane.updateHardwareAcceleration.bind(gMainPane)
);
- setEventListener(
- "connectionSettings",
- "command",
- gMainPane.showConnections
- );
setEventListener(
"browserContainersCheckbox",
"command",
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
index 91e9e469cea2..a89fddd0306d 100644
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
@@ -13,6 +13,7 @@
/* import-globals-from findInPage.js */
/* import-globals-from ../../base/content/utilityOverlay.js */
/* import-globals-from ../../../toolkit/content/preferencesBindings.js */
+/* import-globals-from ../torpreferences/content/torPane.js */
"use strict";
@@ -136,6 +137,14 @@ function init_all() {
register_module("paneSync", gSyncPane);
}
register_module("paneSearchResults", gSearchResultsPane);
+ if (gTorPane.enabled) {
+ document.getElementById("category-tor").hidden = false;
+ register_module("paneTor", gTorPane);
+ } else {
+ // Remove the pane from the DOM so it doesn't get incorrectly included in search results.
+ document.getElementById("template-paneTor").remove();
+ }
+
gSearchResultsPane.init();
gMainPane.preInit();
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
index 2d29b382350d..0139abf95cbd 100644
--- a/browser/components/preferences/preferences.xhtml
+++ b/browser/components/preferences/preferences.xhtml
@@ -13,6 +13,7 @@
<?xml-stylesheet href="chrome://browser/skin/preferences/containers.css"?>
<?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?>
<?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
<!DOCTYPE html [
<!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd">
@@ -155,6 +156,9 @@
<image class="category-icon"/>
<label class="category-name" flex="1" data-l10n-id="pane-experimental-title"></label>
</richlistitem>
+
+#include ../torpreferences/content/torCategory.inc.xhtml
+
</richlistbox>
<spacer flex="1"/>
@@ -208,6 +212,7 @@
#include containers.inc.xhtml
#include sync.inc.xhtml
#include experimental.inc.xhtml
+#include ../torpreferences/content/torPane.xhtml
</vbox>
</vbox>
</vbox>
diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js
index 42b899e0552f..bce7bb7e8a9c 100644
--- a/browser/components/preferences/privacy.js
+++ b/browser/components/preferences/privacy.js
@@ -80,6 +80,7 @@ XPCOMUtils.defineLazyGetter(this, "AlertsServiceDND", function() {
}
});
+// TODO: module import via ChromeUtils.defineModuleGetter
XPCOMUtils.defineLazyScriptGetter(
this,
["SecurityLevelPreferences"],
diff --git a/browser/components/torpreferences/content/parseFunctions.jsm b/browser/components/torpreferences/content/parseFunctions.jsm
new file mode 100644
index 000000000000..954759de63a5
--- /dev/null
+++ b/browser/components/torpreferences/content/parseFunctions.jsm
@@ -0,0 +1,89 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "parsePort",
+ "parseAddrPort",
+ "parseUsernamePassword",
+ "parseAddrPortList",
+ "parseBridgeStrings",
+ "parsePortList",
+];
+
+// expects a string representation of an integer from 1 to 65535
+let parsePort = function(aPort) {
+ // ensure port string is a valid positive integer
+ const validIntRegex = /^[0-9]+$/;
+ if (!validIntRegex.test(aPort)) {
+ throw new Error(`Invalid PORT string : '${aPort}'`);
+ }
+
+ // ensure port value is on valid range
+ let port = Number.parseInt(aPort);
+ if (port < 1 || port > 65535) {
+ throw new Error(
+ `Invalid PORT value, needs to be on range [1,65535] : '${port}'`
+ );
+ }
+
+ return port;
+};
+// expects a string in the format: "ADDRESS:PORT"
+let parseAddrPort = function(aAddrColonPort) {
+ let tokens = aAddrColonPort.split(":");
+ if (tokens.length != 2) {
+ throw new Error(`Invalid ADDRESS:PORT string : '${aAddrColonPort}'`);
+ }
+ let address = tokens[0];
+ let port = parsePort(tokens[1]);
+ return [address, port];
+};
+
+// expects a string in the format: "USERNAME:PASSWORD"
+// split on the first colon and any subsequent go into password
+let parseUsernamePassword = function(aUsernameColonPassword) {
+ let colonIndex = aUsernameColonPassword.indexOf(":");
+ if (colonIndex < 0) {
+ // we don't log the contents of the potentially password containing string
+ throw new Error("Invalid USERNAME:PASSWORD string");
+ }
+
+ let username = aUsernameColonPassword.substring(0, colonIndex);
+ let password = aUsernameColonPassword.substring(colonIndex + 1);
+
+ return [username, password];
+};
+
+// expects a string in the format: ADDRESS:PORT,ADDRESS:PORT,...
+// returns array of ports (as ints)
+let parseAddrPortList = function(aAddrPortList) {
+ let addrPorts = aAddrPortList.split(",");
+ // parse ADDRESS:PORT string and only keep the port (second element in returned array)
+ let retval = addrPorts.map(addrPort => parseAddrPort(addrPort)[1]);
+ return retval;
+};
+
+// expects a '/n' or '/r/n' delimited bridge string, which we split and trim
+// each bridge string can also optionally have 'bridge' at the beginning ie:
+// bridge $(type) $(address):$(port) $(certificate)
+// we strip out the 'bridge' prefix here
+let parseBridgeStrings = function(aBridgeStrings) {
+
+ // replace carriage returns ('\r') with new lines ('\n')
+ aBridgeStrings = aBridgeStrings.replace(/\r/g, "\n");
+ // then replace contiguous new lines ('\n') with a single one
+ aBridgeStrings = aBridgeStrings.replace(/[\n]+/g, "\n");
+
+ // split on the newline and for each bridge string: trim, remove starting 'bridge' string
+ // finally discard entries that are empty strings; empty strings could occur if we receive
+ // a new line containing only whitespace
+ let splitStrings = aBridgeStrings.split("\n");
+ return splitStrings.map(val => val.trim().replace(/^bridge\s+/i, ""))
+ .filter(bridgeString => bridgeString != "");
+};
+
+// expecting a ',' delimited list of ints with possible white space between
+// returns an array of ints
+let parsePortList = function(aPortListString) {
+ let splitStrings = aPortListString.split(",");
+ return splitStrings.map(val => parsePort(val.trim()));
+};
diff --git a/browser/components/torpreferences/content/requestBridgeDialog.jsm b/browser/components/torpreferences/content/requestBridgeDialog.jsm
new file mode 100644
index 000000000000..807d46cdfb18
--- /dev/null
+++ b/browser/components/torpreferences/content/requestBridgeDialog.jsm
@@ -0,0 +1,204 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["RequestBridgeDialog"];
+
+const { BridgeDB } = ChromeUtils.import("resource:///modules/BridgeDB.jsm");
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+class RequestBridgeDialog {
+ constructor() {
+ this._dialog = null;
+ this._submitButton = null;
+ this._dialogDescription = null;
+ this._captchaImage = null;
+ this._captchaEntryTextbox = null;
+ this._captchaRefreshButton = null;
+ this._incorrectCaptchaHbox = null;
+ this._incorrectCaptchaLabel = null;
+ this._bridges = [];
+ this._proxyURI = null;
+ }
+
+ static get selectors() {
+ return {
+ submitButton:
+ "accept" /* not really a selector but a key for dialog's getButton */,
+ dialogDescription: "description#torPreferences-requestBridge-description",
+ captchaImage: "image#torPreferences-requestBridge-captchaImage",
+ captchaEntryTextbox: "input#torPreferences-requestBridge-captchaTextbox",
+ refreshCaptchaButton:
+ "button#torPreferences-requestBridge-refreshCaptchaButton",
+ incorrectCaptchaHbox:
+ "hbox#torPreferences-requestBridge-incorrectCaptchaHbox",
+ incorrectCaptchaLabel:
+ "label#torPreferences-requestBridge-incorrectCaptchaError",
+ };
+ }
+
+ _populateXUL(dialog) {
+ const selectors = RequestBridgeDialog.selectors;
+
+ this._dialog = dialog;
+ const dialogWin = dialog.parentElement;
+ dialogWin.setAttribute(
+ "title",
+ TorStrings.settings.requestBridgeDialogTitle
+ );
+ // user may have opened a Request Bridge dialog in another tab, so update the
+ // CAPTCHA image or close out the dialog if we have a bridge list
+ this._dialog.addEventListener("focusin", () => {
+ const uri = BridgeDB.currentCaptchaImage;
+ const bridges = BridgeDB.currentBridges;
+
+ // new captcha image
+ if (uri) {
+ this._setcaptchaImage(uri);
+ } else if (bridges) {
+ this._bridges = bridges;
+ this._submitButton.disabled = false;
+ this._dialog.cancelDialog();
+ }
+ });
+
+ this._submitButton = this._dialog.getButton(selectors.submitButton);
+ this._submitButton.setAttribute("label", TorStrings.settings.submitCaptcha);
+ this._submitButton.disabled = true;
+ this._dialog.addEventListener("dialogaccept", e => {
+ e.preventDefault();
+ this.onSubmitCaptcha();
+ });
+
+ this._dialogDescription = this._dialog.querySelector(
+ selectors.dialogDescription
+ );
+ this._dialogDescription.textContent =
+ TorStrings.settings.contactingBridgeDB;
+
+ this._captchaImage = this._dialog.querySelector(selectors.captchaImage);
+
+ // request captcha from bridge db
+ BridgeDB.requestNewCaptchaImage(this._proxyURI).then(uri => {
+ this._setcaptchaImage(uri);
+ });
+
+ this._captchaEntryTextbox = this._dialog.querySelector(
+ selectors.captchaEntryTextbox
+ );
+ this._captchaEntryTextbox.setAttribute(
+ "placeholder",
+ TorStrings.settings.captchaTextboxPlaceholder
+ );
+ this._captchaEntryTextbox.disabled = true;
+ // disable submit if entry textbox is empty
+ this._captchaEntryTextbox.oninput = () => {
+ this._submitButton.disabled = this._captchaEntryTextbox.value == "";
+ };
+
+ this._captchaRefreshButton = this._dialog.querySelector(
+ selectors.refreshCaptchaButton
+ );
+ this._captchaRefreshButton.disabled = true;
+
+ this._incorrectCaptchaHbox = this._dialog.querySelector(
+ selectors.incorrectCaptchaHbox
+ );
+ this._incorrectCaptchaLabel = this._dialog.querySelector(
+ selectors.incorrectCaptchaLabel
+ );
+ this._incorrectCaptchaLabel.setAttribute(
+ "value",
+ TorStrings.settings.incorrectCaptcha
+ );
+
+ return true;
+ }
+
+ _setcaptchaImage(uri) {
+ if (uri != this._captchaImage.src) {
+ this._captchaImage.src = uri;
+ this._dialogDescription.textContent = TorStrings.settings.solveTheCaptcha;
+ this._setUIDisabled(false);
+ this._captchaEntryTextbox.focus();
+ this._captchaEntryTextbox.select();
+ }
+ }
+
+ _setUIDisabled(disabled) {
+ this._submitButton.disabled = this._captchaGuessIsEmpty() || disabled;
+ this._captchaEntryTextbox.disabled = disabled;
+ this._captchaRefreshButton.disabled = disabled;
+ }
+
+ _captchaGuessIsEmpty() {
+ return this._captchaEntryTextbox.value == "";
+ }
+
+ init(window, dialog) {
+ // defer to later until firefox has populated the dialog with all our elements
+ window.setTimeout(() => {
+ this._populateXUL(dialog);
+ }, 0);
+ }
+
+ close() {
+ BridgeDB.close();
+ }
+
+ /*
+ Event Handlers
+ */
+ onSubmitCaptcha() {
+ let captchaText = this._captchaEntryTextbox.value.trim();
+ // noop if the field is empty
+ if (captchaText == "") {
+ return;
+ }
+
+ // freeze ui while we make request
+ this._setUIDisabled(true);
+ this._incorrectCaptchaHbox.style.visibility = "hidden";
+
+ BridgeDB.submitCaptchaGuess(captchaText)
+ .then(aBridges => {
+ this._bridges = aBridges;
+
+ this._submitButton.disabled = false;
+ // This was successful, but use cancelDialog() to close, since
+ // we intercept the `dialogaccept` event.
+ this._dialog.cancelDialog();
+ })
+ .catch(aError => {
+ this._bridges = [];
+ this._setUIDisabled(false);
+ this._incorrectCaptchaHbox.style.visibility = "visible";
+ });
+ }
+
+ onRefreshCaptcha() {
+ this._setUIDisabled(true);
+ this._captchaImage.src = "";
+ this._dialogDescription.textContent =
+ TorStrings.settings.contactingBridgeDB;
+ this._captchaEntryTextbox.value = "";
+ this._incorrectCaptchaHbox.style.visibility = "hidden";
+
+ BridgeDB.requestNewCaptchaImage(this._proxyURI).then(uri => {
+ this._setcaptchaImage(uri);
+ });
+ }
+
+ openDialog(gSubDialog, aProxyURI, aCloseCallback) {
+ this._proxyURI = aProxyURI;
+ gSubDialog.open(
+ "chrome://browser/content/torpreferences/requestBridgeDialog.xhtml",
+ {
+ features: "resizable=yes",
+ closingCallback: () => {
+ this.close();
+ aCloseCallback(this._bridges);
+ }
+ },
+ this,
+ );
+ }
+}
diff --git a/browser/components/torpreferences/content/requestBridgeDialog.xhtml b/browser/components/torpreferences/content/requestBridgeDialog.xhtml
new file mode 100644
index 000000000000..64c4507807fb
--- /dev/null
+++ b/browser/components/torpreferences/content/requestBridgeDialog.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
+
+<window type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<dialog id="torPreferences-requestBridge-dialog"
+ buttons="accept,cancel">
+ <!-- ok, so ​ is a zero-width space. We need to have *something* in the innerText so that XUL knows how tall the
+ description node is so that it can determine how large to make the dialog element's inner draw area. If we have
+ nothing in the innerText, then it collapse to 0 height, and the contents of the dialog ends up partially hidden >:( -->
+ <description id="torPreferences-requestBridge-description">​</description>
+ <!-- init to transparent 400x125 png -->
+ <image id="torPreferences-requestBridge-captchaImage" flex="1"/>
+ <hbox id="torPreferences-requestBridge-inputHbox">
+ <html:input id="torPreferences-requestBridge-captchaTextbox" type="text" style="-moz-box-flex: 1;"/>
+ <button id="torPreferences-requestBridge-refreshCaptchaButton"
+ image="chrome://browser/skin/reload.svg"
+ oncommand="requestBridgeDialog.onRefreshCaptcha();"/>
+ </hbox>
+ <hbox id="torPreferences-requestBridge-incorrectCaptchaHbox" align="center">
+ <image id="torPreferences-requestBridge-errorIcon" />
+ <label id="torPreferences-requestBridge-incorrectCaptchaError" flex="1"/>
+ </hbox>
+ <script type="application/javascript"><![CDATA[
+ "use strict";
+
+ let requestBridgeDialog = window.arguments[0];
+ let dialog = document.getElementById("torPreferences-requestBridge-dialog");
+ requestBridgeDialog.init(window, dialog);
+ ]]></script>
+</dialog>
+</window>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torBridgeSettings.jsm b/browser/components/torpreferences/content/torBridgeSettings.jsm
new file mode 100644
index 000000000000..ceb61d3ec972
--- /dev/null
+++ b/browser/components/torpreferences/content/torBridgeSettings.jsm
@@ -0,0 +1,325 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "TorBridgeSource",
+ "TorBridgeSettings",
+ "makeTorBridgeSettingsNone",
+ "makeTorBridgeSettingsBuiltin",
+ "makeTorBridgeSettingsBridgeDB",
+ "makeTorBridgeSettingsUserProvided",
+];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TorProtocolService } = ChromeUtils.import(
+ "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+const TorBridgeSource = {
+ NONE: "NONE",
+ BUILTIN: "BUILTIN",
+ BRIDGEDB: "BRIDGEDB",
+ USERPROVIDED: "USERPROVIDED",
+};
+
+class TorBridgeSettings {
+ constructor() {
+ this._bridgeSource = TorBridgeSource.NONE;
+ this._selectedDefaultBridgeType = null;
+ this._bridgeStrings = [];
+ }
+
+ get selectedDefaultBridgeType() {
+ if (this._bridgeSource == TorBridgeSource.BUILTIN) {
+ return this._selectedDefaultBridgeType;
+ }
+ return undefined;
+ }
+
+ get bridgeSource() {
+ return this._bridgeSource;
+ }
+
+ // for display
+ get bridgeStrings() {
+ return this._bridgeStrings.join("\n");
+ }
+
+ // raw
+ get bridgeStringsArray() {
+ return this._bridgeStrings;
+ }
+
+ static get defaultBridgeTypes() {
+ if (TorBridgeSettings._defaultBridgeTypes) {
+ return TorBridgeSettings._defaultBridgeTypes;
+ }
+
+ let bridgeListBranch = Services.prefs.getBranch(
+ TorStrings.preferenceBranches.defaultBridge
+ );
+ let bridgePrefs = bridgeListBranch.getChildList("", {});
+
+ // an unordered set for shoving bridge types into
+ let bridgeTypes = new Set();
+ // look for keys ending in ".N" and treat string before that as the bridge type
+ const pattern = /\.[0-9]+$/;
+ for (const key of bridgePrefs) {
+ const offset = key.search(pattern);
+ if (offset != -1) {
+ const bt = key.substring(0, offset);
+ bridgeTypes.add(bt);
+ }
+ }
+
+ // recommended bridge type goes first in the list
+ let recommendedBridgeType = Services.prefs.getCharPref(
+ TorStrings.preferenceKeys.recommendedBridgeType,
+ null
+ );
+
+ let retval = [];
+ if (recommendedBridgeType && bridgeTypes.has(recommendedBridgeType)) {
+ retval.push(recommendedBridgeType);
+ }
+
+ for (const bridgeType of bridgeTypes.values()) {
+ if (bridgeType != recommendedBridgeType) {
+ retval.push(bridgeType);
+ }
+ }
+
+ // cache off
+ TorBridgeSettings._defaultBridgeTypes = retval;
+ return retval;
+ }
+
+ _readDefaultBridges(aBridgeType) {
+ let bridgeBranch = Services.prefs.getBranch(
+ TorStrings.preferenceBranches.defaultBridge
+ );
+ let bridgeBranchPrefs = bridgeBranch.getChildList("", {});
+
+ let retval = [];
+
+ // regex matches against strings ending in ".N" where N is a positive integer
+ let pattern = /\.[0-9]+$/;
+ for (const key of bridgeBranchPrefs) {
+ // verify the location of the match is the correct offset required for aBridgeType
+ // to fit, and that the string begins with aBridgeType
+ if (
+ key.search(pattern) == aBridgeType.length &&
+ key.startsWith(aBridgeType)
+ ) {
+ let bridgeStr = bridgeBranch.getCharPref(key);
+ retval.push(bridgeStr);
+ }
+ }
+
+ // fisher-yates shuffle
+ // shuffle so that Tor Browser users don't all try the built-in bridges in the same order
+ for (let i = retval.length - 1; i > 0; --i) {
+ // number n such that 0.0 <= n < 1.0
+ const n = Math.random();
+ // integer j such that 0 <= j <= i
+ const j = Math.floor(n * (i + 1));
+
+ // swap values at indices i and j
+ const tmp = retval[i];
+ retval[i] = retval[j];
+ retval[j] = tmp;
+ }
+
+ return retval;
+ }
+
+ _readBridgeDBBridges() {
+ let bridgeBranch = Services.prefs.getBranch(
+ `${TorStrings.preferenceBranches.bridgeDBBridges}`
+ );
+ let bridgeBranchPrefs = bridgeBranch.getChildList("", {});
+ // the child prefs do not come in any particular order so sort the keys
+ // so the values can be compared to what we get out off torrc
+ bridgeBranchPrefs.sort();
+
+ // just assume all of the prefs under the parent point to valid bridge string
+ let retval = bridgeBranchPrefs.map(key =>
+ bridgeBranch.getCharPref(key).trim()
+ );
+
+ return retval;
+ }
+
+ _readTorrcBridges() {
+ let bridgeList = TorProtocolService.readStringArraySetting(
+ TorStrings.configKeys.bridgeList
+ );
+
+ let retval = [];
+ for (const line of bridgeList) {
+ let trimmedLine = line.trim();
+ if (trimmedLine) {
+ retval.push(trimmedLine);
+ }
+ }
+
+ return retval;
+ }
+
+ // analagous to initBridgeSettings()
+ readSettings() {
+ // restore to defaults
+ this._bridgeSource = TorBridgeSource.NONE;
+ this._selectedDefaultBridgeType = null;
+ this._bridgeStrings = [];
+
+ // So the way tor-launcher determines the origin of the configured bridges is a bit
+ // weird and depends on inferring our scenario based on some firefox prefs and the
+ // relationship between the saved list of bridges in about:config vs the list saved in torrc
+
+ // first off, if "extensions.torlauncher.default_bridge_type" is set to one of our
+ // builtin default types (obfs4, meek-azure, snowflake, etc) then we provide the
+ // bridges in "extensions.torlauncher.default_bridge.*" (filtered by our default_bridge_type)
+
+ // next, we compare the list of bridges saved in torrc to the bridges stored in the
+ // "extensions.torlauncher.bridgedb_bridge."" branch. If they match *exactly* then we assume
+ // the bridges were retrieved from BridgeDB and use those. If the torrc list is empty then we know
+ // we have no bridge settings
+
+ // finally, if none of the previous conditions are not met, it is assumed the bridges stored in
+ // torrc are user-provided
+
+ // what we should(?) do once we excise tor-launcher entirely is explicitly store an int/enum in
+ // about:config that tells us which scenario we are in so we don't have to guess
+
+ let defaultBridgeType = Services.prefs.getCharPref(
+ TorStrings.preferenceKeys.defaultBridgeType,
+ null
+ );
+
+ // check if source is BUILTIN
+ if (defaultBridgeType) {
+ this._bridgeStrings = this._readDefaultBridges(defaultBridgeType);
+ this._bridgeSource = TorBridgeSource.BUILTIN;
+ this._selectedDefaultBridgeType = defaultBridgeType;
+ return;
+ }
+
+ let torrcBridges = this._readTorrcBridges();
+
+ // no stored bridges means no bridge is in use
+ if (torrcBridges.length == 0) {
+ this._bridgeStrings = [];
+ this._bridgeSource = TorBridgeSource.NONE;
+ return;
+ }
+
+ let bridgedbBridges = this._readBridgeDBBridges();
+
+ // if these two lists are equal then we got our bridges from bridgedb
+ // ie: same element in identical order
+ let arraysEqual = (left, right) => {
+ if (left.length != right.length) {
+ return false;
+ }
+ const length = left.length;
+ for (let i = 0; i < length; ++i) {
+ if (left[i] != right[i]) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ // agreement between prefs and torrc means bridgedb bridges
+ if (arraysEqual(torrcBridges, bridgedbBridges)) {
+ this._bridgeStrings = torrcBridges;
+ this._bridgeSource = TorBridgeSource.BRIDGEDB;
+ return;
+ }
+
+ // otherwise they must be user provided
+ this._bridgeStrings = torrcBridges;
+ this._bridgeSource = TorBridgeSource.USERPROVIDED;
+ }
+
+ writeSettings() {
+ let settingsObject = new Map();
+
+ // init tor bridge settings to null
+ settingsObject.set(TorStrings.configKeys.useBridges, null);
+ settingsObject.set(TorStrings.configKeys.bridgeList, null);
+
+ // clear bridge related firefox prefs
+ Services.prefs.setCharPref(TorStrings.preferenceKeys.defaultBridgeType, "");
+ let bridgeBranch = Services.prefs.getBranch(
+ `${TorStrings.preferenceBranches.bridgeDBBridges}`
+ );
+ let bridgeBranchPrefs = bridgeBranch.getChildList("", {});
+ for (const pref of bridgeBranchPrefs) {
+ Services.prefs.clearUserPref(
+ `${TorStrings.preferenceBranches.bridgeDBBridges}${pref}`
+ );
+ }
+
+ switch (this._bridgeSource) {
+ case TorBridgeSource.BUILTIN:
+ // set builtin bridge type to use in prefs
+ Services.prefs.setCharPref(
+ TorStrings.preferenceKeys.defaultBridgeType,
+ this._selectedDefaultBridgeType
+ );
+ break;
+ case TorBridgeSource.BRIDGEDB:
+ // save bridges off to prefs
+ for (let i = 0; i < this.bridgeStringsArray.length; ++i) {
+ Services.prefs.setCharPref(
+ `${TorStrings.preferenceBranches.bridgeDBBridges}${i}`,
+ this.bridgeStringsArray[i]
+ );
+ }
+ break;
+ }
+
+ // write over our bridge list if bridges are enabled
+ if (this._bridgeSource != TorBridgeSource.NONE) {
+ settingsObject.set(TorStrings.configKeys.useBridges, true);
+ settingsObject.set(
+ TorStrings.configKeys.bridgeList,
+ this.bridgeStringsArray
+ );
+ }
+ TorProtocolService.writeSettings(settingsObject);
+ }
+}
+
+function makeTorBridgeSettingsNone() {
+ return new TorBridgeSettings();
+}
+
+function makeTorBridgeSettingsBuiltin(aBridgeType) {
+ let retval = new TorBridgeSettings();
+ retval._bridgeSource = TorBridgeSource.BUILTIN;
+ retval._selectedDefaultBridgeType = aBridgeType;
+ retval._bridgeStrings = retval._readDefaultBridges(aBridgeType);
+
+ return retval;
+}
+
+function makeTorBridgeSettingsBridgeDB(aBridges) {
+ let retval = new TorBridgeSettings();
+ retval._bridgeSource = TorBridgeSource.BRIDGEDB;
+ retval._selectedDefaultBridgeType = null;
+ retval._bridgeStrings = aBridges;
+
+ return retval;
+}
+
+function makeTorBridgeSettingsUserProvided(aBridges) {
+ let retval = new TorBridgeSettings();
+ retval._bridgeSource = TorBridgeSource.USERPROVIDED;
+ retval._selectedDefaultBridgeType = null;
+ retval._bridgeStrings = aBridges;
+
+ return retval;
+}
diff --git a/browser/components/torpreferences/content/torCategory.inc.xhtml b/browser/components/torpreferences/content/torCategory.inc.xhtml
new file mode 100644
index 000000000000..abe56200f571
--- /dev/null
+++ b/browser/components/torpreferences/content/torCategory.inc.xhtml
@@ -0,0 +1,9 @@
+<richlistitem id="category-tor"
+ class="category"
+ value="paneTor"
+ helpTopic="prefs-tor"
+ align="center"
+ hidden="true">
+ <image class="category-icon"/>
+ <label id="torPreferences-labelCategory" class="category-name" flex="1" value="Tor"/>
+</richlistitem>
diff --git a/browser/components/torpreferences/content/torFirewallSettings.jsm b/browser/components/torpreferences/content/torFirewallSettings.jsm
new file mode 100644
index 000000000000..e77f18ef2fae
--- /dev/null
+++ b/browser/components/torpreferences/content/torFirewallSettings.jsm
@@ -0,0 +1,72 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "TorFirewallSettings",
+ "makeTorFirewallSettingsNone",
+ "makeTorFirewallSettingsCustom",
+];
+
+const { TorProtocolService } = ChromeUtils.import(
+ "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+const { parseAddrPortList } = ChromeUtils.import(
+ "chrome://browser/content/torpreferences/parseFunctions.jsm"
+);
+
+class TorFirewallSettings {
+ constructor() {
+ this._allowedPorts = [];
+ }
+
+ get portsConfigurationString() {
+ let portStrings = this._allowedPorts.map(port => `*:${port}`);
+ return portStrings.join(",");
+ }
+
+ get commaSeparatedListString() {
+ return this._allowedPorts.join(",");
+ }
+
+ get hasPorts() {
+ return this._allowedPorts.length > 0;
+ }
+
+ readSettings() {
+ let addressPortList = TorProtocolService.readStringSetting(
+ TorStrings.configKeys.reachableAddresses
+ );
+
+ let allowedPorts = [];
+ if (addressPortList) {
+ allowedPorts = parseAddrPortList(addressPortList);
+ }
+ this._allowedPorts = allowedPorts;
+ }
+
+ writeSettings() {
+ let settingsObject = new Map();
+
+ // init to null so Tor daemon resets if no ports
+ settingsObject.set(TorStrings.configKeys.reachableAddresses, null);
+
+ if (this._allowedPorts.length > 0) {
+ settingsObject.set(
+ TorStrings.configKeys.reachableAddresses,
+ this.portsConfigurationString
+ );
+ }
+
+ TorProtocolService.writeSettings(settingsObject);
+ }
+}
+
+function makeTorFirewallSettingsNone() {
+ return new TorFirewallSettings();
+}
+
+function makeTorFirewallSettingsCustom(aPortsList) {
+ let retval = new TorFirewallSettings();
+ retval._allowedPorts = aPortsList;
+ return retval;
+}
diff --git a/browser/components/torpreferences/content/torLogDialog.jsm b/browser/components/torpreferences/content/torLogDialog.jsm
new file mode 100644
index 000000000000..ecc684d878c2
--- /dev/null
+++ b/browser/components/torpreferences/content/torLogDialog.jsm
@@ -0,0 +1,66 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorLogDialog"];
+
+const { TorProtocolService } = ChromeUtils.import(
+ "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+class TorLogDialog {
+ constructor() {
+ this._dialog = null;
+ this._logTextarea = null;
+ this._copyLogButton = null;
+ }
+
+ static get selectors() {
+ return {
+ copyLogButton: "extra1",
+ logTextarea: "textarea#torPreferences-torDialog-textarea",
+ };
+ }
+
+ _populateXUL(aDialog) {
+ this._dialog = aDialog;
+ const dialogWin = this._dialog.parentElement;
+ dialogWin.setAttribute("title", TorStrings.settings.torLogDialogTitle);
+
+ this._logTextarea = this._dialog.querySelector(
+ TorLogDialog.selectors.logTextarea
+ );
+
+ this._copyLogButton = this._dialog.getButton(
+ TorLogDialog.selectors.copyLogButton
+ );
+ this._copyLogButton.setAttribute("label", TorStrings.settings.copyLog);
+ this._copyLogButton.addEventListener("command", () => {
+ this.copyTorLog();
+ });
+
+ this._logTextarea.value = TorProtocolService.getLog();
+ }
+
+ init(window, aDialog) {
+ // defer to later until firefox has populated the dialog with all our elements
+ window.setTimeout(() => {
+ this._populateXUL(aDialog);
+ }, 0);
+ }
+
+ copyTorLog() {
+ // Copy tor log messages to the system clipboard.
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(this._logTextarea.value);
+ }
+
+ openDialog(gSubDialog) {
+ gSubDialog.open(
+ "chrome://browser/content/torpreferences/torLogDialog.xhtml",
+ { features: "resizable=yes" },
+ this
+ );
+ }
+}
diff --git a/browser/components/torpreferences/content/torLogDialog.xhtml b/browser/components/torpreferences/content/torLogDialog.xhtml
new file mode 100644
index 000000000000..9c17f8132978
--- /dev/null
+++ b/browser/components/torpreferences/content/torLogDialog.xhtml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
+
+<window type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<dialog id="torPreferences-torLog-dialog"
+ buttons="accept,extra1">
+ <html:textarea
+ id="torPreferences-torDialog-textarea"
+ multiline="true"
+ readonly="true"/>
+ <script type="application/javascript"><![CDATA[
+ "use strict";
+
+ let torLogDialog = window.arguments[0];
+ let dialog = document.getElementById("torPreferences-torLog-dialog");
+ torLogDialog.init(window, dialog);
+ ]]></script>
+</dialog>
+</window>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torPane.js b/browser/components/torpreferences/content/torPane.js
new file mode 100644
index 000000000000..49054b5dac6a
--- /dev/null
+++ b/browser/components/torpreferences/content/torPane.js
@@ -0,0 +1,857 @@
+"use strict";
+
+const { TorProtocolService } = ChromeUtils.import(
+ "resource:///modules/TorProtocolService.jsm"
+);
+
+const {
+ TorBridgeSource,
+ TorBridgeSettings,
+ makeTorBridgeSettingsNone,
+ makeTorBridgeSettingsBuiltin,
+ makeTorBridgeSettingsBridgeDB,
+ makeTorBridgeSettingsUserProvided,
+} = ChromeUtils.import(
+ "chrome://browser/content/torpreferences/torBridgeSettings.jsm"
+);
+
+const {
+ TorProxyType,
+ TorProxySettings,
+ makeTorProxySettingsNone,
+ makeTorProxySettingsSocks4,
+ makeTorProxySettingsSocks5,
+ makeTorProxySettingsHTTPS,
+} = ChromeUtils.import(
+ "chrome://browser/content/torpreferences/torProxySettings.jsm"
+);
+const {
+ TorFirewallSettings,
+ makeTorFirewallSettingsNone,
+ makeTorFirewallSettingsCustom,
+} = ChromeUtils.import(
+ "chrome://browser/content/torpreferences/torFirewallSettings.jsm"
+);
+
+const { TorLogDialog } = ChromeUtils.import(
+ "chrome://browser/content/torpreferences/torLogDialog.jsm"
+);
+
+const { RequestBridgeDialog } = ChromeUtils.import(
+ "chrome://browser/content/torpreferences/requestBridgeDialog.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "TorStrings",
+ "resource:///modules/TorStrings.jsm"
+);
+
+const { parsePort, parseBridgeStrings, parsePortList } = ChromeUtils.import(
+ "chrome://browser/content/torpreferences/parseFunctions.jsm"
+);
+
+/*
+ Tor Pane
+
+ Code for populating the XUL in about:preferences#tor, handling input events, interfacing with tor-launcher
+*/
+const gTorPane = (function() {
+ /* CSS selectors for all of the Tor Network DOM elements we need to access */
+ const selectors = {
+ category: {
+ title: "label#torPreferences-labelCategory",
+ },
+ torPreferences: {
+ header: "h1#torPreferences-header",
+ description: "span#torPreferences-description",
+ learnMore: "label#torPreferences-learnMore",
+ },
+ bridges: {
+ header: "h2#torPreferences-bridges-header",
+ description: "span#torPreferences-bridges-description",
+ learnMore: "label#torPreferences-bridges-learnMore",
+ useBridgeCheckbox: "checkbox#torPreferences-bridges-toggle",
+ bridgeSelectionRadiogroup:
+ "radiogroup#torPreferences-bridges-bridgeSelection",
+ builtinBridgeOption: "radio#torPreferences-bridges-radioBuiltin",
+ builtinBridgeList: "menulist#torPreferences-bridges-builtinList",
+ requestBridgeOption: "radio#torPreferences-bridges-radioRequestBridge",
+ requestBridgeButton: "button#torPreferences-bridges-buttonRequestBridge",
+ requestBridgeTextarea:
+ "textarea#torPreferences-bridges-textareaRequestBridge",
+ provideBridgeOption: "radio#torPreferences-bridges-radioProvideBridge",
+ provideBridgeDescription:
+ "description#torPreferences-bridges-descriptionProvideBridge",
+ provideBridgeTextarea:
+ "textarea#torPreferences-bridges-textareaProvideBridge",
+ },
+ advanced: {
+ header: "h2#torPreferences-advanced-header",
+ description: "span#torPreferences-advanced-description",
+ learnMore: "label#torPreferences-advanced-learnMore",
+ useProxyCheckbox: "checkbox#torPreferences-advanced-toggleProxy",
+ proxyTypeLabel: "label#torPreferences-localProxy-type",
+ proxyTypeList: "menulist#torPreferences-localProxy-builtinList",
+ proxyAddressLabel: "label#torPreferences-localProxy-address",
+ proxyAddressTextbox: "input#torPreferences-localProxy-textboxAddress",
+ proxyPortLabel: "label#torPreferences-localProxy-port",
+ proxyPortTextbox: "input#torPreferences-localProxy-textboxPort",
+ proxyUsernameLabel: "label#torPreferences-localProxy-username",
+ proxyUsernameTextbox: "input#torPreferences-localProxy-textboxUsername",
+ proxyPasswordLabel: "label#torPreferences-localProxy-password",
+ proxyPasswordTextbox: "input#torPreferences-localProxy-textboxPassword",
+ useFirewallCheckbox: "checkbox#torPreferences-advanced-toggleFirewall",
+ firewallAllowedPortsLabel: "label#torPreferences-advanced-allowedPorts",
+ firewallAllowedPortsTextbox:
+ "input#torPreferences-advanced-textboxAllowedPorts",
+ torLogsLabel: "label#torPreferences-torLogs",
+ torLogsButton: "button#torPreferences-buttonTorLogs",
+ },
+ }; /* selectors */
+
+ let retval = {
+ // cached frequently accessed DOM elements
+ _useBridgeCheckbox: null,
+ _bridgeSelectionRadiogroup: null,
+ _builtinBridgeOption: null,
+ _builtinBridgeMenulist: null,
+ _requestBridgeOption: null,
+ _requestBridgeButton: null,
+ _requestBridgeTextarea: null,
+ _provideBridgeOption: null,
+ _provideBridgeTextarea: null,
+ _useProxyCheckbox: null,
+ _proxyTypeLabel: null,
+ _proxyTypeMenulist: null,
+ _proxyAddressLabel: null,
+ _proxyAddressTextbox: null,
+ _proxyPortLabel: null,
+ _proxyPortTextbox: null,
+ _proxyUsernameLabel: null,
+ _proxyUsernameTextbox: null,
+ _proxyPasswordLabel: null,
+ _proxyPasswordTextbox: null,
+ _useFirewallCheckbox: null,
+ _allowedPortsLabel: null,
+ _allowedPortsTextbox: null,
+
+ // tor network settings
+ _bridgeSettings: null,
+ _proxySettings: null,
+ _firewallSettings: null,
+
+ // disables the provided list of elements
+ _setElementsDisabled(elements, disabled) {
+ for (let currentElement of elements) {
+ currentElement.disabled = disabled;
+ }
+ },
+
+ // populate xul with strings and cache the relevant elements
+ _populateXUL() {
+ // saves tor settings to disk when navigate away from about:preferences
+ window.addEventListener("blur", val => {
+ TorProtocolService.flushSettings();
+ });
+
+ document
+ .querySelector(selectors.category.title)
+ .setAttribute("value", TorStrings.settings.categoryTitle);
+
+ let prefpane = document.getElementById("mainPrefPane");
+
+ // Heading
+ prefpane.querySelector(selectors.torPreferences.header).innerText =
+ TorStrings.settings.torPreferencesHeading;
+ prefpane.querySelector(selectors.torPreferences.description).textContent =
+ TorStrings.settings.torPreferencesDescription;
+ {
+ let learnMore = prefpane.querySelector(
+ selectors.torPreferences.learnMore
+ );
+ learnMore.setAttribute("value", TorStrings.settings.learnMore);
+ learnMore.setAttribute(
+ "href",
+ TorStrings.settings.learnMoreTorBrowserURL
+ );
+ }
+
+ // Bridge setup
+ prefpane.querySelector(selectors.bridges.header).innerText =
+ TorStrings.settings.bridgesHeading;
+ prefpane.querySelector(selectors.bridges.description).textContent =
+ TorStrings.settings.bridgesDescription;
+ {
+ let learnMore = prefpane.querySelector(selectors.bridges.learnMore);
+ learnMore.setAttribute("value", TorStrings.settings.learnMore);
+ learnMore.setAttribute("href", TorStrings.settings.learnMoreBridgesURL);
+ }
+
+ this._useBridgeCheckbox = prefpane.querySelector(
+ selectors.bridges.useBridgeCheckbox
+ );
+ this._useBridgeCheckbox.setAttribute(
+ "label",
+ TorStrings.settings.useBridge
+ );
+ this._useBridgeCheckbox.addEventListener("command", e => {
+ const checked = this._useBridgeCheckbox.checked;
+ gTorPane.onToggleBridge(checked).onUpdateBridgeSettings();
+ });
+ this._bridgeSelectionRadiogroup = prefpane.querySelector(
+ selectors.bridges.bridgeSelectionRadiogroup
+ );
+ this._bridgeSelectionRadiogroup.value = TorBridgeSource.BUILTIN;
+ this._bridgeSelectionRadiogroup.addEventListener("command", e => {
+ const value = this._bridgeSelectionRadiogroup.value;
+ gTorPane.onSelectBridgeOption(value).onUpdateBridgeSettings();
+ });
+
+ // Builtin bridges
+ this._builtinBridgeOption = prefpane.querySelector(
+ selectors.bridges.builtinBridgeOption
+ );
+ this._builtinBridgeOption.setAttribute(
+ "label",
+ TorStrings.settings.selectBridge
+ );
+ this._builtinBridgeOption.setAttribute("value", TorBridgeSource.BUILTIN);
+ this._builtinBridgeMenulist = prefpane.querySelector(
+ selectors.bridges.builtinBridgeList
+ );
+ this._builtinBridgeMenulist.addEventListener("command", e => {
+ gTorPane.onUpdateBridgeSettings();
+ });
+
+ // Request bridge
+ this._requestBridgeOption = prefpane.querySelector(
+ selectors.bridges.requestBridgeOption
+ );
+ this._requestBridgeOption.setAttribute(
+ "label",
+ TorStrings.settings.requestBridgeFromTorProject
+ );
+ this._requestBridgeOption.setAttribute("value", TorBridgeSource.BRIDGEDB);
+ this._requestBridgeButton = prefpane.querySelector(
+ selectors.bridges.requestBridgeButton
+ );
+ this._requestBridgeButton.setAttribute(
+ "label",
+ TorStrings.settings.requestNewBridge
+ );
+ this._requestBridgeButton.addEventListener("command", () =>
+ gTorPane.onRequestBridge()
+ );
+ this._requestBridgeTextarea = prefpane.querySelector(
+ selectors.bridges.requestBridgeTextarea
+ );
+
+ // Provide a bridge
+ this._provideBridgeOption = prefpane.querySelector(
+ selectors.bridges.provideBridgeOption
+ );
+ this._provideBridgeOption.setAttribute(
+ "label",
+ TorStrings.settings.provideBridge
+ );
+ this._provideBridgeOption.setAttribute(
+ "value",
+ TorBridgeSource.USERPROVIDED
+ );
+ prefpane.querySelector(
+ selectors.bridges.provideBridgeDescription
+ ).textContent = TorStrings.settings.provideBridgeDirections;
+ this._provideBridgeTextarea = prefpane.querySelector(
+ selectors.bridges.provideBridgeTextarea
+ );
+ this._provideBridgeTextarea.setAttribute(
+ "placeholder",
+ TorStrings.settings.provideBridgePlaceholder
+ );
+ this._provideBridgeTextarea.addEventListener("blur", () => {
+ gTorPane.onUpdateBridgeSettings();
+ });
+
+ // Advanced setup
+ prefpane.querySelector(selectors.advanced.header).innerText =
+ TorStrings.settings.advancedHeading;
+ prefpane.querySelector(selectors.advanced.description).textContent =
+ TorStrings.settings.advancedDescription;
+ {
+ let learnMore = prefpane.querySelector(selectors.advanced.learnMore);
+ learnMore.setAttribute("value", TorStrings.settings.learnMore);
+ learnMore.setAttribute(
+ "href",
+ TorStrings.settings.learnMoreNetworkSettingsURL
+ );
+ }
+
+ // Local Proxy
+ this._useProxyCheckbox = prefpane.querySelector(
+ selectors.advanced.useProxyCheckbox
+ );
+ this._useProxyCheckbox.setAttribute(
+ "label",
+ TorStrings.settings.useLocalProxy
+ );
+ this._useProxyCheckbox.addEventListener("command", e => {
+ const checked = this._useProxyCheckbox.checked;
+ gTorPane.onToggleProxy(checked).onUpdateProxySettings();
+ });
+ this._proxyTypeLabel = prefpane.querySelector(
+ selectors.advanced.proxyTypeLabel
+ );
+ this._proxyTypeLabel.setAttribute("value", TorStrings.settings.proxyType);
+
+ let mockProxies = [
+ {
+ value: TorProxyType.SOCKS4,
+ label: TorStrings.settings.proxyTypeSOCKS4,
+ },
+ {
+ value: TorProxyType.SOCKS5,
+ label: TorStrings.settings.proxyTypeSOCKS5,
+ },
+ { value: TorProxyType.HTTPS, label: TorStrings.settings.proxyTypeHTTP },
+ ];
+ this._proxyTypeMenulist = prefpane.querySelector(
+ selectors.advanced.proxyTypeList
+ );
+ this._proxyTypeMenulist.addEventListener("command", e => {
+ const value = this._proxyTypeMenulist.value;
+ gTorPane.onSelectProxyType(value).onUpdateProxySettings();
+ });
+ for (let currentProxy of mockProxies) {
+ let menuEntry = document.createXULElement("menuitem");
+ menuEntry.setAttribute("value", currentProxy.value);
+ menuEntry.setAttribute("label", currentProxy.label);
+ this._proxyTypeMenulist
+ .querySelector("menupopup")
+ .appendChild(menuEntry);
+ }
+
+ this._proxyAddressLabel = prefpane.querySelector(
+ selectors.advanced.proxyAddressLabel
+ );
+ this._proxyAddressLabel.setAttribute(
+ "value",
+ TorStrings.settings.proxyAddress
+ );
+ this._proxyAddressTextbox = prefpane.querySelector(
+ selectors.advanced.proxyAddressTextbox
+ );
+ this._proxyAddressTextbox.setAttribute(
+ "placeholder",
+ TorStrings.settings.proxyAddressPlaceholder
+ );
+ this._proxyAddressTextbox.addEventListener("blur", () => {
+ gTorPane.onUpdateProxySettings();
+ });
+ this._proxyPortLabel = prefpane.querySelector(
+ selectors.advanced.proxyPortLabel
+ );
+ this._proxyPortLabel.setAttribute("value", TorStrings.settings.proxyPort);
+ this._proxyPortTextbox = prefpane.querySelector(
+ selectors.advanced.proxyPortTextbox
+ );
+ this._proxyPortTextbox.addEventListener("blur", () => {
+ gTorPane.onUpdateProxySettings();
+ });
+ this._proxyUsernameLabel = prefpane.querySelector(
+ selectors.advanced.proxyUsernameLabel
+ );
+ this._proxyUsernameLabel.setAttribute(
+ "value",
+ TorStrings.settings.proxyUsername
+ );
+ this._proxyUsernameTextbox = prefpane.querySelector(
+ selectors.advanced.proxyUsernameTextbox
+ );
+ this._proxyUsernameTextbox.setAttribute(
+ "placeholder",
+ TorStrings.settings.proxyUsernamePasswordPlaceholder
+ );
+ this._proxyUsernameTextbox.addEventListener("blur", () => {
+ gTorPane.onUpdateProxySettings();
+ });
+ this._proxyPasswordLabel = prefpane.querySelector(
+ selectors.advanced.proxyPasswordLabel
+ );
+ this._proxyPasswordLabel.setAttribute(
+ "value",
+ TorStrings.settings.proxyPassword
+ );
+ this._proxyPasswordTextbox = prefpane.querySelector(
+ selectors.advanced.proxyPasswordTextbox
+ );
+ this._proxyPasswordTextbox.setAttribute(
+ "placeholder",
+ TorStrings.settings.proxyUsernamePasswordPlaceholder
+ );
+ this._proxyPasswordTextbox.addEventListener("blur", () => {
+ gTorPane.onUpdateProxySettings();
+ });
+
+ // Local firewall
+ this._useFirewallCheckbox = prefpane.querySelector(
+ selectors.advanced.useFirewallCheckbox
+ );
+ this._useFirewallCheckbox.setAttribute(
+ "label",
+ TorStrings.settings.useFirewall
+ );
+ this._useFirewallCheckbox.addEventListener("command", e => {
+ const checked = this._useFirewallCheckbox.checked;
+ gTorPane.onToggleFirewall(checked).onUpdateFirewallSettings();
+ });
+ this._allowedPortsLabel = prefpane.querySelector(
+ selectors.advanced.firewallAllowedPortsLabel
+ );
+ this._allowedPortsLabel.setAttribute(
+ "value",
+ TorStrings.settings.allowedPorts
+ );
+ this._allowedPortsTextbox = prefpane.querySelector(
+ selectors.advanced.firewallAllowedPortsTextbox
+ );
+ this._allowedPortsTextbox.setAttribute(
+ "placeholder",
+ TorStrings.settings.allowedPortsPlaceholder
+ );
+ this._allowedPortsTextbox.addEventListener("blur", () => {
+ gTorPane.onUpdateFirewallSettings();
+ });
+
+ // Tor logs
+ prefpane
+ .querySelector(selectors.advanced.torLogsLabel)
+ .setAttribute("value", TorStrings.settings.showTorDaemonLogs);
+ let torLogsButton = prefpane.querySelector(
+ selectors.advanced.torLogsButton
+ );
+ torLogsButton.setAttribute("label", TorStrings.settings.showLogs);
+ torLogsButton.addEventListener("command", () => {
+ gTorPane.onViewTorLogs();
+ });
+
+ // Disable all relevant elements by default
+ this._setElementsDisabled(
+ [
+ this._builtinBridgeOption,
+ this._builtinBridgeMenulist,
+ this._requestBridgeOption,
+ this._requestBridgeButton,
+ this._requestBridgeTextarea,
+ this._provideBridgeOption,
+ this._provideBridgeTextarea,
+ this._proxyTypeLabel,
+ this._proxyTypeMenulist,
+ this._proxyAddressLabel,
+ this._proxyAddressTextbox,
+ this._proxyPortLabel,
+ this._proxyPortTextbox,
+ this._proxyUsernameLabel,
+ this._proxyUsernameTextbox,
+ this._proxyPasswordLabel,
+ this._proxyPasswordTextbox,
+ this._allowedPortsLabel,
+ this._allowedPortsTextbox,
+ ],
+ true
+ );
+
+ // load bridge settings
+ let torBridgeSettings = new TorBridgeSettings();
+ torBridgeSettings.readSettings();
+
+ // populate the bridge list
+ for (let currentBridge of TorBridgeSettings.defaultBridgeTypes) {
+ let menuEntry = document.createXULElement("menuitem");
+ menuEntry.setAttribute("value", currentBridge);
+ menuEntry.setAttribute("label", currentBridge);
+ this._builtinBridgeMenulist
+ .querySelector("menupopup")
+ .appendChild(menuEntry);
+ }
+
+ this.onSelectBridgeOption(torBridgeSettings.bridgeSource);
+ this.onToggleBridge(
+ torBridgeSettings.bridgeSource != TorBridgeSource.NONE
+ );
+ switch (torBridgeSettings.bridgeSource) {
+ case TorBridgeSource.NONE:
+ break;
+ case TorBridgeSource.BUILTIN:
+ this._builtinBridgeMenulist.value =
+ torBridgeSettings.selectedDefaultBridgeType;
+ break;
+ case TorBridgeSource.BRIDGEDB:
+ this._requestBridgeTextarea.value = torBridgeSettings.bridgeStrings;
+ break;
+ case TorBridgeSource.USERPROVIDED:
+ this._provideBridgeTextarea.value = torBridgeSettings.bridgeStrings;
+ break;
+ }
+
+ this._bridgeSettings = torBridgeSettings;
+
+ // load proxy settings
+ let torProxySettings = new TorProxySettings();
+ torProxySettings.readSettings();
+
+ if (torProxySettings.type != TorProxyType.NONE) {
+ this.onToggleProxy(true);
+ this.onSelectProxyType(torProxySettings.type);
+ this._proxyAddressTextbox.value = torProxySettings.address;
+ this._proxyPortTextbox.value = torProxySettings.port;
+ this._proxyUsernameTextbox.value = torProxySettings.username;
+ this._proxyPasswordTextbox.value = torProxySettings.password;
+ }
+
+ this._proxySettings = torProxySettings;
+
+ // load firewall settings
+ let torFirewallSettings = new TorFirewallSettings();
+ torFirewallSettings.readSettings();
+
+ if (torFirewallSettings.hasPorts) {
+ this.onToggleFirewall(true);
+ this._allowedPortsTextbox.value =
+ torFirewallSettings.commaSeparatedListString;
+ }
+
+ this._firewallSettings = torFirewallSettings;
+ },
+
+ init() {
+ this._populateXUL();
+ },
+
+ // whether the page should be present in about:preferences
+ get enabled() {
+ return TorProtocolService.ownsTorDaemon;
+ },
+
+ //
+ // Callbacks
+ //
+
+ // callback when using bridges toggled
+ onToggleBridge(enabled) {
+ this._useBridgeCheckbox.checked = enabled;
+ let disabled = !enabled;
+
+ // first disable all the bridge related elements
+ this._setElementsDisabled(
+ [
+ this._builtinBridgeOption,
+ this._builtinBridgeMenulist,
+ this._requestBridgeOption,
+ this._requestBridgeButton,
+ this._requestBridgeTextarea,
+ this._provideBridgeOption,
+ this._provideBridgeTextarea,
+ ],
+ disabled
+ );
+
+ // and selectively re-enable based on the radiogroup's current value
+ if (enabled) {
+ this.onSelectBridgeOption(this._bridgeSelectionRadiogroup.value);
+ } else {
+ this.onSelectBridgeOption(TorBridgeSource.NONE);
+ }
+ return this;
+ },
+
+ // callback when a bridge option is selected
+ onSelectBridgeOption(source) {
+ // disable all of the bridge elements under radio buttons
+ this._setElementsDisabled(
+ [
+ this._builtinBridgeMenulist,
+ this._requestBridgeButton,
+ this._requestBridgeTextarea,
+ this._provideBridgeTextarea,
+ ],
+ true
+ );
+
+ if (source != TorBridgeSource.NONE) {
+ this._bridgeSelectionRadiogroup.value = source;
+ }
+
+ switch (source) {
+ case TorBridgeSource.BUILTIN: {
+ this._setElementsDisabled([this._builtinBridgeMenulist], false);
+ break;
+ }
+ case TorBridgeSource.BRIDGEDB: {
+ this._setElementsDisabled(
+ [this._requestBridgeButton, this._requestBridgeTextarea],
+ false
+ );
+ break;
+ }
+ case TorBridgeSource.USERPROVIDED: {
+ this._setElementsDisabled([this._provideBridgeTextarea], false);
+ break;
+ }
+ }
+ return this;
+ },
+
+ // called when the request bridge button is activated
+ onRequestBridge() {
+ let requestBridgeDialog = new RequestBridgeDialog();
+ requestBridgeDialog.openDialog(
+ gSubDialog,
+ this._proxySettings.proxyURI,
+ aBridges => {
+ if (aBridges.length > 0) {
+ let bridgeSettings = makeTorBridgeSettingsBridgeDB(aBridges);
+ bridgeSettings.writeSettings();
+ this._bridgeSettings = bridgeSettings;
+
+ this._requestBridgeTextarea.value = bridgeSettings.bridgeStrings;
+ }
+ }
+ );
+ return this;
+ },
+
+ // pushes bridge settings from UI to tor
+ onUpdateBridgeSettings() {
+ let bridgeSettings = null;
+
+ let source = this._useBridgeCheckbox.checked
+ ? this._bridgeSelectionRadiogroup.value
+ : TorBridgeSource.NONE;
+ switch (source) {
+ case TorBridgeSource.NONE: {
+ bridgeSettings = makeTorBridgeSettingsNone();
+ break;
+ }
+ case TorBridgeSource.BUILTIN: {
+ // if there is a built-in bridge already selected, use that
+ let bridgeType = this._builtinBridgeMenulist.value;
+ if (bridgeType) {
+ bridgeSettings = makeTorBridgeSettingsBuiltin(bridgeType);
+ } else {
+ bridgeSettings = makeTorBridgeSettingsNone();
+ }
+ break;
+ }
+ case TorBridgeSource.BRIDGEDB: {
+ // if there are bridgedb bridges saved in the text area, use them
+ let bridgeStrings = this._requestBridgeTextarea.value;
+ if (bridgeStrings) {
+ let bridgeStringList = parseBridgeStrings(bridgeStrings);
+ bridgeSettings = makeTorBridgeSettingsBridgeDB(bridgeStringList);
+ } else {
+ bridgeSettings = makeTorBridgeSettingsNone();
+ }
+ break;
+ }
+ case TorBridgeSource.USERPROVIDED: {
+ // if bridges already exist in the text area, use them
+ let bridgeStrings = this._provideBridgeTextarea.value;
+ if (bridgeStrings) {
+ let bridgeStringList = parseBridgeStrings(bridgeStrings);
+ bridgeSettings = makeTorBridgeSettingsUserProvided(
+ bridgeStringList
+ );
+ } else {
+ bridgeSettings = makeTorBridgeSettingsNone();
+ }
+ break;
+ }
+ }
+ bridgeSettings.writeSettings();
+ this._bridgeSettings = bridgeSettings;
+ return this;
+ },
+
+ // callback when proxy is toggled
+ onToggleProxy(enabled) {
+ this._useProxyCheckbox.checked = enabled;
+ let disabled = !enabled;
+
+ this._setElementsDisabled(
+ [
+ this._proxyTypeLabel,
+ this._proxyTypeMenulist,
+ this._proxyAddressLabel,
+ this._proxyAddressTextbox,
+ this._proxyPortLabel,
+ this._proxyPortTextbox,
+ this._proxyUsernameLabel,
+ this._proxyUsernameTextbox,
+ this._proxyPasswordLabel,
+ this._proxyPasswordTextbox,
+ ],
+ disabled
+ );
+ this.onSelectProxyType(this._proxyTypeMenulist.value);
+ return this;
+ },
+
+ // callback when proxy type is changed
+ onSelectProxyType(value) {
+ if (value == "") {
+ value = TorProxyType.NONE;
+ }
+ this._proxyTypeMenulist.value = value;
+ switch (value) {
+ case TorProxyType.NONE: {
+ this._setElementsDisabled(
+ [
+ this._proxyAddressLabel,
+ this._proxyAddressTextbox,
+ this._proxyPortLabel,
+ this._proxyPortTextbox,
+ this._proxyUsernameLabel,
+ this._proxyUsernameTextbox,
+ this._proxyPasswordLabel,
+ this._proxyPasswordTextbox,
+ ],
+ true
+ ); // DISABLE
+
+ this._proxyAddressTextbox.value = "";
+ this._proxyPortTextbox.value = "";
+ this._proxyUsernameTextbox.value = "";
+ this._proxyPasswordTextbox.value = "";
+ break;
+ }
+ case TorProxyType.SOCKS4: {
+ this._setElementsDisabled(
+ [
+ this._proxyAddressLabel,
+ this._proxyAddressTextbox,
+ this._proxyPortLabel,
+ this._proxyPortTextbox,
+ ],
+ false
+ ); // ENABLE
+ this._setElementsDisabled(
+ [
+ this._proxyUsernameLabel,
+ this._proxyUsernameTextbox,
+ this._proxyPasswordLabel,
+ this._proxyPasswordTextbox,
+ ],
+ true
+ ); // DISABLE
+
+ this._proxyUsernameTextbox.value = "";
+ this._proxyPasswordTextbox.value = "";
+ break;
+ }
+ case TorProxyType.SOCKS5:
+ case TorProxyType.HTTPS: {
+ this._setElementsDisabled(
+ [
+ this._proxyAddressLabel,
+ this._proxyAddressTextbox,
+ this._proxyPortLabel,
+ this._proxyPortTextbox,
+ this._proxyUsernameLabel,
+ this._proxyUsernameTextbox,
+ this._proxyPasswordLabel,
+ this._proxyPasswordTextbox,
+ ],
+ false
+ ); // ENABLE
+ break;
+ }
+ }
+ return this;
+ },
+
+ // pushes proxy settings from UI to tor
+ onUpdateProxySettings() {
+ const proxyType = this._useProxyCheckbox.checked
+ ? this._proxyTypeMenulist.value
+ : TorProxyType.NONE;
+ const addressString = this._proxyAddressTextbox.value;
+ const portString = this._proxyPortTextbox.value;
+ const usernameString = this._proxyUsernameTextbox.value;
+ const passwordString = this._proxyPasswordTextbox.value;
+
+ let proxySettings = null;
+
+ switch (proxyType) {
+ case TorProxyType.NONE:
+ proxySettings = makeTorProxySettingsNone();
+ break;
+ case TorProxyType.SOCKS4:
+ proxySettings = makeTorProxySettingsSocks4(
+ addressString,
+ parsePort(portString)
+ );
+ break;
+ case TorProxyType.SOCKS5:
+ proxySettings = makeTorProxySettingsSocks5(
+ addressString,
+ parsePort(portString),
+ usernameString,
+ passwordString
+ );
+ break;
+ case TorProxyType.HTTPS:
+ proxySettings = makeTorProxySettingsHTTPS(
+ addressString,
+ parsePort(portString),
+ usernameString,
+ passwordString
+ );
+ break;
+ }
+
+ proxySettings.writeSettings();
+ this._proxySettings = proxySettings;
+ return this;
+ },
+
+ // callback when firewall proxy is toggled
+ onToggleFirewall(enabled) {
+ this._useFirewallCheckbox.checked = enabled;
+ let disabled = !enabled;
+
+ this._setElementsDisabled(
+ [this._allowedPortsLabel, this._allowedPortsTextbox],
+ disabled
+ );
+
+ return this;
+ },
+
+ // pushes firewall settings from UI to tor
+ onUpdateFirewallSettings() {
+ let portListString = this._useFirewallCheckbox.checked
+ ? this._allowedPortsTextbox.value
+ : "";
+ let firewallSettings = null;
+
+ if (portListString) {
+ firewallSettings = makeTorFirewallSettingsCustom(
+ parsePortList(portListString)
+ );
+ } else {
+ firewallSettings = makeTorFirewallSettingsNone();
+ }
+
+ firewallSettings.writeSettings();
+ this._firewallSettings = firewallSettings;
+ return this;
+ },
+
+ onViewTorLogs() {
+ let torLogDialog = new TorLogDialog();
+ torLogDialog.openDialog(gSubDialog);
+ },
+ };
+ return retval;
+})(); /* gTorPane */
diff --git a/browser/components/torpreferences/content/torPane.xhtml b/browser/components/torpreferences/content/torPane.xhtml
new file mode 100644
index 000000000000..3c966b2b3726
--- /dev/null
+++ b/browser/components/torpreferences/content/torPane.xhtml
@@ -0,0 +1,123 @@
+<!-- Tor panel -->
+
+<script type="application/javascript"
+ src="chrome://browser/content/torpreferences/torPane.js"/>
+<html:template id="template-paneTor">
+<hbox id="torPreferencesCategory"
+ class="subcategory"
+ data-category="paneTor"
+ hidden="true">
+ <html:h1 id="torPreferences-header"/>
+</hbox>
+
+<groupbox data-category="paneTor"
+ hidden="true">
+ <description flex="1">
+ <html:span id="torPreferences-description" class="tail-with-learn-more"/>
+ <label id="torPreferences-learnMore" class="learnMore text-link" is="text-link"/>
+ </description>
+</groupbox>
+
+<!-- Bridges -->
+<groupbox id="torPreferences-bridges-group"
+ data-category="paneTor"
+ hidden="true">
+ <html:h2 id="torPreferences-bridges-header"/>
+ <description flex="1">
+ <html:span id="torPreferences-bridges-description" class="tail-with-learn-more"/>
+ <label id="torPreferences-bridges-learnMore" class="learnMore text-link" is="text-link"/>
+ </description>
+ <checkbox id="torPreferences-bridges-toggle"/>
+ <radiogroup id="torPreferences-bridges-bridgeSelection">
+ <hbox class="indent">
+ <radio id="torPreferences-bridges-radioBuiltin"/>
+ <spacer flex="1"/>
+ <menulist id="torPreferences-bridges-builtinList" class="torMarginFix">
+ <menupopup/>
+ </menulist>
+ </hbox>
+ <vbox class="indent">
+ <hbox>
+ <radio id="torPreferences-bridges-radioRequestBridge"/>
+ <space flex="1"/>
+ <button id="torPreferences-bridges-buttonRequestBridge" class="torMarginFix"/>
+ </hbox>
+ <html:textarea
+ id="torPreferences-bridges-textareaRequestBridge"
+ class="indent torMarginFix"
+ multiline="true"
+ rows="3"
+ readonly="true"/>
+ </vbox>
+ <hbox class="indent" flex="1">
+ <vbox flex="1">
+ <radio id="torPreferences-bridges-radioProvideBridge"/>
+ <description id="torPreferences-bridges-descriptionProvideBridge" class="indent"/>
+ <html:textarea
+ id="torPreferences-bridges-textareaProvideBridge"
+ class="indent torMarginFix"
+ multiline="true"
+ rows="3"/>
+ </vbox>
+ </hbox>
+ </radiogroup>
+</groupbox>
+
+<!-- Advanced -->
+<groupbox id="torPreferences-advanced-group"
+ data-category="paneTor"
+ hidden="true">
+ <html:h2 id="torPreferences-advanced-header"/>
+ <description flex="1">
+ <html:span id="torPreferences-advanced-description" class="tail-with-learn-more"/>
+ <label id="torPreferences-advanced-learnMore" class="learnMore text-link" is="text-link" style="display:none"/>
+ </description>
+ <box id="torPreferences-advanced-grid">
+ <!-- Local Proxy -->
+ <hbox class="torPreferences-advanced-checkbox-container">
+ <checkbox id="torPreferences-advanced-toggleProxy"/>
+ </hbox>
+ <hbox class="indent" align="center">
+ <label id="torPreferences-localProxy-type"/>
+ </hbox>
+ <hbox align="center">
+ <spacer flex="1"/>
+ <menulist id="torPreferences-localProxy-builtinList" class="torMarginFix">
+ <menupopup/>
+ </menulist>
+ </hbox>
+ <hbox class="indent" align="center">
+ <label id="torPreferences-localProxy-address"/>
+ </hbox>
+ <hbox align="center">
+ <html:input id="torPreferences-localProxy-textboxAddress" type="text" class="torMarginFix"/>
+ <label id="torPreferences-localProxy-port"/>
+ <!-- proxy-port-input class style pulled from preferences.css and used in the vanilla proxy setup menu -->
+ <html:input id="torPreferences-localProxy-textboxPort" class="proxy-port-input torMarginFix" hidespinbuttons="true" type="number" min="0" max="65535" maxlength="5"/>
+ </hbox>
+ <hbox class="indent" align="center">
+ <label id="torPreferences-localProxy-username"/>
+ </hbox>
+ <hbox align="center">
+ <html:input id="torPreferences-localProxy-textboxUsername" type="text" class="torMarginFix"/>
+ <label id="torPreferences-localProxy-password"/>
+ <html:input id="torPreferences-localProxy-textboxPassword" class="torMarginFix" type="password"/>
+ </hbox>
+ <!-- Firewall -->
+ <hbox class="torPreferences-advanced-checkbox-container">
+ <checkbox id="torPreferences-advanced-toggleFirewall"/>
+ </hbox>
+ <hbox class="indent" align="center">
+ <label id="torPreferences-advanced-allowedPorts"/>
+ </hbox>
+ <hbox align="center">
+ <html:input id="torPreferences-advanced-textboxAllowedPorts" type="text" class="torMarginFix" value="80,443"/>
+ </hbox>
+ </box>
+ <hbox id="torPreferences-torDaemon-hbox" align="center">
+ <label id="torPreferences-torLogs"/>
+ <spacer flex="1"/>
+ <button id="torPreferences-buttonTorLogs" class="torMarginFix"/>
+ </hbox>
+</groupbox>
+</html:template>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torPreferences.css b/browser/components/torpreferences/content/torPreferences.css
new file mode 100644
index 000000000000..4dac2c457823
--- /dev/null
+++ b/browser/components/torpreferences/content/torPreferences.css
@@ -0,0 +1,77 @@
+#category-tor > .category-icon {
+ list-style-image: url("chrome://browser/content/torpreferences/torPreferencesIcon.svg");
+}
+
+#torPreferences-advanced-grid {
+ display: grid;
+ grid-template-columns: auto 1fr;
+}
+
+.torPreferences-advanced-checkbox-container {
+ grid-column: 1 / 3;
+}
+
+#torPreferences-localProxy-textboxAddress,
+#torPreferences-localProxy-textboxUsername,
+#torPreferences-localProxy-textboxPassword,
+#torPreferences-advanced-textboxAllowedPorts {
+ -moz-box-flex: 1;
+}
+
+hbox#torPreferences-torDaemon-hbox {
+ margin-top: 20px;
+}
+
+description#torPreferences-requestBridge-description {
+ /*margin-bottom: 1em;*/
+ min-height: 2em;
+}
+
+image#torPreferences-requestBridge-captchaImage {
+ margin: 1em;
+ min-height: 125px;
+}
+
+button#torPreferences-requestBridge-refreshCaptchaButton {
+ min-width: initial;
+}
+
+dialog#torPreferences-requestBridge-dialog > hbox {
+ margin-bottom: 1em;
+}
+
+/*
+ Various elements that really should be lining up don't because they have inconsistent margins
+*/
+.torMarginFix {
+ margin-left : 4px;
+ margin-right : 4px;
+}
+
+/*
+ This hbox is hidden by css here by default so that the
+ xul dialog allocates enough screen space for the error message
+ element, otherwise it gets cut off since dialog's overflow is hidden
+*/
+hbox#torPreferences-requestBridge-incorrectCaptchaHbox {
+ visibility: hidden;
+}
+
+image#torPreferences-requestBridge-errorIcon {
+ list-style-image: url("chrome://browser/skin/warning.svg");
+}
+
+groupbox#torPreferences-bridges-group textarea {
+ white-space: pre;
+ overflow: auto;
+}
+
+textarea#torPreferences-torDialog-textarea {
+ -moz-box-flex: 1;
+ font-family: monospace;
+ font-size: 0.8em;
+ white-space: pre;
+ overflow: auto;
+ /* 10 lines */
+ min-height: 20em;
+}
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torPreferencesIcon.svg b/browser/components/torpreferences/content/torPreferencesIcon.svg
new file mode 100644
index 000000000000..d7895f1107c5
--- /dev/null
+++ b/browser/components/torpreferences/content/torPreferencesIcon.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+ <g fill="context-fill" fill-opacity="context-fill-opacity" fill-rule="nonzero">
+ <path d="M12.0246161,21.8174863 L12.0246161,20.3628098 C16.6324777,20.3495038 20.3634751,16.6108555 20.3634751,11.9996673 C20.3634751,7.38881189 16.6324777,3.65016355 12.0246161,3.63685757 L12.0246161,2.18218107 C17.4358264,2.1958197 21.8178189,6.58546322 21.8178189,11.9996673 C21.8178189,17.4142042 17.4358264,21.8041803 12.0246161,21.8174863 L12.0246161,21.8174863 Z M12.0246161,16.7259522 C14.623607,16.7123136 16.7272828,14.6023175 16.7272828,11.9996673 C16.7272828,9.39734991 14.623607,7.28735377 12.0246161,7.27371516 L12.0246161,5.81937131 C15.4272884,5.8326773 18.1819593,8.59400123 18.1819593,11.9996673 C18.1819593,15.4056661 15.4272884,18.1669901 12.0246161,18.1802961 L12.0246161,16.7259522 Z M12.0246161,9.45556355 C13.4187503,9.46886953 14.5454344,10.6022066 14.5454344,11.9996673 C14.5454344,13.3974608 13.4187503,14.5307978 12.0246161,14.5441038 L12.0246161,9.45556355 Z M0,11.9996673 C0,18.6273771 5.37229031,24 12,24 C18.6273771,24 24,18.6273771 24,11.9996673 C24,5.37229031
18.6273771,0 12,0 C5.37229031,0 0,5.37229031 0,11.9996673 Z"/>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torProxySettings.jsm b/browser/components/torpreferences/content/torProxySettings.jsm
new file mode 100644
index 000000000000..98bb5e8d5cbf
--- /dev/null
+++ b/browser/components/torpreferences/content/torProxySettings.jsm
@@ -0,0 +1,245 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "TorProxyType",
+ "TorProxySettings",
+ "makeTorProxySettingsNone",
+ "makeTorProxySettingsSocks4",
+ "makeTorProxySettingsSocks5",
+ "makeTorProxySettingsHTTPS",
+];
+
+const { TorProtocolService } = ChromeUtils.import(
+ "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+const { parseAddrPort, parseUsernamePassword } = ChromeUtils.import(
+ "chrome://browser/content/torpreferences/parseFunctions.jsm"
+);
+
+const TorProxyType = {
+ NONE: "NONE",
+ SOCKS4: "SOCKS4",
+ SOCKS5: "SOCKS5",
+ HTTPS: "HTTPS",
+};
+
+class TorProxySettings {
+ constructor() {
+ this._proxyType = TorProxyType.NONE;
+ this._proxyAddress = undefined;
+ this._proxyPort = undefined;
+ this._proxyUsername = undefined;
+ this._proxyPassword = undefined;
+ }
+
+ get type() {
+ return this._proxyType;
+ }
+ get address() {
+ return this._proxyAddress;
+ }
+ get port() {
+ return this._proxyPort;
+ }
+ get username() {
+ return this._proxyUsername;
+ }
+ get password() {
+ return this._proxyPassword;
+ }
+ get proxyURI() {
+ switch (this._proxyType) {
+ case TorProxyType.SOCKS4:
+ return `socks4a://${this._proxyAddress}:${this._proxyPort}`;
+ case TorProxyType.SOCKS5:
+ if (this._proxyUsername) {
+ return `socks5://${this._proxyUsername}:${this._proxyPassword}@${
+ this._proxyAddress
+ }:${this._proxyPort}`;
+ }
+ return `socks5://${this._proxyAddress}:${this._proxyPort}`;
+ case TorProxyType.HTTPS:
+ if (this._proxyUsername) {
+ return `http://${this._proxyUsername}:${this._proxyPassword}@${
+ this._proxyAddress
+ }:${this._proxyPort}`;
+ }
+ return `http://${this._proxyAddress}:${this._proxyPort}`;
+ }
+ return undefined;
+ }
+
+ // attempts to read proxy settings from Tor daemon
+ readSettings() {
+ // SOCKS4
+ {
+ let addressPort = TorProtocolService.readStringSetting(
+ TorStrings.configKeys.socks4Proxy
+ );
+ if (addressPort) {
+ // address+port
+ let [proxyAddress, proxyPort] = parseAddrPort(addressPort);
+
+ this._proxyType = TorProxyType.SOCKS4;
+ this._proxyAddress = proxyAddress;
+ this._proxyPort = proxyPort;
+ this._proxyUsername = "";
+ this._proxyPassword = "";
+
+ return;
+ }
+ }
+
+ // SOCKS5
+ {
+ let addressPort = TorProtocolService.readStringSetting(
+ TorStrings.configKeys.socks5Proxy
+ );
+
+ if (addressPort) {
+ // address+port
+ let [proxyAddress, proxyPort] = parseAddrPort(addressPort);
+ // username
+ let proxyUsername = TorProtocolService.readStringSetting(
+ TorStrings.configKeys.socks5ProxyUsername
+ );
+ // password
+ let proxyPassword = TorProtocolService.readStringSetting(
+ TorStrings.configKeys.socks5ProxyPassword
+ );
+
+ this._proxyType = TorProxyType.SOCKS5;
+ this._proxyAddress = proxyAddress;
+ this._proxyPort = proxyPort;
+ this._proxyUsername = proxyUsername;
+ this._proxyPassword = proxyPassword;
+
+ return;
+ }
+ }
+
+ // HTTP
+ {
+ let addressPort = TorProtocolService.readStringSetting(
+ TorStrings.configKeys.httpsProxy
+ );
+
+ if (addressPort) {
+ // address+port
+ let [proxyAddress, proxyPort] = parseAddrPort(addressPort);
+
+ // username:password
+ let proxyAuthenticator = TorProtocolService.readStringSetting(
+ TorStrings.configKeys.httpsProxyAuthenticator
+ );
+
+ let [proxyUsername, proxyPassword] = ["", ""];
+ if (proxyAuthenticator) {
+ [proxyUsername, proxyPassword] = parseUsernamePassword(
+ proxyAuthenticator
+ );
+ }
+
+ this._proxyType = TorProxyType.HTTPS;
+ this._proxyAddress = proxyAddress;
+ this._proxyPort = proxyPort;
+ this._proxyUsername = proxyUsername;
+ this._proxyPassword = proxyPassword;
+ }
+ }
+ // no proxy settings
+ } /* TorProxySettings::ReadFromTor() */
+
+ // attempts to write proxy settings to Tor daemon
+ // throws on error
+ writeSettings() {
+ let settingsObject = new Map();
+
+ // init proxy related settings to null so Tor daemon resets them
+ settingsObject.set(TorStrings.configKeys.socks4Proxy, null);
+ settingsObject.set(TorStrings.configKeys.socks5Proxy, null);
+ settingsObject.set(TorStrings.configKeys.socks5ProxyUsername, null);
+ settingsObject.set(TorStrings.configKeys.socks5ProxyPassword, null);
+ settingsObject.set(TorStrings.configKeys.httpsProxy, null);
+ settingsObject.set(TorStrings.configKeys.httpsProxyAuthenticator, null);
+
+ switch (this._proxyType) {
+ case TorProxyType.SOCKS4:
+ settingsObject.set(
+ TorStrings.configKeys.socks4Proxy,
+ `${this._proxyAddress}:${this._proxyPort}`
+ );
+ break;
+ case TorProxyType.SOCKS5:
+ settingsObject.set(
+ TorStrings.configKeys.socks5Proxy,
+ `${this._proxyAddress}:${this._proxyPort}`
+ );
+ settingsObject.set(
+ TorStrings.configKeys.socks5ProxyUsername,
+ this._proxyUsername
+ );
+ settingsObject.set(
+ TorStrings.configKeys.socks5ProxyPassword,
+ this._proxyPassword
+ );
+ break;
+ case TorProxyType.HTTPS:
+ settingsObject.set(
+ TorStrings.configKeys.httpsProxy,
+ `${this._proxyAddress}:${this._proxyPort}`
+ );
+ settingsObject.set(
+ TorStrings.configKeys.httpsProxyAuthenticator,
+ `${this._proxyUsername}:${this._proxyPassword}`
+ );
+ break;
+ }
+
+ TorProtocolService.writeSettings(settingsObject);
+ } /* TorProxySettings::WriteToTor() */
+}
+
+// factory methods for our various supported proxies
+function makeTorProxySettingsNone() {
+ return new TorProxySettings();
+}
+
+function makeTorProxySettingsSocks4(aProxyAddress, aProxyPort) {
+ let retval = new TorProxySettings();
+ retval._proxyType = TorProxyType.SOCKS4;
+ retval._proxyAddress = aProxyAddress;
+ retval._proxyPort = aProxyPort;
+ return retval;
+}
+
+function makeTorProxySettingsSocks5(
+ aProxyAddress,
+ aProxyPort,
+ aProxyUsername,
+ aProxyPassword
+) {
+ let retval = new TorProxySettings();
+ retval._proxyType = TorProxyType.SOCKS5;
+ retval._proxyAddress = aProxyAddress;
+ retval._proxyPort = aProxyPort;
+ retval._proxyUsername = aProxyUsername;
+ retval._proxyPassword = aProxyPassword;
+ return retval;
+}
+
+function makeTorProxySettingsHTTPS(
+ aProxyAddress,
+ aProxyPort,
+ aProxyUsername,
+ aProxyPassword
+) {
+ let retval = new TorProxySettings();
+ retval._proxyType = TorProxyType.HTTPS;
+ retval._proxyAddress = aProxyAddress;
+ retval._proxyPort = aProxyPort;
+ retval._proxyUsername = aProxyUsername;
+ retval._proxyPassword = aProxyPassword;
+ return retval;
+}
diff --git a/browser/components/torpreferences/jar.mn b/browser/components/torpreferences/jar.mn
new file mode 100644
index 000000000000..857bc9ee3eac
--- /dev/null
+++ b/browser/components/torpreferences/jar.mn
@@ -0,0 +1,14 @@
+browser.jar:
+ content/browser/torpreferences/parseFunctions.jsm (content/parseFunctions.jsm)
+ content/browser/torpreferences/requestBridgeDialog.xhtml (content/requestBridgeDialog.xhtml)
+ content/browser/torpreferences/requestBridgeDialog.jsm (content/requestBridgeDialog.jsm)
+ content/browser/torpreferences/torBridgeSettings.jsm (content/torBridgeSettings.jsm)
+ content/browser/torpreferences/torCategory.inc.xhtml (content/torCategory.inc.xhtml)
+ content/browser/torpreferences/torFirewallSettings.jsm (content/torFirewallSettings.jsm)
+ content/browser/torpreferences/torLogDialog.jsm (content/torLogDialog.jsm)
+ content/browser/torpreferences/torLogDialog.xhtml (content/torLogDialog.xhtml)
+ content/browser/torpreferences/torPane.js (content/torPane.js)
+ content/browser/torpreferences/torPane.xhtml (content/torPane.xhtml)
+ content/browser/torpreferences/torPreferences.css (content/torPreferences.css)
+ content/browser/torpreferences/torPreferencesIcon.svg (content/torPreferencesIcon.svg)
+ content/browser/torpreferences/torProxySettings.jsm (content/torProxySettings.jsm)
diff --git a/browser/components/torpreferences/moz.build b/browser/components/torpreferences/moz.build
new file mode 100644
index 000000000000..2661ad7cb9f3
--- /dev/null
+++ b/browser/components/torpreferences/moz.build
@@ -0,0 +1 @@
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/browser/modules/BridgeDB.jsm b/browser/modules/BridgeDB.jsm
new file mode 100644
index 000000000000..9f76b4f05a69
--- /dev/null
+++ b/browser/modules/BridgeDB.jsm
@@ -0,0 +1,110 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["BridgeDB"];
+
+const { TorLauncherBridgeDB } = ChromeUtils.import(
+ "resource://torlauncher/modules/tl-bridgedb.jsm"
+);
+const { TorProtocolService } = ChromeUtils.import(
+ "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+var BridgeDB = {
+ _moatRequestor: null,
+ _currentCaptchaInfo: null,
+ _bridges: null,
+
+ get currentCaptchaImage() {
+ if (this._currentCaptchaInfo) {
+ return this._currentCaptchaInfo.captchaImage;
+ }
+ return null;
+ },
+
+ get currentBridges() {
+ return this._bridges;
+ },
+
+ async submitCaptchaGuess(aCaptchaSolution) {
+ if (this._moatRequestor && this._currentCaptchaInfo) {
+ return this._moatRequestor
+ .finishFetch(
+ this._currentCaptchaInfo.transport,
+ this._currentCaptchaInfo.challenge,
+ aCaptchaSolution
+ )
+ .then(aBridgeInfo => {
+ this._moatRequestor.close();
+ this._moatRequestor = null;
+ this._currentCaptchaInfo = null;
+ this._bridges = aBridgeInfo.bridges;
+ // array of bridge strings
+ return this._bridges;
+ });
+ }
+
+ return new Promise((aResponse, aReject) => {
+ aReject(new Error("Invalid _moatRequestor or _currentCaptchaInfo"));
+ });
+ },
+
+ async requestNewCaptchaImage(aProxyURI) {
+ // close and clear out existing state on captcha request
+ this.close();
+
+ let transportPlugins = await TorProtocolService.readStringArraySetting(
+ TorStrings.configKeys.clientTransportPlugin
+ );
+
+ let meekClientPath;
+ let meekTransport; // We support both "meek" and "meek_lite".
+ let meekClientArgs;
+ // TODO: shouldn't this early out once meek settings are found?
+ for (const line of transportPlugins) {
+ // Parse each ClientTransportPlugin line and look for the meek or
+ // meek_lite transport. This code works a lot like the Tor daemon's
+ // parse_transport_line() function.
+ let tokens = line.split(" ");
+ if (tokens.length > 2 && tokens[1] == "exec") {
+ let transportArray = tokens[0].split(",").map(aStr => aStr.trim());
+ let transport = transportArray.find(
+ aTransport => aTransport === "meek"
+ );
+ if (!transport) {
+ transport = transportArray.find(
+ aTransport => aTransport === "meek_lite"
+ );
+ }
+ if (transport) {
+ meekTransport = transport;
+ meekClientPath = tokens[2];
+ meekClientArgs = tokens.slice(3);
+ }
+ }
+ }
+
+ this._moatRequestor = TorLauncherBridgeDB.createMoatRequestor();
+
+ return this._moatRequestor
+ .init(aProxyURI, meekTransport, meekClientPath, meekClientArgs)
+ .then(() => {
+ // TODO: get this from TorLauncherUtil
+ let bridgeType = "obfs4";
+ return this._moatRequestor.fetchBridges([bridgeType]);
+ })
+ .then(aCaptchaInfo => {
+ // cache off the current captcha info as the challenge is needed for response
+ this._currentCaptchaInfo = aCaptchaInfo;
+ return aCaptchaInfo.captchaImage;
+ });
+ },
+
+ close() {
+ if (this._moatRequestor) {
+ this._moatRequestor.close();
+ this._moatRequestor = null;
+ }
+ this._currentCaptchaInfo = null;
+ },
+};
diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm
new file mode 100644
index 000000000000..409d6be60b83
--- /dev/null
+++ b/browser/modules/TorProtocolService.jsm
@@ -0,0 +1,323 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorProtocolService"];
+
+const { TorLauncherUtil } = ChromeUtils.import(
+ "resource://torlauncher/modules/tl-util.jsm"
+);
+
+var TorProtocolService = {
+ _tlps: Cc["@torproject.org/torlauncher-protocol-service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject,
+
+ // maintain a map of tor settings set by Tor Browser so that we don't
+ // repeatedly set the same key/values over and over
+ // this map contains string keys to primitive or array values
+ _settingsCache: new Map(),
+
+ _typeof(aValue) {
+ switch (typeof aValue) {
+ case "boolean":
+ return "boolean";
+ case "string":
+ return "string";
+ case "object":
+ if (aValue == null) {
+ return "null";
+ } else if (Array.isArray(aValue)) {
+ return "array";
+ }
+ return "object";
+ }
+ return "unknown";
+ },
+
+ _assertValidSettingKey(aSetting) {
+ // ensure the 'key' is a string
+ if (typeof aSetting != "string") {
+ throw new Error(
+ `Expected setting of type string but received ${typeof aSetting}`
+ );
+ }
+ },
+
+ _assertValidSetting(aSetting, aValue) {
+ this._assertValidSettingKey(aSetting);
+
+ const valueType = this._typeof(aValue);
+ switch (valueType) {
+ case "boolean":
+ case "string":
+ case "null":
+ return;
+ case "array":
+ for (const element of aValue) {
+ if (typeof element != "string") {
+ throw new Error(
+ `Setting '${aSetting}' array contains value of invalid type '${typeof element}'`
+ );
+ }
+ }
+ return;
+ default:
+ throw new Error(
+ `Invalid object type received for setting '${aSetting}'`
+ );
+ }
+ },
+
+ // takes a Map containing tor settings
+ // throws on error
+ async writeSettings(aSettingsObj) {
+ // only write settings that have changed
+ let newSettings = new Map();
+ for (const [setting, value] of aSettingsObj) {
+ let saveSetting = false;
+
+ // make sure we have valid data here
+ this._assertValidSetting(setting, value);
+
+ if (!this._settingsCache.has(setting)) {
+ // no cached setting, so write
+ saveSetting = true;
+ } else {
+ const cachedValue = this._settingsCache.get(setting);
+ if (value != cachedValue) {
+ // compare arrays member-wise
+ if (Array.isArray(value) && Array.isArray(cachedValue)) {
+ if (value.length != cachedValue.length) {
+ saveSetting = true;
+ } else {
+ const arrayLength = value.length;
+ for (let i = 0; i < arrayLength; ++i) {
+ if (value[i] != cachedValue[i]) {
+ saveSetting = true;
+ break;
+ }
+ }
+ }
+ } else {
+ // some other different values
+ saveSetting = true;
+ }
+ }
+ }
+
+ if (saveSetting) {
+ newSettings.set(setting, value);
+ }
+ }
+
+ // only write if new setting to save
+ if (newSettings.size > 0) {
+ // convert settingsObject map to js object for torlauncher-protocol-service
+ let settingsObject = {};
+ for (const [setting, value] of newSettings) {
+ settingsObject[setting] = value;
+ }
+
+ let errorObject = {};
+ if (! await this._TorLauncherProtocolService.TorSetConfWithReply(settingsObject, errorObject)) {
+ throw new Error(errorObject.details);
+ }
+
+ // save settings to cache after successfully writing to Tor
+ for (const [setting, value] of newSettings) {
+ this._settingsCache.set(setting, value);
+ }
+ }
+ },
+
+ async _readSetting(aSetting) {
+ this._assertValidSettingKey(aSetting);
+ let reply = await this._TorLauncherProtocolService.TorGetConf(aSetting);
+ if (this._TorLauncherProtocolService.TorCommandSucceeded(reply)) {
+ return reply.lineArray;
+ }
+ throw new Error(reply.lineArray.join("\n"));
+ },
+
+ async _readBoolSetting(aSetting) {
+ let lineArray = await this._readSetting(aSetting);
+ if (lineArray.length != 1) {
+ throw new Error(
+ `Expected an array with length 1 but received array of length ${
+ lineArray.length
+ }`
+ );
+ }
+
+ let retval = lineArray[0];
+ switch (retval) {
+ case "0":
+ return false;
+ case "1":
+ return true;
+ default:
+ throw new Error(`Expected boolean (1 or 0) but received '${retval}'`);
+ }
+ },
+
+ async _readStringSetting(aSetting) {
+ let lineArray = await this._readSetting(aSetting);
+ if (lineArray.length != 1) {
+ throw new Error(
+ `Expected an array with length 1 but received array of length ${
+ lineArray.length
+ }`
+ );
+ }
+ return lineArray[0];
+ },
+
+ async _readStringArraySetting(aSetting) {
+ let lineArray = await this._readSetting(aSetting);
+ return lineArray;
+ },
+
+ async readBoolSetting(aSetting) {
+ let value = await this._readBoolSetting(aSetting);
+ this._settingsCache.set(aSetting, value);
+ return value;
+ },
+
+ async readStringSetting(aSetting) {
+ let value = await this._readStringSetting(aSetting);
+ this._settingsCache.set(aSetting, value);
+ return value;
+ },
+
+ async readStringArraySetting(aSetting) {
+ let value = await this._readStringArraySetting(aSetting);
+ this._settingsCache.set(aSetting, value);
+ return value;
+ },
+
+ // writes current tor settings to disk
+ async flushSettings() {
+ await this.sendCommand("SAVECONF");
+ },
+
+ getLog() {
+ let countObj = { value: 0 };
+ let torLog = this._tlps.TorGetLog(countObj);
+ return torLog;
+ },
+
+ // true if we launched and control tor, false if using system tor
+ get ownsTorDaemon() {
+ return this._TorLauncherUtil.shouldStartAndOwnTor;
+ },
+
+ // Assumes `ownsTorDaemon` is true
+ isNetworkDisabled() {
+ const reply = TorProtocolService._TorLauncherProtocolService.TorGetConfBool(
+ "DisableNetwork",
+ true
+ );
+ if (TorProtocolService._TorLauncherProtocolService.TorCommandSucceeded(reply)) {
+ return reply.retVal;
+ }
+ return true;
+ },
+
+ async enableNetwork() {
+ let settings = {};
+ settings.DisableNetwork = false;
+ let errorObject = {};
+ if (! await this._TorLauncherProtocolService.TorSetConfWithReply(settings, errorObject)) {
+ throw new Error(errorObject.details);
+ }
+ },
+
+ async sendCommand(cmd) {
+ return await this._TorLauncherProtocolService.TorSendCommand(cmd);
+ },
+
+ retrieveBootstrapStatus() {
+ return this._TorLauncherProtocolService.TorRetrieveBootstrapStatus();
+ },
+
+ _GetSaveSettingsErrorMessage(aDetails) {
+ try {
+ return this._TorLauncherUtil.getSaveSettingsErrorMessage(aDetails);
+ } catch (e) {
+ console.log("GetSaveSettingsErrorMessage error", e);
+ return "Unexpected Error";
+ }
+ },
+
+ async setConfWithReply(settings) {
+ let result = false;
+ const error = {};
+ try {
+ result = await this._TorLauncherProtocolService.TorSetConfWithReply(settings, error);
+ } catch (e) {
+ console.log("TorSetConfWithReply error", e);
+ error.details = this._GetSaveSettingsErrorMessage(e.message);
+ }
+ return { result, error };
+ },
+
+ isBootstrapDone() {
+ return this._TorProcessService.mIsBootstrapDone;
+ },
+
+ clearBootstrapError() {
+ return this._TorProcessService.TorClearBootstrapError();
+ },
+
+ torBootstrapErrorOccurred() {
+ return this._TorProcessService.TorBootstrapErrorOccurred;
+ },
+
+ // Resolves to null if ok, or an error otherwise
+ async connect() {
+ const kTorConfKeyDisableNetwork = "DisableNetwork";
+ const settings = {};
+ settings[kTorConfKeyDisableNetwork] = false;
+ const { result, error } = await this.setConfWithReply(settings);
+ if (!result) {
+ return error;
+ }
+ try {
+ await this.sendCommand("SAVECONF");
+ this.clearBootstrapError();
+ this.retrieveBootstrapStatus();
+ } catch (e) {
+ return error;
+ }
+ return null;
+ },
+
+ torLogHasWarnOrErr() {
+ return this._TorLauncherProtocolService.TorLogHasWarnOrErr;
+ },
+
+ async torStopBootstrap() {
+ // Tell tor to disable use of the network; this should stop the bootstrap
+ // process.
+ const kErrorPrefix = "Setting DisableNetwork=1 failed: ";
+ try {
+ let settings = {};
+ settings.DisableNetwork = true;
+ const { result, error } = await this.setConfWithReply(settings);
+ if (!result) {
+ console.log(
+ `Error stopping bootstrap ${kErrorPrefix} ${error.details}`
+ );
+ }
+ } catch (e) {
+ console.log(`Error stopping bootstrap ${kErrorPrefix} ${e}`);
+ }
+ this.retrieveBootstrapStatus();
+ },
+
+ get torProcessStatus() {
+ if (this._TorProcessService) {
+ return this._TorProcessService.TorProcessStatus;
+ }
+ return TorProcessStatus.Unknown;
+ },
+};
diff --git a/browser/modules/moz.build b/browser/modules/moz.build
index 3774c12f4ef0..1f7c6bc4c67e 100644
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -128,6 +128,7 @@ EXTRA_JS_MODULES += [
"AboutNewTab.jsm",
"AppUpdater.jsm",
"AsyncTabSwitcher.jsm",
+ "BridgeDB.jsm",
"BrowserUIUtils.jsm",
"BrowserUsageTelemetry.jsm",
"BrowserWindowTracker.jsm",
@@ -152,6 +153,7 @@ EXTRA_JS_MODULES += [
"TabsList.jsm",
"TabUnloader.jsm",
"ThemeVariableMap.jsm",
+ "TorProtocolService.jsm",
"TorStrings.jsm",
"TransientPrefs.jsm",
"webrtcUI.jsm",
1
0

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 30237: Add v3 onion services client authentication prompt
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit 5367c81a994ef3e444d3789f90541436c252bf1d
Author: Kathy Brade <brade(a)pearlcrescent.com>
Date: Tue Nov 12 16:11:05 2019 -0500
Bug 30237: Add v3 onion services client authentication prompt
When Tor informs the browser that client authentication is needed,
temporarily load about:blank instead of about:neterror and prompt
for the user's key.
If a correctly formatted key is entered, use Tor's ONION_CLIENT_AUTH_ADD
control port command to add the key (via Torbutton's control port
module) and reload the page.
If the user cancels the prompt, display the standard about:neterror
"Unable to connect" page. This requires a small change to
browser/actors/NetErrorChild.jsm to account for the fact that the
docShell no longer has the failedChannel information. The failedChannel
is used to extract TLS-related error info, which is not applicable
in the case of a canceled .onion authentication prompt.
Add a leaveOpen option to PopupNotifications.show so we can display
error messages within the popup notification doorhanger without
closing the prompt.
Add support for onion services strings to the TorStrings module.
Add support for Tor extended SOCKS errors (Tor proposal 304) to the
socket transport and SOCKS layers. Improved display of all of these
errors will be implemented as part of bug 30025.
Also fixes bug 19757:
Add a "Remember this key" checkbox to the client auth prompt.
Add an "Onion Services Authentication" section within the
about:preferences "Privacy & Security section" to allow
viewing and removal of v3 onion client auth keys that have
been stored on disk.
Also fixes bug 19251: use enhanced error pages for onion service errors.
---
browser/actors/NetErrorChild.jsm | 7 +
browser/base/content/browser.js | 10 +
browser/base/content/browser.xhtml | 1 +
browser/base/content/certerror/aboutNetError.js | 10 +-
browser/base/content/certerror/aboutNetError.xhtml | 1 +
browser/base/content/main-popupset.inc.xhtml | 1 +
browser/base/content/navigator-toolbox.inc.xhtml | 1 +
browser/base/content/tab-content.js | 6 +
browser/components/moz.build | 1 +
.../content/authNotificationIcon.inc.xhtml | 6 +
.../onionservices/content/authPopup.inc.xhtml | 16 ++
.../onionservices/content/authPreferences.css | 20 ++
.../content/authPreferences.inc.xhtml | 19 ++
.../onionservices/content/authPreferences.js | 66 +++++
.../components/onionservices/content/authPrompt.js | 316 +++++++++++++++++++++
.../components/onionservices/content/authUtil.jsm | 47 +++
.../onionservices/content/netError/browser.svg | 3 +
.../onionservices/content/netError/network.svg | 3 +
.../content/netError/onionNetError.css | 88 ++++++
.../content/netError/onionNetError.js | 243 ++++++++++++++++
.../onionservices/content/netError/onionsite.svg | 8 +
.../onionservices/content/onionservices.css | 69 +++++
.../onionservices/content/savedKeysDialog.js | 259 +++++++++++++++++
.../onionservices/content/savedKeysDialog.xhtml | 42 +++
browser/components/onionservices/jar.mn | 9 +
browser/components/onionservices/moz.build | 1 +
browser/components/preferences/preferences.xhtml | 1 +
browser/components/preferences/privacy.inc.xhtml | 2 +
browser/components/preferences/privacy.js | 7 +
browser/themes/shared/notification-icons.inc.css | 3 +
docshell/base/nsDocShell.cpp | 81 +++++-
dom/ipc/BrowserParent.cpp | 21 ++
dom/ipc/BrowserParent.h | 3 +
dom/ipc/PBrowser.ipdl | 9 +
js/xpconnect/src/xpc.msg | 10 +
netwerk/base/nsSocketTransport2.cpp | 6 +
netwerk/socket/nsSOCKSIOLayer.cpp | 49 ++++
toolkit/modules/PopupNotifications.jsm | 6 +
toolkit/modules/RemotePageAccessManager.jsm | 1 +
.../lib/environments/frame-script.js | 1 +
xpcom/base/ErrorList.py | 22 ++
41 files changed, 1473 insertions(+), 2 deletions(-)
diff --git a/browser/actors/NetErrorChild.jsm b/browser/actors/NetErrorChild.jsm
index 82978412fe24..164fb7c95cd1 100644
--- a/browser/actors/NetErrorChild.jsm
+++ b/browser/actors/NetErrorChild.jsm
@@ -13,6 +13,8 @@ const { RemotePageChild } = ChromeUtils.import(
"resource://gre/actors/RemotePageChild.jsm"
);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
XPCOMUtils.defineLazyServiceGetter(
this,
"gSerializationHelper",
@@ -33,6 +35,7 @@ class NetErrorChild extends RemotePageChild {
"RPMAddToHistogram",
"RPMRecordTelemetryEvent",
"RPMGetHttpResponseHeader",
+ "RPMGetTorStrings",
];
this.exportFunctions(exportableFunctions);
}
@@ -115,4 +118,8 @@ class NetErrorChild extends RemotePageChild {
return "";
}
+
+ RPMGetTorStrings() {
+ return Cu.cloneInto(TorStrings.onionServices, this.contentWindow);
+ }
}
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index b121ee301ee7..f33442c4badc 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -229,6 +229,11 @@ XPCOMUtils.defineLazyScriptGetter(
["SecurityLevelButton"],
"chrome://browser/content/securitylevel/securityLevel.js"
);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["OnionAuthPrompt"],
+ "chrome://browser/content/onionservices/authPrompt.js"
+);
XPCOMUtils.defineLazyScriptGetter(
this,
"gEditItemOverlay",
@@ -1777,6 +1782,9 @@ var gBrowserInit = {
// Init the SecuritySettingsButton
SecurityLevelButton.init();
+ // Init the OnionAuthPrompt
+ OnionAuthPrompt.init();
+
// Certain kinds of automigration rely on this notification to complete
// their tasks BEFORE the browser window is shown. SessionStore uses it to
// restore tabs into windows AFTER important parts like gMultiProcessBrowser
@@ -2494,6 +2502,8 @@ var gBrowserInit = {
SecurityLevelButton.uninit();
+ OnionAuthPrompt.uninit();
+
gAccessibilityServiceIndicator.uninit();
if (gToolbarKeyNavEnabled) {
diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml
index 8fbfa05196b0..65445a099148 100644
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -33,6 +33,7 @@
<?xml-stylesheet href="chrome://browser/skin/places/editBookmark.css" type="text/css"?>
<?xml-stylesheet href="chrome://torbutton/skin/tor-circuit-display.css" type="text/css"?>
<?xml-stylesheet href="chrome://torbutton/skin/torbutton.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/onionservices/onionservices.css" type="text/css"?>
# All DTD information is stored in a separate file so that it can be shared by
# hiddenWindowMac.xhtml.
diff --git a/browser/base/content/certerror/aboutNetError.js b/browser/base/content/certerror/aboutNetError.js
index 31c4838a053d..e5b223025a8b 100644
--- a/browser/base/content/certerror/aboutNetError.js
+++ b/browser/base/content/certerror/aboutNetError.js
@@ -3,6 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env mozilla/frame-script */
+/* import-globals-from ../../components/onionservices/content/netError/onionNetError.js */
import "chrome://global/content/certviewer/pvutils_bundle.js";
import "chrome://global/content/certviewer/asn1js_bundle.js";
@@ -307,7 +308,10 @@ function initPage() {
errDesc = document.getElementById("ed_generic");
}
- setErrorPageStrings(err);
+ const isOnionError = err.startsWith("onionServices.");
+ if (!isOnionError) {
+ setErrorPageStrings(err);
+ }
var sd = document.getElementById("errorShortDescText");
if (sd) {
@@ -459,6 +463,10 @@ function initPage() {
span.textContent = HOST_NAME;
}
}
+
+ if (isOnionError) {
+ OnionServicesAboutNetError.initPage(document);
+ }
}
function setupBlockingReportingUI() {
diff --git a/browser/base/content/certerror/aboutNetError.xhtml b/browser/base/content/certerror/aboutNetError.xhtml
index c645a2f2cc77..bf9a8fd58347 100644
--- a/browser/base/content/certerror/aboutNetError.xhtml
+++ b/browser/base/content/certerror/aboutNetError.xhtml
@@ -209,5 +209,6 @@
</div>
</body>
<script src="chrome://browser/content/certerror/aboutNetErrorCodes.js"/>
+ <script src="chrome://browser/content/onionservices/netError/onionNetError.js"/>
<script type="module" src="chrome://browser/content/certerror/aboutNetError.js"/>
</html>
diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml
index adf0a4f59e4b..f627b97cba3f 100644
--- a/browser/base/content/main-popupset.inc.xhtml
+++ b/browser/base/content/main-popupset.inc.xhtml
@@ -520,6 +520,7 @@
#include ../../components/downloads/content/downloadsPanel.inc.xhtml
#include ../../../devtools/startup/enableDevToolsPopup.inc.xhtml
#include ../../components/securitylevel/content/securityLevelPanel.inc.xhtml
+#include ../../components/onionservices/content/authPopup.inc.xhtml
#include browser-allTabsMenu.inc.xhtml
<tooltip id="dynamic-shortcut-tooltip"
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index 7359cec49696..7a2715e9e604 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -268,6 +268,7 @@
data-l10n-id="urlbar-indexed-db-notification-anchor"/>
<image id="password-notification-icon" class="notification-anchor-icon login-icon" role="button"
data-l10n-id="urlbar-password-notification-anchor"/>
+#include ../../components/onionservices/content/authNotificationIcon.inc.xhtml
<stack id="plugins-notification-icon" class="notification-anchor-icon" role="button" align="center" data-l10n-id="urlbar-plugins-notification-anchor">
<image class="plugin-icon" />
<image id="plugin-icon-badge" />
diff --git a/browser/base/content/tab-content.js b/browser/base/content/tab-content.js
index 83e55cf5ed87..96360a4307d2 100644
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -7,4 +7,10 @@
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { OnionAuthUtil } = ChromeUtils.import(
+ "chrome://browser/content/onionservices/authUtil.jsm"
+);
+
Services.obs.notifyObservers(this, "tab-content-frameloader-created");
+
+OnionAuthUtil.addCancelMessageListener(this, docShell);
diff --git a/browser/components/moz.build b/browser/components/moz.build
index b409974a965c..0ea2969e60b0 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -38,6 +38,7 @@ DIRS += [
"extensions",
"migration",
"newtab",
+ "onionservices",
"originattributes",
"ion",
"places",
diff --git a/browser/components/onionservices/content/authNotificationIcon.inc.xhtml b/browser/components/onionservices/content/authNotificationIcon.inc.xhtml
new file mode 100644
index 000000000000..91274d612739
--- /dev/null
+++ b/browser/components/onionservices/content/authNotificationIcon.inc.xhtml
@@ -0,0 +1,6 @@
+# Copyright (c) 2020, The Tor Project, Inc.
+
+<image id="tor-clientauth-notification-icon"
+ class="notification-anchor-icon tor-clientauth-icon"
+ role="button"
+ tooltiptext="&torbutton.onionServices.authPrompt.tooltip;"/>
diff --git a/browser/components/onionservices/content/authPopup.inc.xhtml b/browser/components/onionservices/content/authPopup.inc.xhtml
new file mode 100644
index 000000000000..bd0ec3aa0b00
--- /dev/null
+++ b/browser/components/onionservices/content/authPopup.inc.xhtml
@@ -0,0 +1,16 @@
+# Copyright (c) 2020, The Tor Project, Inc.
+
+<popupnotification id="tor-clientauth-notification" hidden="true">
+ <popupnotificationcontent orient="vertical">
+ <description id="tor-clientauth-notification-desc"/>
+ <label id="tor-clientauth-notification-learnmore"
+ class="text-link popup-notification-learnmore-link"
+ is="text-link"/>
+ <html:div>
+ <html:input id="tor-clientauth-notification-key" type="password"/>
+ <html:div id="tor-clientauth-warning"/>
+ <checkbox id="tor-clientauth-persistkey-checkbox"
+ label="&torbutton.onionServices.authPrompt.persistCheckboxLabel;"/>
+ </html:div>
+ </popupnotificationcontent>
+</popupnotification>
diff --git a/browser/components/onionservices/content/authPreferences.css b/browser/components/onionservices/content/authPreferences.css
new file mode 100644
index 000000000000..b3fb79b26ddc
--- /dev/null
+++ b/browser/components/onionservices/content/authPreferences.css
@@ -0,0 +1,20 @@
+/* Copyright (c) 2020, The Tor Project, Inc. */
+
+#torOnionServiceKeys-overview-container {
+ margin-right: 30px;
+}
+
+#onionservices-savedkeys-tree treechildren::-moz-tree-cell-text {
+ font-size: 80%;
+}
+
+#onionservices-savedkeys-errorContainer {
+ margin-top: 4px;
+ min-height: 3em;
+}
+
+#onionservices-savedkeys-errorIcon {
+ margin-right: 4px;
+ list-style-image: url("chrome://browser/skin/warning.svg");
+ visibility: hidden;
+}
diff --git a/browser/components/onionservices/content/authPreferences.inc.xhtml b/browser/components/onionservices/content/authPreferences.inc.xhtml
new file mode 100644
index 000000000000..f69c9dde66a2
--- /dev/null
+++ b/browser/components/onionservices/content/authPreferences.inc.xhtml
@@ -0,0 +1,19 @@
+# Copyright (c) 2020, The Tor Project, Inc.
+
+<groupbox id="torOnionServiceKeys" orient="vertical"
+ data-category="panePrivacy" hidden="true">
+ <label><html:h2 id="torOnionServiceKeys-header"/></label>
+ <hbox>
+ <description id="torOnionServiceKeys-overview-container" flex="1">
+ <html:span id="torOnionServiceKeys-overview"
+ class="tail-with-learn-more"/>
+ <label id="torOnionServiceKeys-learnMore" class="learnMore text-link"
+ is="text-link"/>
+ </description>
+ <vbox align="end">
+ <button id="torOnionServiceKeys-savedKeys"
+ is="highlightable-button"
+ class="accessory-button"/>
+ </vbox>
+ </hbox>
+</groupbox>
diff --git a/browser/components/onionservices/content/authPreferences.js b/browser/components/onionservices/content/authPreferences.js
new file mode 100644
index 000000000000..52f8272020cc
--- /dev/null
+++ b/browser/components/onionservices/content/authPreferences.js
@@ -0,0 +1,66 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "TorStrings",
+ "resource:///modules/TorStrings.jsm"
+);
+
+/*
+ Onion Services Client Authentication Preferences Code
+
+ Code to handle init and update of onion services authentication section
+ in about:preferences#privacy
+*/
+
+const OnionServicesAuthPreferences = {
+ selector: {
+ groupBox: "#torOnionServiceKeys",
+ header: "#torOnionServiceKeys-header",
+ overview: "#torOnionServiceKeys-overview",
+ learnMore: "#torOnionServiceKeys-learnMore",
+ savedKeysButton: "#torOnionServiceKeys-savedKeys",
+ },
+
+ init() {
+ // populate XUL with localized strings
+ this._populateXUL();
+ },
+
+ _populateXUL() {
+ const groupbox = document.querySelector(this.selector.groupBox);
+
+ let elem = groupbox.querySelector(this.selector.header);
+ elem.textContent = TorStrings.onionServices.authPreferences.header;
+
+ elem = groupbox.querySelector(this.selector.overview);
+ elem.textContent = TorStrings.onionServices.authPreferences.overview;
+
+ elem = groupbox.querySelector(this.selector.learnMore);
+ elem.setAttribute("value", TorStrings.onionServices.learnMore);
+ elem.setAttribute("href", TorStrings.onionServices.learnMoreURL);
+
+ elem = groupbox.querySelector(this.selector.savedKeysButton);
+ elem.setAttribute(
+ "label",
+ TorStrings.onionServices.authPreferences.savedKeys
+ );
+ elem.addEventListener("command", () =>
+ OnionServicesAuthPreferences.onViewSavedKeys()
+ );
+ },
+
+ onViewSavedKeys() {
+ gSubDialog.open(
+ "chrome://browser/content/onionservices/savedKeysDialog.xhtml"
+ );
+ },
+}; // OnionServicesAuthPreferences
+
+Object.defineProperty(this, "OnionServicesAuthPreferences", {
+ value: OnionServicesAuthPreferences,
+ enumerable: true,
+ writable: false,
+});
diff --git a/browser/components/onionservices/content/authPrompt.js b/browser/components/onionservices/content/authPrompt.js
new file mode 100644
index 000000000000..d4a59ac46487
--- /dev/null
+++ b/browser/components/onionservices/content/authPrompt.js
@@ -0,0 +1,316 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ OnionAuthUtil: "chrome://browser/content/onionservices/authUtil.jsm",
+ CommonUtils: "resource://services-common/utils.js",
+ TorStrings: "resource:///modules/TorStrings.jsm",
+});
+
+const OnionAuthPrompt = (function() {
+ // OnionServicesAuthPrompt objects run within the main/chrome process.
+ // aReason is the topic passed within the observer notification that is
+ // causing this auth prompt to be displayed.
+ function OnionServicesAuthPrompt(aBrowser, aFailedURI, aReason, aOnionName) {
+ this._browser = aBrowser;
+ this._failedURI = aFailedURI;
+ this._reasonForPrompt = aReason;
+ this._onionName = aOnionName;
+ }
+
+ OnionServicesAuthPrompt.prototype = {
+ show(aWarningMessage) {
+ let mainAction = {
+ label: TorStrings.onionServices.authPrompt.done,
+ accessKey: TorStrings.onionServices.authPrompt.doneAccessKey,
+ leaveOpen: true, // Callback is responsible for closing the notification.
+ callback: this._onDone.bind(this),
+ };
+
+ let dialogBundle = Services.strings.createBundle(
+ "chrome://global/locale/dialog.properties");
+
+ let cancelAccessKey = dialogBundle.GetStringFromName("accesskey-cancel");
+ if (!cancelAccessKey)
+ cancelAccessKey = "c"; // required by PopupNotifications.show()
+
+ let cancelAction = {
+ label: dialogBundle.GetStringFromName("button-cancel"),
+ accessKey: cancelAccessKey,
+ callback: this._onCancel.bind(this),
+ };
+
+ let _this = this;
+ let options = {
+ autofocus: true,
+ hideClose: true,
+ persistent: true,
+ removeOnDismissal: false,
+ eventCallback(aTopic) {
+ if (aTopic === "showing") {
+ _this._onPromptShowing(aWarningMessage);
+ } else if (aTopic === "shown") {
+ _this._onPromptShown();
+ } else if (aTopic === "removed") {
+ _this._onPromptRemoved();
+ }
+ }
+ };
+
+ this._prompt = PopupNotifications.show(this._browser,
+ OnionAuthUtil.domid.notification, "",
+ OnionAuthUtil.domid.anchor,
+ mainAction, [cancelAction], options);
+ },
+
+ _onPromptShowing(aWarningMessage) {
+ let xulDoc = this._browser.ownerDocument;
+ let descElem = xulDoc.getElementById(OnionAuthUtil.domid.description);
+ if (descElem) {
+ // Handle replacement of the onion name within the localized
+ // string ourselves so we can show the onion name as bold text.
+ // We do this by splitting the localized string and creating
+ // several HTML <span> elements.
+ while (descElem.firstChild)
+ descElem.removeChild(descElem.firstChild);
+
+ let fmtString = TorStrings.onionServices.authPrompt.description;
+ let prefix = "";
+ let suffix = "";
+ const kToReplace = "%S";
+ let idx = fmtString.indexOf(kToReplace);
+ if (idx < 0) {
+ prefix = fmtString;
+ } else {
+ prefix = fmtString.substring(0, idx);
+ suffix = fmtString.substring(idx + kToReplace.length);
+ }
+
+ const kHTMLNS = "http://www.w3.org/1999/xhtml";
+ let span = xulDoc.createElementNS(kHTMLNS, "span");
+ span.textContent = prefix;
+ descElem.appendChild(span);
+ span = xulDoc.createElementNS(kHTMLNS, "span");
+ span.id = OnionAuthUtil.domid.onionNameSpan;
+ span.textContent = this._onionName;
+ descElem.appendChild(span);
+ span = xulDoc.createElementNS(kHTMLNS, "span");
+ span.textContent = suffix;
+ descElem.appendChild(span);
+ }
+
+ // Set "Learn More" label and href.
+ let learnMoreElem = xulDoc.getElementById(OnionAuthUtil.domid.learnMore);
+ if (learnMoreElem) {
+ learnMoreElem.setAttribute("value", TorStrings.onionServices.learnMore);
+ learnMoreElem.setAttribute("href", TorStrings.onionServices.learnMoreURL);
+ }
+
+ this._showWarning(aWarningMessage);
+ let checkboxElem = this._getCheckboxElement();
+ if (checkboxElem) {
+ checkboxElem.checked = false;
+ }
+ },
+
+ _onPromptShown() {
+ let keyElem = this._getKeyElement();
+ if (keyElem) {
+ keyElem.setAttribute("placeholder",
+ TorStrings.onionServices.authPrompt.keyPlaceholder);
+ this._boundOnKeyFieldKeyPress = this._onKeyFieldKeyPress.bind(this);
+ this._boundOnKeyFieldInput = this._onKeyFieldInput.bind(this);
+ keyElem.addEventListener("keypress", this._boundOnKeyFieldKeyPress);
+ keyElem.addEventListener("input", this._boundOnKeyFieldInput);
+ keyElem.focus();
+ }
+ },
+
+ _onPromptRemoved() {
+ if (this._boundOnKeyFieldKeyPress) {
+ let keyElem = this._getKeyElement();
+ if (keyElem) {
+ keyElem.value = "";
+ keyElem.removeEventListener("keypress",
+ this._boundOnKeyFieldKeyPress);
+ this._boundOnKeyFieldKeyPress = undefined;
+ keyElem.removeEventListener("input", this._boundOnKeyFieldInput);
+ this._boundOnKeyFieldInput = undefined;
+ }
+ }
+ },
+
+ _onKeyFieldKeyPress(aEvent) {
+ if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
+ this._onDone();
+ } else if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+ this._prompt.remove();
+ this._onCancel();
+ }
+ },
+
+ _onKeyFieldInput(aEvent) {
+ this._showWarning(undefined); // Remove the warning.
+ },
+
+ _onDone() {
+ let keyElem = this._getKeyElement();
+ if (!keyElem)
+ return;
+
+ let base64key = this._keyToBase64(keyElem.value);
+ if (!base64key) {
+ this._showWarning(TorStrings.onionServices.authPrompt.invalidKey);
+ return;
+ }
+
+ this._prompt.remove();
+
+ // Use Torbutton's controller module to add the private key to Tor.
+ let controllerFailureMsg =
+ TorStrings.onionServices.authPrompt.failedToSetKey;
+ try {
+ let { controller } =
+ Cu.import("resource://torbutton/modules/tor-control-port.js", {});
+ let torController = controller(aError => {
+ this.show(controllerFailureMsg);
+ });
+ let onionAddr = this._onionName.toLowerCase().replace(/\.onion$/, "");
+ let checkboxElem = this._getCheckboxElement();
+ let isPermanent = (checkboxElem && checkboxElem.checked);
+ torController.onionAuthAdd(onionAddr, base64key, isPermanent)
+ .then(aResponse => {
+ // Success! Reload the page.
+ this._browser.sendMessageToActor(
+ "Browser:Reload",
+ {},
+ "BrowserTab"
+ );
+ })
+ .catch(aError => {
+ if (aError.torMessage)
+ this.show(aError.torMessage);
+ else
+ this.show(controllerFailureMsg);
+ });
+ } catch (e) {
+ this.show(controllerFailureMsg);
+ }
+ },
+
+ _onCancel() {
+ // Arrange for an error page to be displayed.
+ this._browser.messageManager.sendAsyncMessage(
+ OnionAuthUtil.message.authPromptCanceled,
+ {failedURI: this._failedURI.spec,
+ reasonForPrompt: this._reasonForPrompt});
+ },
+
+ _getKeyElement() {
+ let xulDoc = this._browser.ownerDocument;
+ return xulDoc.getElementById(OnionAuthUtil.domid.keyElement);
+ },
+
+ _getCheckboxElement() {
+ let xulDoc = this._browser.ownerDocument;
+ return xulDoc.getElementById(OnionAuthUtil.domid.checkboxElement);
+ },
+
+ _showWarning(aWarningMessage) {
+ let xulDoc = this._browser.ownerDocument;
+ let warningElem =
+ xulDoc.getElementById(OnionAuthUtil.domid.warningElement);
+ let keyElem = this._getKeyElement();
+ if (warningElem) {
+ if (aWarningMessage) {
+ warningElem.textContent = aWarningMessage;
+ warningElem.removeAttribute("hidden");
+ if (keyElem)
+ keyElem.className = "invalid";
+ } else {
+ warningElem.setAttribute("hidden", "true");
+ if (keyElem)
+ keyElem.className = "";
+ }
+ }
+ },
+
+ // Returns undefined if the key is the wrong length or format.
+ _keyToBase64(aKeyString) {
+ if (!aKeyString)
+ return undefined;
+
+ let base64key;
+ if (aKeyString.length == 52) {
+ // The key is probably base32-encoded. Attempt to decode.
+ // Although base32 specifies uppercase letters, we accept lowercase
+ // as well because users may type in lowercase or copy a key out of
+ // a tor onion-auth file (which uses lowercase).
+ let rawKey;
+ try {
+ rawKey = CommonUtils.decodeBase32(aKeyString.toUpperCase());
+ } catch (e) {}
+
+ if (rawKey) try {
+ base64key = btoa(rawKey);
+ } catch (e) {}
+ } else if ((aKeyString.length == 44) &&
+ /^[a-zA-Z0-9+/]*=*$/.test(aKeyString)) {
+ // The key appears to be a correctly formatted base64 value. If not,
+ // tor will return an error when we try to add the key via the
+ // control port.
+ base64key = aKeyString;
+ }
+
+ return base64key;
+ },
+ };
+
+ let retval = {
+ init() {
+ Services.obs.addObserver(this, OnionAuthUtil.topic.clientAuthMissing);
+ Services.obs.addObserver(this, OnionAuthUtil.topic.clientAuthIncorrect);
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, OnionAuthUtil.topic.clientAuthMissing);
+ Services.obs.removeObserver(this, OnionAuthUtil.topic.clientAuthIncorrect);
+ },
+
+ // aSubject is the DOM Window or browser where the prompt should be shown.
+ // aData contains the .onion name.
+ observe(aSubject, aTopic, aData) {
+ if ((aTopic != OnionAuthUtil.topic.clientAuthMissing) &&
+ (aTopic != OnionAuthUtil.topic.clientAuthIncorrect)) {
+ return;
+ }
+
+ let browser;
+ if (aSubject instanceof Ci.nsIDOMWindow) {
+ let contentWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ browser = contentWindow.docShell.chromeEventHandler;
+ } else {
+ browser = aSubject.QueryInterface(Ci.nsIBrowser);
+ }
+
+ if (!gBrowser.browsers.some(aBrowser => aBrowser == browser)) {
+ return; // This window does not contain the subject browser; ignore.
+ }
+
+ let failedURI = browser.currentURI;
+ let authPrompt = new OnionServicesAuthPrompt(browser, failedURI,
+ aTopic, aData);
+ authPrompt.show(undefined);
+ }
+ };
+
+ return retval;
+})(); /* OnionAuthPrompt */
+
+
+Object.defineProperty(this, "OnionAuthPrompt", {
+ value: OnionAuthPrompt,
+ enumerable: true,
+ writable: false
+});
diff --git a/browser/components/onionservices/content/authUtil.jsm b/browser/components/onionservices/content/authUtil.jsm
new file mode 100644
index 000000000000..c9d83774da1f
--- /dev/null
+++ b/browser/components/onionservices/content/authUtil.jsm
@@ -0,0 +1,47 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "OnionAuthUtil",
+];
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const OnionAuthUtil = {
+ topic: {
+ clientAuthMissing: "tor-onion-services-clientauth-missing",
+ clientAuthIncorrect: "tor-onion-services-clientauth-incorrect",
+ },
+ message: {
+ authPromptCanceled: "Tor:OnionServicesAuthPromptCanceled",
+ },
+ domid: {
+ anchor: "tor-clientauth-notification-icon",
+ notification: "tor-clientauth",
+ description: "tor-clientauth-notification-desc",
+ learnMore: "tor-clientauth-notification-learnmore",
+ onionNameSpan: "tor-clientauth-notification-onionname",
+ keyElement: "tor-clientauth-notification-key",
+ warningElement: "tor-clientauth-warning",
+ checkboxElement: "tor-clientauth-persistkey-checkbox",
+ },
+
+ addCancelMessageListener(aTabContent, aDocShell) {
+ aTabContent.addMessageListener(this.message.authPromptCanceled,
+ (aMessage) => {
+ // Upon cancellation of the client authentication prompt, display
+ // the appropriate error page. When calling the docShell
+ // displayLoadError() function, we pass undefined for the failed
+ // channel so that displayLoadError() can determine that it should
+ // not display the client authentication prompt a second time.
+ let failedURI = Services.io.newURI(aMessage.data.failedURI);
+ let reasonForPrompt = aMessage.data.reasonForPrompt;
+ let errorCode =
+ (reasonForPrompt === this.topic.clientAuthMissing) ?
+ Cr.NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH :
+ Cr.NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH;
+ aDocShell.displayLoadError(errorCode, failedURI, undefined, undefined);
+ });
+ },
+};
diff --git a/browser/components/onionservices/content/netError/browser.svg b/browser/components/onionservices/content/netError/browser.svg
new file mode 100644
index 000000000000..1359679f7171
--- /dev/null
+++ b/browser/components/onionservices/content/netError/browser.svg
@@ -0,0 +1,3 @@
+<svg fill="none" height="60" viewBox="0 0 60 60" width="60" xmlns="http://www.w3.org/2000/svg">
+ <path fill="context-fill" fill-opacity="context-fill-opacity" d="m49 6h-37.5c-1.98912 0-3.89678.79018-5.3033 2.1967s-2.1967 3.3142-2.1967 5.3033v33.75c0 1.9891.79018 3.8968 2.1967 5.3033s3.31418 2.1967 5.3033 2.1967h37.5c1.9891 0 3.8968-.7902 5.3033-2.1967s2.1967-3.3142 2.1967-5.3033v-33.75c0-1.9891-.7902-3.89678-2.1967-5.3033s-3.3142-2.1967-5.3033-2.1967zm-38.0625 4.6875h38.625l2.25 2.25v8.0625h-43.125v-8.0625zm38.625 39.375h-38.625l-2.25-2.25v-22.125h43.125v22.125z"/>
+</svg>
diff --git a/browser/components/onionservices/content/netError/network.svg b/browser/components/onionservices/content/netError/network.svg
new file mode 100644
index 000000000000..68610e30bfca
--- /dev/null
+++ b/browser/components/onionservices/content/netError/network.svg
@@ -0,0 +1,3 @@
+<svg fill="none" height="60" viewBox="0 0 60 60" width="60" xmlns="http://www.w3.org/2000/svg">
+ <path fill="context-fill" fill-opacity="context-fill-opacity" d="m30 1.875c-7.4592 0-14.6129 2.96316-19.8874 8.2376-5.27444 5.2745-8.2376 12.4282-8.2376 19.8874s2.96316 14.6129 8.2376 19.8874c5.2745 5.2744 12.4282 8.2376 19.8874 8.2376s14.6129-2.9632 19.8874-8.2376c5.2744-5.2745 8.2376-12.4282 8.2376-19.8874s-2.9632-14.6129-8.2376-19.8874c-5.2745-5.27444-12.4282-8.2376-19.8874-8.2376zm9.1762 6.5625c3.8504 1.6533 7.1876 4.3079 9.6646 7.6877 2.477 3.3799 4.0034 7.3615 4.4205 11.531h-8.3588c-.4617-6.9829-2.9858-13.6716-7.2525-19.2187zm-7.6837 0c5.0739 5.1814 8.1562 11.9874 8.7037 19.2187h-20.3924c.5475-7.2313 3.6298-14.0373 8.7037-19.2187zm-10.6725 0h1.53c-4.2651 5.548-6.789 12.2362-7.2525 19.2187h-8.35875c.41632-4.1692 1.942-8.1508 4.41835-11.5306 2.4764-3.3799 5.813-6.0346 9.6629-7.6881zm0 43.125c-3.8504-1.6528-7.1874-4.3074-9.6639-7.6874-2.47642-3.38-4.0018-7.3619-4.41735-11.5313h8.35875c.4617 6.9829 2.9858 13.6716 7.2525 19.2187zm7.6875 0c-5.0739-5.1814-8.1562-11.9874-8.7037-19.2
187h20.3887c-.5475 7.2313-3.6298 14.0373-8.7037 19.2187zm10.6725 0h-1.5338c4.2683-5.5462 6.7926-12.2354 7.2525-19.2187h8.3588c-.4156 4.1689-1.9406 8.1504-4.4163 11.5302-2.4757 3.3799-5.8118 6.0348-9.6612 7.6885z"/>
+</svg>
diff --git a/browser/components/onionservices/content/netError/onionNetError.css b/browser/components/onionservices/content/netError/onionNetError.css
new file mode 100644
index 000000000000..2c92b187b71c
--- /dev/null
+++ b/browser/components/onionservices/content/netError/onionNetError.css
@@ -0,0 +1,88 @@
+/* Copyright (c) 2020, The Tor Project, Inc. */
+
+#onionErrorDiagramContainer {
+ margin: 0px auto 40px 0px;
+ /* 3 icons 64px wide each seperated by a 64px gap */
+ width: 384px;
+ display: grid;
+ grid-row-gap: 15px;
+ grid-column-gap: 64px;
+ grid-template-columns: 1fr 1fr 1fr;
+}
+
+#onionErrorDiagramContainer > div {
+ margin: auto;
+ position: relative; /* needed to allow overlay of the ok or error icon */
+}
+
+.onionErrorImage {
+ width: 64px;
+ height: 64px;
+ background-size: 64px 64px;
+ background-position: center;
+ background-repeat: no-repeat;
+ -moz-context-properties: fill;
+ fill: var(--in-content-icon-color);
+ opacity: 50%;
+}
+
+/* TODO: remove these --warning-color definitions after we
+ are esr92 based (tor-browser#40640 */
+.onionErrorImage {
+ --warning-color: #ffa436;
+}
+
+@media (-moz-toolbar-prefers-color-scheme: dark) {
+ .onionErrorImage {
+ --warning-color: #ffbd4f;
+ }
+}
+
+@media (prefers-contrast) {
+ .onionErrorImage {
+ --warning-color: var(--in-content-page-color);
+ }
+}
+
+.onionErrorImage[status] {
+ opacity: 100%;
+}
+
+#onionErrorBrowserImage {
+ background-image: url("browser.svg");
+}
+
+#onionErrorNetworkImage {
+ background-image: url("network.svg");
+}
+
+#onionErrorOnionSiteImage {
+ background-image: url("onionsite.svg");
+}
+
+/* rules to support overlay of the ok or error icon */
+.onionErrorImage[status]::after {
+ content: " ";
+ position: absolute;
+ left: -8px;
+ top: calc((64px - 24px) / 2);
+ width: 24px;
+ height: 24px;
+ -moz-context-properties: fill;
+ fill: var(--in-content-page-background);
+
+ background-repeat: no-repeat;
+ background-position: center;
+ border: 3px solid var(--in-content-page-background);
+ border-radius: 50%;
+}
+
+.onionErrorImage[status="ok"]::after {
+ background-color: var(--in-content-icon-color);
+ background-image: url("chrome://global/skin/icons/check.svg");
+}
+
+.onionErrorImage[status="error"]::after {
+ background-color: var(--warning-color);
+ background-image: url("chrome://global/skin/icons/close.svg");
+}
diff --git a/browser/components/onionservices/content/netError/onionNetError.js b/browser/components/onionservices/content/netError/onionNetError.js
new file mode 100644
index 000000000000..745c58ec6124
--- /dev/null
+++ b/browser/components/onionservices/content/netError/onionNetError.js
@@ -0,0 +1,243 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+/* eslint-env mozilla/frame-script */
+
+var OnionServicesAboutNetError = {
+ _selector: {
+ textContainer: "div#text-container",
+ header: ".title-text",
+ longDesc: "#errorLongDesc",
+ learnMoreContainer: "#learnMoreContainer",
+ learnMoreLink: "#learnMoreLink",
+ contentContainer: "#errorLongContent",
+ tryAgainButtonContainer: "#netErrorButtonContainer",
+ },
+ _status: {
+ ok: "ok",
+ error: "error",
+ },
+
+ _diagramInfoMap: undefined,
+
+ // Public functions (called from outside this file).
+ //
+ // This initPage() function may need to be updated if the structure of
+ // browser/base/content/aboutNetError.xhtml changes. Specifically, it
+ // references the following elements:
+ // query string parameter e
+ // class title-text
+ // id errorLongDesc
+ // id learnMoreContainer
+ // id learnMoreLink
+ // id errorLongContent
+ initPage(aDoc) {
+ const searchParams = new URLSearchParams(aDoc.documentURI.split("?")[1]);
+ const err = searchParams.get("e");
+
+ const errPrefix = "onionServices.";
+ const errName = err.substring(errPrefix.length);
+
+ this._strings = RPMGetTorStrings();
+
+ const stringsObj = this._strings[errName];
+ if (!stringsObj) {
+ return;
+ }
+
+ this._insertStylesheet(aDoc);
+
+ const pageTitle = stringsObj.pageTitle;
+ const header = stringsObj.header;
+ const longDescription = stringsObj.longDescription; // optional
+ const learnMoreURL = stringsObj.learnMoreURL;
+
+ if (pageTitle) {
+ aDoc.title = pageTitle;
+ }
+
+ if (header) {
+ const headerElem = aDoc.querySelector(this._selector.header);
+ if (headerElem) {
+ headerElem.textContent = header;
+ }
+ }
+
+ const ld = aDoc.querySelector(this._selector.longDesc);
+ if (ld) {
+ if (longDescription) {
+ const hexErr = this._hexErrorFromName(errName);
+ ld.textContent = longDescription.replace("%S", hexErr);
+ } else {
+ // This onion service error does not have a long description. Since
+ // it is set to a generic error string by the code in
+ // browser/base/content/aboutNetError.js, hide it here.
+ ld.style.display = "none";
+ }
+ }
+
+ if (learnMoreURL) {
+ const lmContainer = aDoc.querySelector(this._selector.learnMoreContainer);
+ if (lmContainer) {
+ lmContainer.style.display = "block";
+ }
+ const lmLink = lmContainer.querySelector(this._selector.learnMoreLink);
+ if (lmLink) {
+ lmLink.setAttribute("href", learnMoreURL);
+ }
+ }
+
+ // Remove the "Try Again" button if the user made a typo in the .onion
+ // address since it is not useful in that case.
+ if (errName === "badAddress") {
+ const tryAgainButton = aDoc.querySelector(
+ this._selector.tryAgainButtonContainer
+ );
+ if (tryAgainButton) {
+ tryAgainButton.style.display = "none";
+ }
+ }
+
+ this._insertDiagram(aDoc, errName);
+ }, // initPage()
+
+ _insertStylesheet(aDoc) {
+ const url =
+ "chrome://browser/content/onionservices/netError/onionNetError.css";
+ let linkElem = aDoc.createElement("link");
+ linkElem.rel = "stylesheet";
+ linkElem.href = url;
+ linkElem.type = "text/css";
+ aDoc.head.appendChild(linkElem);
+ },
+
+ _insertDiagram(aDoc, aErrorName) {
+ // The onion error diagram consists of a grid of div elements.
+ // The first row contains three images (Browser, Network, Onionsite) and
+ // the second row contains labels for the images that are in the first row.
+ // The _diagramInfoMap describes for each type of onion service error
+ // whether a small ok or error status icon is overlaid on top of the main
+ // Browser/Network/Onionsite images.
+ if (!this._diagramInfoMap) {
+ this._diagramInfoMap = new Map();
+ this._diagramInfoMap.set("descNotFound", {
+ browser: this._status.ok,
+ network: this._status.ok,
+ onionSite: this._status.error,
+ });
+ this._diagramInfoMap.set("descInvalid", {
+ browser: this._status.ok,
+ network: this._status.error,
+ });
+ this._diagramInfoMap.set("introFailed", {
+ browser: this._status.ok,
+ network: this._status.error,
+ });
+ this._diagramInfoMap.set("rendezvousFailed", {
+ browser: this._status.ok,
+ network: this._status.error,
+ });
+ this._diagramInfoMap.set("clientAuthMissing", {
+ browser: this._status.error,
+ });
+ this._diagramInfoMap.set("clientAuthIncorrect", {
+ browser: this._status.error,
+ });
+ this._diagramInfoMap.set("badAddress", {
+ browser: this._status.error,
+ });
+ this._diagramInfoMap.set("introTimedOut", {
+ browser: this._status.ok,
+ network: this._status.error,
+ });
+ }
+
+ const diagramInfo = this._diagramInfoMap.get(aErrorName);
+
+ const container = this._createDiv(aDoc, "onionErrorDiagramContainer");
+ const imageClass = "onionErrorImage";
+
+ const browserImage = this._createDiv(
+ aDoc,
+ "onionErrorBrowserImage",
+ imageClass,
+ container
+ );
+ if (diagramInfo && diagramInfo.browser) {
+ browserImage.setAttribute("status", diagramInfo.browser);
+ }
+
+ const networkImage = this._createDiv(
+ aDoc,
+ "onionErrorNetworkImage",
+ imageClass,
+ container
+ );
+ if (diagramInfo && diagramInfo.network) {
+ networkImage.setAttribute("status", diagramInfo.network);
+ }
+
+ const onionSiteImage = this._createDiv(
+ aDoc,
+ "onionErrorOnionSiteImage",
+ imageClass,
+ container
+ );
+ if (diagramInfo && diagramInfo.onionSite) {
+ onionSiteImage.setAttribute("status", diagramInfo.onionSite);
+ }
+
+ let labelDiv = this._createDiv(aDoc, undefined, undefined, container);
+ labelDiv.textContent = this._strings.errorPage.browser;
+ labelDiv = this._createDiv(aDoc, undefined, undefined, container);
+ labelDiv.textContent = this._strings.errorPage.network;
+ labelDiv = this._createDiv(aDoc, undefined, undefined, container);
+ labelDiv.textContent = this._strings.errorPage.onionSite;
+
+ const textContainer = aDoc.querySelector(
+ this._selector.textContainer
+ );
+ textContainer?.insertBefore(container, textContainer.firstChild);
+ }, // _insertDiagram()
+
+ _createDiv(aDoc, aID, aClass, aParentElem) {
+ const div = aDoc.createElement("div");
+ if (aID) {
+ div.id = aID;
+ }
+ if (aClass) {
+ div.setAttribute("class", aClass);
+ }
+ if (aParentElem) {
+ aParentElem.appendChild(div);
+ }
+
+ return div;
+ },
+
+ _hexErrorFromName(aErrorName) {
+ // We do not have access to the original Tor SOCKS error code here, so
+ // perform a reverse mapping from the error name.
+ switch (aErrorName) {
+ case "descNotFound":
+ return "0xF0";
+ case "descInvalid":
+ return "0xF1";
+ case "introFailed":
+ return "0xF2";
+ case "rendezvousFailed":
+ return "0xF3";
+ case "clientAuthMissing":
+ return "0xF4";
+ case "clientAuthIncorrect":
+ return "0xF5";
+ case "badAddress":
+ return "0xF6";
+ case "introTimedOut":
+ return "0xF7";
+ }
+
+ return "";
+ },
+};
diff --git a/browser/components/onionservices/content/netError/onionsite.svg b/browser/components/onionservices/content/netError/onionsite.svg
new file mode 100644
index 000000000000..c1b2d7382dc9
--- /dev/null
+++ b/browser/components/onionservices/content/netError/onionsite.svg
@@ -0,0 +1,8 @@
+<svg fill="none" height="60" viewBox="0 0 60 60" width="60" xmlns="http://www.w3.org/2000/svg">
+ <g fill="context-fill" fill-opacity="context-fill-opacity">
+ <path clip-rule="evenodd" d="m11.25 6h37.5c1.9891 0 3.8968.79018 5.3033 2.1967s2.1967 3.3142 2.1967 5.3033v33.75c0 1.9891-.7902 3.8968-2.1967 5.3033s-3.3142 2.1967-5.3033 2.1967h-37.5c-1.98912 0-3.89678-.7902-5.3033-2.1967s-2.1967-3.3142-2.1967-5.3033v-33.75c0-1.9891.79018-3.89678 2.1967-5.3033s3.31418-2.1967 5.3033-2.1967zm-.5625 4.6875h38.625l2.25 2.25v34.875l-2.25 2.25h-38.625l-2.25-2.25v-34.875z" fill-rule="evenodd"/>
+ <path d="m15.9606 22c-.52 0-1.0187-.2107-1.3863-.5858-.3677-.3751-.5743-.8838-.5743-1.4142s.2066-1.0391.5743-1.4142c.3676-.3751.8663-.5858 1.3863-.5858h14.0788c.52 0 1.0187.2107 1.3863.5858.3677.3751.5743.8838.5743 1.4142s-.2066 1.0391-.5743 1.4142c-.3676.3751-.8663.5858-1.3863.5858z"/>
+ <path d="m44.0709 32h-28.1418c-.5116 0-1.0023-.2107-1.3641-.5858s-.565-.8838-.565-1.4142.2032-1.0391.565-1.4142.8525-.5858 1.3641-.5858h28.1418c.5116 0 1.0023.2107 1.3641.5858s.565.8838.565 1.4142-.2032 1.0391-.565 1.4142-.8525.5858-1.3641.5858z"/>
+ <path d="m44.0709 42h-28.1418c-.5116 0-1.0023-.2107-1.3641-.5858s-.565-.8838-.565-1.4142.2032-1.0391.565-1.4142.8525-.5858 1.3641-.5858h28.1418c.5116 0 1.0023.2107 1.3641.5858s.565.8838.565 1.4142-.2032 1.0391-.565 1.4142-.8525.5858-1.3641.5858z"/>
+ </g>
+</svg>
diff --git a/browser/components/onionservices/content/onionservices.css b/browser/components/onionservices/content/onionservices.css
new file mode 100644
index 000000000000..e2621ec8266d
--- /dev/null
+++ b/browser/components/onionservices/content/onionservices.css
@@ -0,0 +1,69 @@
+/* Copyright (c) 2020, The Tor Project, Inc. */
+
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+html|*#tor-clientauth-notification-onionname {
+ font-weight: bold;
+}
+
+html|*#tor-clientauth-notification-key {
+ box-sizing: border-box;
+ width: 100%;
+ margin-top: 15px;
+ padding: 6px;
+}
+
+/* Start of rules adapted from
+ * browser/components/newtab/css/activity-stream-mac.css (linux and windows
+ * use the same rules).
+ */
+html|*#tor-clientauth-notification-key.invalid {
+ border: 1px solid #D70022;
+ box-shadow: 0 0 0 1px #D70022, 0 0 0 4px rgba(215, 0, 34, 0.3);
+}
+
+html|*#tor-clientauth-warning {
+ display: inline-block;
+ animation: fade-up-tt 450ms;
+ background: #D70022;
+ border-radius: 2px;
+ color: #FFF;
+ inset-inline-start: 3px;
+ padding: 5px 12px;
+ position: relative;
+ top: 6px;
+ z-index: 1;
+}
+
+html|*#tor-clientauth-warning[hidden] {
+ display: none;
+}
+
+html|*#tor-clientauth-warning::before {
+ background: #D70022;
+ bottom: -8px;
+ content: '.';
+ height: 16px;
+ inset-inline-start: 12px;
+ position: absolute;
+ text-indent: -999px;
+ top: -7px;
+ transform: rotate(45deg);
+ white-space: nowrap;
+ width: 16px;
+ z-index: -1;
+}
+
+@keyframes fade-up-tt {
+ 0% {
+ opacity: 0;
+ transform: translateY(15px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+/* End of rules adapted from
+ * browser/components/newtab/css/activity-stream-mac.css
+ */
diff --git a/browser/components/onionservices/content/savedKeysDialog.js b/browser/components/onionservices/content/savedKeysDialog.js
new file mode 100644
index 000000000000..b1376bbabe85
--- /dev/null
+++ b/browser/components/onionservices/content/savedKeysDialog.js
@@ -0,0 +1,259 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "TorStrings",
+ "resource:///modules/TorStrings.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "controller",
+ "resource://torbutton/modules/tor-control-port.js"
+);
+
+var gOnionServicesSavedKeysDialog = {
+ selector: {
+ dialog: "#onionservices-savedkeys-dialog",
+ intro: "#onionservices-savedkeys-intro",
+ tree: "#onionservices-savedkeys-tree",
+ onionSiteCol: "#onionservices-savedkeys-siteCol",
+ onionKeyCol: "#onionservices-savedkeys-keyCol",
+ errorIcon: "#onionservices-savedkeys-errorIcon",
+ errorMessage: "#onionservices-savedkeys-errorMessage",
+ removeButton: "#onionservices-savedkeys-remove",
+ removeAllButton: "#onionservices-savedkeys-removeall",
+ },
+
+ _tree: undefined,
+ _isBusy: false, // true when loading data, deleting a key, etc.
+
+ // Public functions (called from outside this file).
+ async deleteSelectedKeys() {
+ this._setBusyState(true);
+
+ const indexesToDelete = [];
+ const count = this._tree.view.selection.getRangeCount();
+ for (let i = 0; i < count; ++i) {
+ const minObj = {};
+ const maxObj = {};
+ this._tree.view.selection.getRangeAt(i, minObj, maxObj);
+ for (let idx = minObj.value; idx <= maxObj.value; ++idx) {
+ indexesToDelete.push(idx);
+ }
+ }
+
+ if (indexesToDelete.length > 0) {
+ const controllerFailureMsg =
+ TorStrings.onionServices.authPreferences.failedToRemoveKey;
+ try {
+ const torController = controller(aError => {
+ this._showError(controllerFailureMsg);
+ });
+
+ // Remove in reverse index order to avoid issues caused by index changes.
+ for (let i = indexesToDelete.length - 1; i >= 0; --i) {
+ await this._deleteOneKey(torController, indexesToDelete[i]);
+ }
+ } catch (e) {
+ if (e.torMessage) {
+ this._showError(e.torMessage);
+ } else {
+ this._showError(controllerFailureMsg);
+ }
+ }
+ }
+
+ this._setBusyState(false);
+ },
+
+ async deleteAllKeys() {
+ this._tree.view.selection.selectAll();
+ await this.deleteSelectedKeys();
+ },
+
+ updateButtonsState() {
+ const haveSelection = this._tree.view.selection.getRangeCount() > 0;
+ const dialog = document.querySelector(this.selector.dialog);
+ const removeSelectedBtn = dialog.querySelector(this.selector.removeButton);
+ removeSelectedBtn.disabled = this._isBusy || !haveSelection;
+ const removeAllBtn = dialog.querySelector(this.selector.removeAllButton);
+ removeAllBtn.disabled = this._isBusy || this.rowCount === 0;
+ },
+
+ // Private functions.
+ _onLoad() {
+ document.mozSubdialogReady = this._init();
+ },
+
+ async _init() {
+ await this._populateXUL();
+
+ window.addEventListener("keypress", this._onWindowKeyPress.bind(this));
+
+ // We don't use await here because we want _loadSavedKeys() to run
+ // in the background and not block loading of this dialog.
+ this._loadSavedKeys();
+ },
+
+ async _populateXUL() {
+ const dialog = document.querySelector(this.selector.dialog);
+ const authPrefStrings = TorStrings.onionServices.authPreferences;
+ dialog.setAttribute("title", authPrefStrings.dialogTitle);
+
+ let elem = dialog.querySelector(this.selector.intro);
+ elem.textContent = authPrefStrings.dialogIntro;
+
+ elem = dialog.querySelector(this.selector.onionSiteCol);
+ elem.setAttribute("label", authPrefStrings.onionSite);
+
+ elem = dialog.querySelector(this.selector.onionKeyCol);
+ elem.setAttribute("label", authPrefStrings.onionKey);
+
+ elem = dialog.querySelector(this.selector.removeButton);
+ elem.setAttribute("label", authPrefStrings.remove);
+
+ elem = dialog.querySelector(this.selector.removeAllButton);
+ elem.setAttribute("label", authPrefStrings.removeAll);
+
+ this._tree = dialog.querySelector(this.selector.tree);
+ },
+
+ async _loadSavedKeys() {
+ const controllerFailureMsg =
+ TorStrings.onionServices.authPreferences.failedToGetKeys;
+ this._setBusyState(true);
+
+ try {
+ this._tree.view = this;
+
+ const torController = controller(aError => {
+ this._showError(controllerFailureMsg);
+ });
+
+ const keyInfoList = await torController.onionAuthViewKeys();
+ if (keyInfoList) {
+ // Filter out temporary keys.
+ this._keyInfoList = keyInfoList.filter(aKeyInfo => {
+ if (!aKeyInfo.Flags) {
+ return false;
+ }
+
+ const flags = aKeyInfo.Flags.split(",");
+ return flags.includes("Permanent");
+ });
+
+ // Sort by the .onion address.
+ this._keyInfoList.sort((aObj1, aObj2) => {
+ const hsAddr1 = aObj1.hsAddress.toLowerCase();
+ const hsAddr2 = aObj2.hsAddress.toLowerCase();
+ if (hsAddr1 < hsAddr2) {
+ return -1;
+ }
+ return hsAddr1 > hsAddr2 ? 1 : 0;
+ });
+ }
+
+ // Render the tree content.
+ this._tree.rowCountChanged(0, this.rowCount);
+ } catch (e) {
+ if (e.torMessage) {
+ this._showError(e.torMessage);
+ } else {
+ this._showError(controllerFailureMsg);
+ }
+ }
+
+ this._setBusyState(false);
+ },
+
+ // This method may throw; callers should catch errors.
+ async _deleteOneKey(aTorController, aIndex) {
+ const keyInfoObj = this._keyInfoList[aIndex];
+ await aTorController.onionAuthRemove(keyInfoObj.hsAddress);
+ this._tree.view.selection.clearRange(aIndex, aIndex);
+ this._keyInfoList.splice(aIndex, 1);
+ this._tree.rowCountChanged(aIndex + 1, -1);
+ },
+
+ _setBusyState(aIsBusy) {
+ this._isBusy = aIsBusy;
+ this.updateButtonsState();
+ },
+
+ _onWindowKeyPress(event) {
+ if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
+ window.close();
+ } else if (event.keyCode === KeyEvent.DOM_VK_DELETE) {
+ this.deleteSelectedKeys();
+ }
+ },
+
+ _showError(aMessage) {
+ const dialog = document.querySelector(this.selector.dialog);
+ const errorIcon = dialog.querySelector(this.selector.errorIcon);
+ errorIcon.style.visibility = aMessage ? "visible" : "hidden";
+ const errorDesc = dialog.querySelector(this.selector.errorMessage);
+ errorDesc.textContent = aMessage ? aMessage : "";
+ },
+
+ // XUL tree widget view implementation.
+ get rowCount() {
+ return this._keyInfoList ? this._keyInfoList.length : 0;
+ },
+
+ getCellText(aRow, aCol) {
+ let val = "";
+ if (this._keyInfoList && aRow < this._keyInfoList.length) {
+ const keyInfo = this._keyInfoList[aRow];
+ if (aCol.id.endsWith("-siteCol")) {
+ val = keyInfo.hsAddress;
+ } else if (aCol.id.endsWith("-keyCol")) {
+ val = keyInfo.typeAndKey;
+ // Omit keyType because it is always "x25519".
+ const idx = val.indexOf(":");
+ if (idx > 0) {
+ val = val.substring(idx + 1);
+ }
+ }
+ }
+
+ return val;
+ },
+
+ isSeparator(index) {
+ return false;
+ },
+
+ isSorted() {
+ return false;
+ },
+
+ isContainer(index) {
+ return false;
+ },
+
+ setTree(tree) {},
+
+ getImageSrc(row, column) {},
+
+ getCellValue(row, column) {},
+
+ cycleHeader(column) {},
+
+ getRowProperties(row) {
+ return "";
+ },
+
+ getColumnProperties(column) {
+ return "";
+ },
+
+ getCellProperties(row, column) {
+ return "";
+ },
+};
+
+window.addEventListener("load", () => gOnionServicesSavedKeysDialog._onLoad());
diff --git a/browser/components/onionservices/content/savedKeysDialog.xhtml b/browser/components/onionservices/content/savedKeysDialog.xhtml
new file mode 100644
index 000000000000..3db9bb05ea82
--- /dev/null
+++ b/browser/components/onionservices/content/savedKeysDialog.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!-- Copyright (c) 2020, The Tor Project, Inc. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/onionservices/authPreferences.css" type="text/css"?>
+
+<window id="onionservices-savedkeys-dialog"
+ windowtype="OnionServices:SavedKeys"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ style="width: 45em;">
+
+ <script src="chrome://browser/content/onionservices/savedKeysDialog.js"/>
+
+ <vbox id="onionservices-savedkeys" class="contentPane" flex="1">
+ <label id="onionservices-savedkeys-intro"
+ control="onionservices-savedkeys-tree"/>
+ <separator class="thin"/>
+ <tree id="onionservices-savedkeys-tree" flex="1" hidecolumnpicker="true"
+ width="750"
+ style="height: 20em;"
+ onselect="gOnionServicesSavedKeysDialog.updateButtonsState();">
+ <treecols>
+ <treecol id="onionservices-savedkeys-siteCol" flex="1" persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="onionservices-savedkeys-keyCol" flex="1" persist="width"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <hbox id="onionservices-savedkeys-errorContainer" align="baseline" flex="1">
+ <image id="onionservices-savedkeys-errorIcon"/>
+ <description id="onionservices-savedkeys-errorMessage" flex="1"/>
+ </hbox>
+ <separator class="thin"/>
+ <hbox id="onionservices-savedkeys-buttons">
+ <button id="onionservices-savedkeys-remove" disabled="true"
+ oncommand="gOnionServicesSavedKeysDialog.deleteSelectedKeys();"/>
+ <button id="onionservices-savedkeys-removeall"
+ oncommand="gOnionServicesSavedKeysDialog.deleteAllKeys();"/>
+ </hbox>
+ </vbox>
+</window>
diff --git a/browser/components/onionservices/jar.mn b/browser/components/onionservices/jar.mn
new file mode 100644
index 000000000000..9d6ce88d1841
--- /dev/null
+++ b/browser/components/onionservices/jar.mn
@@ -0,0 +1,9 @@
+browser.jar:
+ content/browser/onionservices/authPreferences.css (content/authPreferences.css)
+ content/browser/onionservices/authPreferences.js (content/authPreferences.js)
+ content/browser/onionservices/authPrompt.js (content/authPrompt.js)
+ content/browser/onionservices/authUtil.jsm (content/authUtil.jsm)
+ content/browser/onionservices/netError/ (content/netError/*)
+ content/browser/onionservices/onionservices.css (content/onionservices.css)
+ content/browser/onionservices/savedKeysDialog.js (content/savedKeysDialog.js)
+ content/browser/onionservices/savedKeysDialog.xhtml (content/savedKeysDialog.xhtml)
diff --git a/browser/components/onionservices/moz.build b/browser/components/onionservices/moz.build
new file mode 100644
index 000000000000..2661ad7cb9f3
--- /dev/null
+++ b/browser/components/onionservices/moz.build
@@ -0,0 +1 @@
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
index 0139abf95cbd..07ab5cc7b626 100644
--- a/browser/components/preferences/preferences.xhtml
+++ b/browser/components/preferences/preferences.xhtml
@@ -12,6 +12,7 @@
<?xml-stylesheet href="chrome://browser/skin/preferences/search.css"?>
<?xml-stylesheet href="chrome://browser/skin/preferences/containers.css"?>
<?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?>
+<?xml-stylesheet href="chrome://browser/content/onionservices/authPreferences.css"?>
<?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?>
<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml
index 3e0c0c8dc74c..7d3bc3a663ed 100644
--- a/browser/components/preferences/privacy.inc.xhtml
+++ b/browser/components/preferences/privacy.inc.xhtml
@@ -505,6 +505,8 @@
<label id="fips-desc" hidden="true" data-l10n-id="forms-master-pw-fips-desc"></label>
</groupbox>
+#include ../onionservices/content/authPreferences.inc.xhtml
+
<!-- The form autofill section is inserted in to this box
after the form autofill extension has initialized. -->
<groupbox id="formAutofillGroupBox"
diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js
index bce7bb7e8a9c..932d4291e486 100644
--- a/browser/components/preferences/privacy.js
+++ b/browser/components/preferences/privacy.js
@@ -80,6 +80,12 @@ XPCOMUtils.defineLazyGetter(this, "AlertsServiceDND", function() {
}
});
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["OnionServicesAuthPreferences"],
+ "chrome://browser/content/onionservices/authPreferences.js"
+);
+
// TODO: module import via ChromeUtils.defineModuleGetter
XPCOMUtils.defineLazyScriptGetter(
this,
@@ -522,6 +528,7 @@ var gPrivacyPane = {
this.trackingProtectionReadPrefs();
this.networkCookieBehaviorReadPrefs();
this._initTrackingProtectionExtensionControl();
+ OnionServicesAuthPreferences.init();
this._initSecurityLevel();
Services.telemetry.setEventRecordingEnabled("pwmgr", true);
diff --git a/browser/themes/shared/notification-icons.inc.css b/browser/themes/shared/notification-icons.inc.css
index 658fa7f7430a..67dd640baf16 100644
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -137,6 +137,9 @@
list-style-image: url(chrome://browser/skin/notification-icons/persistent-storage.svg);
}
+/* Reuse Firefox's login (key) icon for the Tor onion services auth. prompt */
+.popup-notification-icon[popupid="tor-clientauth"],
+.tor-clientauth-icon,
.popup-notification-icon[popupid="password"],
.login-icon {
list-style-image: url(chrome://browser/skin/login.svg);
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index 68d5621b41e7..a5d837c36158 100644
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -3587,6 +3587,7 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
}
} else {
// Errors requiring simple formatting
+ bool isOnionAuthError = false;
switch (aError) {
case NS_ERROR_MALFORMED_URI:
// URI is malformed
@@ -3669,10 +3670,44 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
// HTTP/2 or HTTP/3 stack detected a protocol error
error = "networkProtocolError";
break;
-
+ case NS_ERROR_TOR_ONION_SVC_NOT_FOUND:
+ error = "onionServices.descNotFound";
+ break;
+ case NS_ERROR_TOR_ONION_SVC_IS_INVALID:
+ error = "onionServices.descInvalid";
+ break;
+ case NS_ERROR_TOR_ONION_SVC_INTRO_FAILED:
+ error = "onionServices.introFailed";
+ break;
+ case NS_ERROR_TOR_ONION_SVC_REND_FAILED:
+ error = "onionServices.rendezvousFailed";
+ break;
+ case NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH:
+ error = "onionServices.clientAuthMissing";
+ isOnionAuthError = true;
+ break;
+ case NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH:
+ error = "onionServices.clientAuthIncorrect";
+ isOnionAuthError = true;
+ break;
+ case NS_ERROR_TOR_ONION_SVC_BAD_ADDRESS:
+ error = "onionServices.badAddress";
+ break;
+ case NS_ERROR_TOR_ONION_SVC_INTRO_TIMEDOUT:
+ error = "onionServices.introTimedOut";
+ break;
default:
break;
}
+
+ // The presence of aFailedChannel indicates that we arrived here due to a
+ // failed connection attempt. Note that we will arrive here a second time
+ // if the user cancels the Tor client auth prompt, but in that case we
+ // will not have a failed channel and therefore we will not prompt again.
+ if (isOnionAuthError && aFailedChannel) {
+ // Display about:blank while the Tor client auth prompt is open.
+ errorPage.AssignLiteral("blank");
+ }
}
// If the HTTPS-Only Mode upgraded this request and the upgrade might have
@@ -3755,6 +3790,20 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
nsAutoString str;
rv =
stringBundle->FormatStringFromName(errorDescriptionID, formatStrs, str);
+ if (NS_FAILED(rv)) {
+ // As a fallback, check torbutton.properties for the error string.
+ const char bundleURL[] = "chrome://torbutton/locale/torbutton.properties";
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::services::GetStringBundleService();
+ if (stringBundleService) {
+ nsCOMPtr<nsIStringBundle> tbStringBundle;
+ if (NS_SUCCEEDED(stringBundleService->CreateBundle(
+ bundleURL, getter_AddRefs(tbStringBundle)))) {
+ rv = tbStringBundle->FormatStringFromName(errorDescriptionID,
+ formatStrs, str);
+ }
+ }
+ }
NS_ENSURE_SUCCESS(rv, rv);
messageStr.Assign(str);
}
@@ -6173,6 +6222,7 @@ nsresult nsDocShell::FilterStatusForErrorPage(
aStatus == NS_ERROR_FILE_ACCESS_DENIED ||
aStatus == NS_ERROR_CORRUPTED_CONTENT ||
aStatus == NS_ERROR_INVALID_CONTENT_ENCODING ||
+ NS_ERROR_GET_MODULE(aStatus) == NS_ERROR_MODULE_TOR ||
NS_ERROR_GET_MODULE(aStatus) == NS_ERROR_MODULE_SECURITY) {
// Errors to be shown for any frame
return aStatus;
@@ -7956,6 +8006,35 @@ nsresult nsDocShell::CreateContentViewer(const nsACString& aContentType,
FireOnLocationChange(this, aRequest, mCurrentURI, locationFlags);
}
+ // Arrange to show a Tor onion service client authentication prompt if
+ // appropriate.
+ if ((mLoadType == LOAD_ERROR_PAGE) && failedChannel) {
+ nsresult status = NS_OK;
+ if (NS_SUCCEEDED(failedChannel->GetStatus(&status)) &&
+ ((status == NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH) ||
+ (status == NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH))) {
+ nsAutoCString onionHost;
+ failedURI->GetHost(onionHost);
+ const char* topic = (status == NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH)
+ ? "tor-onion-services-clientauth-missing"
+ : "tor-onion-services-clientauth-incorrect";
+ if (XRE_IsContentProcess()) {
+ nsCOMPtr<nsIBrowserChild> browserChild = GetBrowserChild();
+ if (browserChild) {
+ static_cast<BrowserChild*>(browserChild.get())
+ ->SendShowOnionServicesAuthPrompt(onionHost, nsCString(topic));
+ }
+ } else {
+ nsCOMPtr<nsPIDOMWindowOuter> browserWin = GetWindow();
+ nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
+ if (browserWin && obsSvc) {
+ obsSvc->NotifyObservers(browserWin, topic,
+ NS_ConvertUTF8toUTF16(onionHost).get());
+ }
+ }
+ }
+ }
+
return NS_OK;
}
diff --git a/dom/ipc/BrowserParent.cpp b/dom/ipc/BrowserParent.cpp
index 05d77937f986..4145111ae849 100644
--- a/dom/ipc/BrowserParent.cpp
+++ b/dom/ipc/BrowserParent.cpp
@@ -3810,6 +3810,27 @@ mozilla::ipc::IPCResult BrowserParent::RecvShowCanvasPermissionPrompt(
return IPC_OK();
}
+mozilla::ipc::IPCResult BrowserParent::RecvShowOnionServicesAuthPrompt(
+ const nsCString& aOnionName, const nsCString& aTopic) {
+ nsCOMPtr<nsIBrowser> browser =
+ mFrameElement ? mFrameElement->AsBrowser() : nullptr;
+ if (!browser) {
+ // If the tab is being closed, the browser may not be available.
+ // In this case we can ignore the request.
+ return IPC_OK();
+ }
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (!os) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ nsresult rv = os->NotifyObservers(browser, aTopic.get(),
+ NS_ConvertUTF8toUTF16(aOnionName).get());
+ if (NS_FAILED(rv)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
mozilla::ipc::IPCResult BrowserParent::RecvVisitURI(nsIURI* aURI,
nsIURI* aLastVisitedURI,
const uint32_t& aFlags) {
diff --git a/dom/ipc/BrowserParent.h b/dom/ipc/BrowserParent.h
index 80e4d055e26c..a36ebfc8ca05 100644
--- a/dom/ipc/BrowserParent.h
+++ b/dom/ipc/BrowserParent.h
@@ -736,6 +736,9 @@ class BrowserParent final : public PBrowserParent,
mozilla::ipc::IPCResult RecvShowCanvasPermissionPrompt(
const nsCString& aOrigin, const bool& aHideDoorHanger);
+ mozilla::ipc::IPCResult RecvShowOnionServicesAuthPrompt(
+ const nsCString& aOnionName, const nsCString& aTopic);
+
mozilla::ipc::IPCResult RecvSetSystemFont(const nsCString& aFontName);
mozilla::ipc::IPCResult RecvGetSystemFont(nsCString* aFontName);
diff --git a/dom/ipc/PBrowser.ipdl b/dom/ipc/PBrowser.ipdl
index 9750219fa46a..5706c7f5da00 100644
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -577,6 +577,15 @@ parent:
async RequestPointerCapture(uint32_t aPointerId) returns (bool aSuccess);
async ReleasePointerCapture(uint32_t aPointerId);
+ /**
+ * This function is used to notify the parent that it should display a
+ * onion services client authentication prompt.
+ *
+ * @param aOnionHost The hostname of the .onion that needs authentication.
+ * @param aTopic The reason for the prompt.
+ */
+ async ShowOnionServicesAuthPrompt(nsCString aOnionHost, nsCString aTopic);
+
child:
async NativeSynthesisResponse(uint64_t aObserverId, nsCString aResponse);
async UpdateEpoch(uint32_t aEpoch);
diff --git a/js/xpconnect/src/xpc.msg b/js/xpconnect/src/xpc.msg
index c7fbdd23f378..07f529957bd0 100644
--- a/js/xpconnect/src/xpc.msg
+++ b/js/xpconnect/src/xpc.msg
@@ -248,5 +248,15 @@ XPC_MSG_DEF(NS_ERROR_FINGERPRINTING_URI , "The URI is fingerprinti
XPC_MSG_DEF(NS_ERROR_CRYPTOMINING_URI , "The URI is cryptomining")
XPC_MSG_DEF(NS_ERROR_SOCIALTRACKING_URI , "The URI is social tracking")
+/* Codes related to Tor */
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_NOT_FOUND , "Tor onion service descriptor cannot be found")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_IS_INVALID , "Tor onion service descriptor is invalid")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_INTRO_FAILED , "Tor onion service introduction failed")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_REND_FAILED , "Tor onion service rendezvous failed")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH, "Tor onion service missing client authorization")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH , "Tor onion service wrong client authorization")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_BAD_ADDRESS , "Tor onion service bad address")
+XPC_MSG_DEF(NS_ERROR_TOR_ONION_SVC_INTRO_TIMEDOUT , "Tor onion service introduction timed out")
+
/* Profile manager error codes */
XPC_MSG_DEF(NS_ERROR_DATABASE_CHANGED , "Flushing the profiles to disk would have overwritten changes made elsewhere.")
diff --git a/netwerk/base/nsSocketTransport2.cpp b/netwerk/base/nsSocketTransport2.cpp
index 8f44441e1fd0..99a6f3b60ac3 100644
--- a/netwerk/base/nsSocketTransport2.cpp
+++ b/netwerk/base/nsSocketTransport2.cpp
@@ -216,6 +216,12 @@ nsresult ErrorAccordingToNSPR(PRErrorCode errorCode) {
default:
if (psm::IsNSSErrorCode(errorCode)) {
rv = psm::GetXPCOMFromNSSError(errorCode);
+ } else {
+ // If we received a Tor extended error code via SOCKS, pass it through.
+ nsresult res = nsresult(errorCode);
+ if (NS_ERROR_GET_MODULE(res) == NS_ERROR_MODULE_TOR) {
+ rv = res;
+ }
}
break;
diff --git a/netwerk/socket/nsSOCKSIOLayer.cpp b/netwerk/socket/nsSOCKSIOLayer.cpp
index 119a3cbf4c51..f9fc29552ace 100644
--- a/netwerk/socket/nsSOCKSIOLayer.cpp
+++ b/netwerk/socket/nsSOCKSIOLayer.cpp
@@ -979,6 +979,55 @@ PRStatus nsSOCKSSocketInfo::ReadV5ConnectResponseTop() {
"08, Address type not supported."));
c = PR_BAD_ADDRESS_ERROR;
break;
+ case 0xF0: // Tor SOCKS5_HS_NOT_FOUND
+ LOGERROR(
+ ("socks5: connect failed: F0,"
+ " Tor onion service descriptor can not be found."));
+ c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_NOT_FOUND);
+ break;
+ case 0xF1: // Tor SOCKS5_HS_IS_INVALID
+ LOGERROR(
+ ("socks5: connect failed: F1,"
+ " Tor onion service descriptor is invalid."));
+ c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_IS_INVALID);
+ break;
+ case 0xF2: // Tor SOCKS5_HS_INTRO_FAILED
+ LOGERROR(
+ ("socks5: connect failed: F2,"
+ " Tor onion service introduction failed."));
+ c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_INTRO_FAILED);
+ break;
+ case 0xF3: // Tor SOCKS5_HS_REND_FAILED
+ LOGERROR(
+ ("socks5: connect failed: F3,"
+ " Tor onion service rendezvous failed."));
+ c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_REND_FAILED);
+ break;
+ case 0xF4: // Tor SOCKS5_HS_MISSING_CLIENT_AUTH
+ LOGERROR(
+ ("socks5: connect failed: F4,"
+ " Tor onion service missing client authorization."));
+ c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH);
+ break;
+ case 0xF5: // Tor SOCKS5_HS_BAD_CLIENT_AUTH
+ LOGERROR(
+ ("socks5: connect failed: F5,"
+ " Tor onion service wrong client authorization."));
+ c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH);
+ break;
+ case 0xF6: // Tor SOCKS5_HS_BAD_ADDRESS
+ LOGERROR(
+ ("socks5: connect failed: F6,"
+ " Tor onion service bad address."));
+ c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_BAD_ADDRESS);
+ break;
+ case 0xF7: // Tor SOCKS5_HS_INTRO_TIMEDOUT
+ LOGERROR(
+ ("socks5: connect failed: F7,"
+ " Tor onion service introduction timed out."));
+ c = static_cast<uint32_t>(NS_ERROR_TOR_ONION_SVC_INTRO_TIMEDOUT);
+ break;
+
default:
LOGERROR(("socks5: connect failed."));
break;
diff --git a/toolkit/modules/PopupNotifications.jsm b/toolkit/modules/PopupNotifications.jsm
index d6518723afab..9764cfd496c3 100644
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -410,6 +410,8 @@ PopupNotifications.prototype = {
* will be dismissed instead of removed after running the callback.
* - [optional] disabled (boolean): If this is true, the button
* will be disabled.
+ * - [optional] leaveOpen (boolean): If this is true, the notification
+ * will not be removed after running the callback.
* - [optional] disableHighlight (boolean): If this is true, the button
* will not apply the default highlight style.
* If null, the notification will have a default "OK" action button
@@ -1916,6 +1918,10 @@ PopupNotifications.prototype = {
this._dismiss();
return;
}
+
+ if (action.leaveOpen) {
+ return;
+ }
}
this._remove(notification);
diff --git a/toolkit/modules/RemotePageAccessManager.jsm b/toolkit/modules/RemotePageAccessManager.jsm
index e5111ff83782..c12e71ac4d42 100644
--- a/toolkit/modules/RemotePageAccessManager.jsm
+++ b/toolkit/modules/RemotePageAccessManager.jsm
@@ -102,6 +102,7 @@ let RemotePageAccessManager = {
RPMAddToHistogram: ["*"],
RPMGetInnerMostURI: ["*"],
RPMGetHttpResponseHeader: ["*"],
+ RPMGetTorStrings: ["*"],
},
"about:plugins": {
RPMSendQuery: ["RequestPlugins"],
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js
index 15c15615ad97..57458ba0bf5e 100644
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js
@@ -41,5 +41,6 @@ module.exports = {
RPMGetHttpResponseHeader: false,
RPMTryPingSecureWWWLink: false,
RPMOpenSecureWWWLink: false,
+ RPMGetTorStrings: false,
},
};
diff --git a/xpcom/base/ErrorList.py b/xpcom/base/ErrorList.py
index c22c27be8546..8fbcc7f663df 100755
--- a/xpcom/base/ErrorList.py
+++ b/xpcom/base/ErrorList.py
@@ -89,6 +89,7 @@ modules["ERRORRESULT"] = Mod(43)
# Win32 system error codes, which are not mapped to a specific other value,
# see Bug 1686041.
modules["WIN32"] = Mod(44)
+modules["TOR"] = Mod(45)
# NS_ERROR_MODULE_GENERAL should be used by modules that do not
# care if return code values overlap. Callers of methods that
@@ -1181,6 +1182,27 @@ with modules["ERRORRESULT"]:
errors["NS_ERROR_INTERNAL_ERRORRESULT_RANGEERROR"] = FAILURE(5)
+# =======================================================================
+# 45: Tor-specific error codes.
+# =======================================================================
+with modules["TOR"]:
+ # Tor onion service descriptor can not be found.
+ errors["NS_ERROR_TOR_ONION_SVC_NOT_FOUND"] = FAILURE(1)
+ # Tor onion service descriptor is invalid.
+ errors["NS_ERROR_TOR_ONION_SVC_IS_INVALID"] = FAILURE(2)
+ # Tor onion service introduction failed.
+ errors["NS_ERROR_TOR_ONION_SVC_INTRO_FAILED"] = FAILURE(3)
+ # Tor onion service rendezvous failed.
+ errors["NS_ERROR_TOR_ONION_SVC_REND_FAILED"] = FAILURE(4)
+ # Tor onion service missing client authorization.
+ errors["NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH"] = FAILURE(5)
+ # Tor onion service wrong client authorization.
+ errors["NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH"] = FAILURE(6)
+ # Tor onion service bad address.
+ errors["NS_ERROR_TOR_ONION_SVC_BAD_ADDRESS"] = FAILURE(7)
+ # Tor onion service introduction timed out.
+ errors["NS_ERROR_TOR_ONION_SVC_INTRO_TIMEDOUT"] = FAILURE(8)
+
# =======================================================================
# 51: NS_ERROR_MODULE_GENERAL
# =======================================================================
1
0

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 32658: Create a new MAR signing key
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit a16070d947df0036347bf3195932bb6430630258
Author: Georg Koppen <gk(a)torproject.org>
Date: Fri Jan 17 12:54:31 2020 +0000
Bug 32658: Create a new MAR signing key
It's time for our rotation again: Move the backup key in the front
position and add a new backup key.
Bug 33803: Move our primary nightly MAR signing key to tor-browser
Bug 33803: Add a secondary nightly MAR signing key
---
.../update/updater/nightly_aurora_level3_primary.der | Bin 1225 -> 1245 bytes
.../updater/nightly_aurora_level3_secondary.der | Bin 1225 -> 1245 bytes
toolkit/mozapps/update/updater/release_primary.der | Bin 1225 -> 1229 bytes
toolkit/mozapps/update/updater/release_secondary.der | Bin 1225 -> 1229 bytes
4 files changed, 0 insertions(+), 0 deletions(-)
diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der
index 44fd95dcff89..d579cf801e1a 100644
Binary files a/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der and b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der differ
diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der
index 90f8e6e82c63..7cbfa77d06e7 100644
Binary files a/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der and b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der differ
diff --git a/toolkit/mozapps/update/updater/release_primary.der b/toolkit/mozapps/update/updater/release_primary.der
index 1d94f88ad73b..0103a171de88 100644
Binary files a/toolkit/mozapps/update/updater/release_primary.der and b/toolkit/mozapps/update/updater/release_primary.der differ
diff --git a/toolkit/mozapps/update/updater/release_secondary.der b/toolkit/mozapps/update/updater/release_secondary.der
index 474706c4b73c..fcee3944e9b7 100644
Binary files a/toolkit/mozapps/update/updater/release_secondary.der and b/toolkit/mozapps/update/updater/release_secondary.der differ
1
0

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 25741 - TBA: Disable GeckoNetworkManager
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit bffe7042f46cced8835c851dc2bef0ba33d5cc16
Author: Matthew Finkel <Matthew.Finkel(a)gmail.com>
Date: Thu Apr 26 22:22:51 2018 +0000
Bug 25741 - TBA: Disable GeckoNetworkManager
The browser should not need information related to the network
interface or network state, tor should take care of that.
---
.../src/main/java/org/mozilla/geckoview/GeckoRuntime.java | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
index f084b522ad53..b94d8e803b6b 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
@@ -122,7 +122,9 @@ public final class GeckoRuntime implements Parcelable {
mPaused = false;
// Monitor network status and send change notifications to Gecko
// while active.
- GeckoNetworkManager.getInstance().start(GeckoAppShell.getApplicationContext());
+ if (BuildConfig.TOR_BROWSER_VERSION == "") {
+ GeckoNetworkManager.getInstance().start(GeckoAppShell.getApplicationContext());
+ }
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
@@ -130,7 +132,9 @@ public final class GeckoRuntime implements Parcelable {
Log.d(LOGTAG, "Lifecycle: onPause");
mPaused = true;
// Stop monitoring network status while inactive.
- GeckoNetworkManager.getInstance().stop();
+ if (BuildConfig.TOR_BROWSER_VERSION == "") {
+ GeckoNetworkManager.getInstance().stop();
+ }
GeckoThread.onPause();
}
}
1
0

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 28125 - Prevent non-Necko network connections
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit 7c0197768e62fc33bda557715b3b5363c64d2707
Author: Matthew Finkel <Matthew.Finkel(a)gmail.com>
Date: Thu Oct 25 19:17:09 2018 +0000
Bug 28125 - Prevent non-Necko network connections
---
.../gecko/media/GeckoMediaDrmBridgeV21.java | 49 +---------------------
.../exoplayer2/upstream/DefaultHttpDataSource.java | 47 ++-------------------
2 files changed, 4 insertions(+), 92 deletions(-)
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
index 6e75e6fb4dd5..7faa9bc2821d 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
@@ -490,54 +490,7 @@ public class GeckoMediaDrmBridgeV21 implements GeckoMediaDrm {
@Override
protected Void doInBackground(final Void... params) {
- HttpURLConnection urlConnection = null;
- BufferedReader in = null;
- try {
- final URI finalURI = new URI(mURL + "&signedRequest=" + URLEncoder.encode(new String(mDrmRequest), "UTF-8"));
- urlConnection = (HttpURLConnection) ProxySelector.openConnectionWithProxy(finalURI);
- urlConnection.setRequestMethod("POST");
- if (DEBUG) Log.d(LOGTAG, "Provisioning, posting url =" + finalURI.toString());
-
- // Add data
- urlConnection.setRequestProperty("Accept", "*/*");
- urlConnection.setRequestProperty("User-Agent", getCDMUserAgent());
- urlConnection.setRequestProperty("Content-Type", "application/json");
-
- // Execute HTTP Post Request
- urlConnection.connect();
-
- final int responseCode = urlConnection.getResponseCode();
- if (responseCode == HttpURLConnection.HTTP_OK) {
- in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), UTF_8));
- String inputLine;
- final StringBuffer response = new StringBuffer();
-
- while ((inputLine = in.readLine()) != null) {
- response.append(inputLine);
- }
- in.close();
- mResponseBody = String.valueOf(response).getBytes(UTF_8);
- if (DEBUG) Log.d(LOGTAG, "Provisioning, response received.");
- if (mResponseBody != null) Log.d(LOGTAG, "response length=" + mResponseBody.length);
- } else {
- Log.d(LOGTAG, "Provisioning, server returned HTTP error code :" + responseCode);
- }
- } catch (final IOException e) {
- Log.e(LOGTAG, "Got exception during posting provisioning request ...", e);
- } catch (final URISyntaxException e) {
- Log.e(LOGTAG, "Got exception during creating uri ...", e);
- } finally {
- if (urlConnection != null) {
- urlConnection.disconnect();
- }
- try {
- if (in != null) {
- in.close();
- }
- } catch (final IOException e) {
- Log.e(LOGTAG, "Exception during closing in ...", e);
- }
- }
+ Log.i(LOGTAG, "This is Tor Browser. Skipping.");
return null;
}
diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
index 6e5095b0a4c9..a585e283ed4e 100644
--- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
+++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
@@ -46,6 +46,7 @@ import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import org.mozilla.gecko.util.ProxySelector;
+
/**
* An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
*
@@ -516,50 +517,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
boolean followRedirects,
Map<String, String> requestParameters)
throws IOException, URISyntaxException {
- /**
- * Tor Project modified the way the connection object was created. For the sake of
- * simplicity, instead of duplicating the whole file we changed the connection object
- * to use the ProxySelector.
- */
- HttpURLConnection connection = (HttpURLConnection) ProxySelector.openConnectionWithProxy(url.toURI());
-
- connection.setConnectTimeout(connectTimeoutMillis);
- connection.setReadTimeout(readTimeoutMillis);
-
- Map<String, String> requestHeaders = new HashMap<>();
- if (defaultRequestProperties != null) {
- requestHeaders.putAll(defaultRequestProperties.getSnapshot());
- }
- requestHeaders.putAll(requestProperties.getSnapshot());
- requestHeaders.putAll(requestParameters);
-
- for (Map.Entry<String, String> property : requestHeaders.entrySet()) {
- connection.setRequestProperty(property.getKey(), property.getValue());
- }
-
- if (!(position == 0 && length == C.LENGTH_UNSET)) {
- String rangeRequest = "bytes=" + position + "-";
- if (length != C.LENGTH_UNSET) {
- rangeRequest += (position + length - 1);
- }
- connection.setRequestProperty("Range", rangeRequest);
- }
- connection.setRequestProperty("User-Agent", userAgent);
- connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity");
- connection.setInstanceFollowRedirects(followRedirects);
- connection.setDoOutput(httpBody != null);
- connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
-
- if (httpBody != null) {
- connection.setFixedLengthStreamingMode(httpBody.length);
- connection.connect();
- OutputStream os = connection.getOutputStream();
- os.write(httpBody);
- os.close();
- } else {
- connection.connect();
- }
- return connection;
+ Log.i(TAG, "This is Tor Browser. Skipping.");
+ throw new IOException();
}
/** Creates an {@link HttpURLConnection} that is connected with the {@code url}. */
1
0

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 33342: Avoid disconnect search addon error after removal.
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit 0f9028fcd40024bbfb9b45f703e77daf4fc37a8b
Author: Alex Catarineu <acat(a)torproject.org>
Date: Fri Mar 13 18:19:30 2020 +0100
Bug 33342: Avoid disconnect search addon error after removal.
We removed the addon in #32767, but it was still being loaded
from addonStartup.json.lz4 and throwing an error on startup
because its resource: location is not available anymore.
---
toolkit/mozapps/extensions/internal/XPIProvider.jsm | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
index 04d57a42348e..ba562c92948d 100644
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -959,6 +959,12 @@ var BuiltInLocation = new (class _BuiltInLocation extends XPIStateLocation {
isLinkedAddon(/* aId */) {
return false;
}
+
+ restore(saved) {
+ super.restore(saved);
+ // Bug 33342: avoid restoring disconnect addon from addonStartup.json.lz4.
+ this.removeAddon("disconnect(a)search.mozilla.org");
+ }
})();
/**
1
0

[tor-browser/tor-browser-91.3.0esr-11.0-1] Bug 21952: Implement Onion-Location
by sysrqb@torproject.org 02 Nov '21
by sysrqb@torproject.org 02 Nov '21
02 Nov '21
commit 33e14475fc9a7805c63e07bebd930e90606e36cd
Author: Alex Catarineu <acat(a)torproject.org>
Date: Thu Mar 5 22:16:39 2020 +0100
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.
---
browser/base/content/browser.js | 12 ++
browser/base/content/navigator-toolbox.inc.xhtml | 3 +
browser/components/BrowserGlue.jsm | 13 ++
.../onionservices/OnionLocationChild.jsm | 39 +++++
.../onionservices/OnionLocationParent.jsm | 168 +++++++++++++++++++++
.../content/onionlocation-notification-icons.css | 5 +
.../onionservices/content/onionlocation-urlbar.css | 60 ++++++++
.../content/onionlocation-urlbar.inc.xhtml | 10 ++
.../onionservices/content/onionlocation.svg | 3 +
.../content/onionlocationPreferences.inc.xhtml | 11 ++
.../content/onionlocationPreferences.js | 31 ++++
browser/components/onionservices/jar.mn | 2 +
browser/components/onionservices/moz.build | 2 +
browser/components/preferences/privacy.inc.xhtml | 2 +
browser/components/preferences/privacy.js | 17 +++
browser/themes/shared/notification-icons.inc.css | 2 +
browser/themes/shared/urlbar-searchbar.inc.css | 2 +
dom/base/Document.cpp | 34 ++++-
dom/base/Document.h | 2 +
dom/webidl/Document.webidl | 8 +
modules/libpref/init/StaticPrefList.yaml | 5 +
xpcom/ds/StaticAtoms.py | 1 +
22 files changed, 431 insertions(+), 1 deletion(-)
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index b934e112eb23..090de22cb294 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -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",
@@ -5304,6 +5305,7 @@ var XULBrowserWindow = {
CFRPageActions.updatePageActions(gBrowser.selectedBrowser);
AboutReaderParent.updateReaderButton(gBrowser.selectedBrowser);
+ OnionLocationParent.updateOnionLocationBadge(gBrowser.selectedBrowser);
if (!gMultiProcessBrowser) {
// Bug 1108553 - Cannot rotate images with e10s
@@ -5794,6 +5796,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/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index 7a2715e9e604..1aad36ab3bfc 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -358,6 +358,9 @@
onclick="FullZoom.reset(); FullZoom.resetScalingZoom();"
tooltip="dynamic-shortcut-tooltip"
hidden="true"/>
+
+#include ../../components/onionservices/content/onionlocation-urlbar.inc.xhtml
+
<hbox id="pageActionButton"
class="urlbar-page-action urlbar-icon-wrapper"
role="button"
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index 818cbb04837a..3fc7e912ff0c 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -545,6 +545,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",
diff --git a/browser/components/onionservices/OnionLocationChild.jsm b/browser/components/onionservices/OnionLocationChild.jsm
new file mode 100644
index 000000000000..9e00054ac56c
--- /dev/null
+++ b/browser/components/onionservices/OnionLocationChild.jsm
@@ -0,0 +1,39 @@
+// 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
+ );
+ }
+ }
+ }
+}
diff --git a/browser/components/onionservices/OnionLocationParent.jsm b/browser/components/onionservices/OnionLocationParent.jsm
new file mode 100644
index 000000000000..f6250e554862
--- /dev/null
+++ b/browser/components/onionservices/OnionLocationParent.jsm
@@ -0,0 +1,168 @@
+// 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/080f9ed47742644d2ff84f7aa0b10aea5c4…
+ 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);
+ }
+ }
+}
diff --git a/browser/components/onionservices/content/onionlocation-notification-icons.css b/browser/components/onionservices/content/onionlocation-notification-icons.css
new file mode 100644
index 000000000000..7c8a6d892c6f
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocation-notification-icons.css
@@ -0,0 +1,5 @@
+/* Copyright (c) 2020, The Tor Project, Inc. */
+
+.onionlocation-notification-icon {
+ display: none;
+}
\ No newline at end of file
diff --git a/browser/components/onionservices/content/onionlocation-urlbar.css b/browser/components/onionservices/content/onionlocation-urlbar.css
new file mode 100644
index 000000000000..7b7051ace675
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocation-urlbar.css
@@ -0,0 +1,60 @@
+/* Copyright (c) 2020, The Tor Project, Inc. */
+
+#onion-location-box {
+ height: 28px;
+
+ background-color: var(--purple-60);
+ -moz-context-properties: fill;
+ fill: white;
+}
+
+#onion-location-box:hover {
+ background-color: var(--purple-70);
+}
+
+#onion-location-box:active {
+ background-color: var(--purple-80);
+}
+
+@media (prefers-color-scheme: dark) {
+ #onion-location-box {
+ background-color: var(--purple-50);
+ }
+
+ #onion-location-box:hover {
+ background-color: var(--purple-60);
+ }
+
+ #onion-location-box:active {
+ background-color: var(--purple-70);
+ }
+}
+
+#onion-location-button {
+ list-style-image: url(chrome://browser/content/onionservices/onionlocation.svg);
+ padding-inline-start: 0.5em;
+}
+
+label#onion-label {
+ line-height: 28px;
+ margin: 0;
+ padding-block: 0;
+ padding-inline: 0.5em;
+ color: white;
+ font-weight: bold;
+}
+
+/* set appropriate sizes for the non-standard ui densities */
+:root[uidensity=compact] hbox.urlbar-page-action#onion-location-box {
+ height: 24px;
+}
+:root[uidensity=compact] label#onion-label {
+ line-height: 24px;
+}
+
+:root[uidensity=touch] hbox.urlbar-page-action#onion-location-box {
+ height: 30px;
+}
+:root[uidensity=touch] label#onion-label {
+ line-height: 30px;
+}
diff --git a/browser/components/onionservices/content/onionlocation-urlbar.inc.xhtml b/browser/components/onionservices/content/onionlocation-urlbar.inc.xhtml
new file mode 100644
index 000000000000..b612a4236f3c
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocation-urlbar.inc.xhtml
@@ -0,0 +1,10 @@
+# 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>
diff --git a/browser/components/onionservices/content/onionlocation.svg b/browser/components/onionservices/content/onionlocation.svg
new file mode 100644
index 000000000000..37f40ac1812f
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocation.svg
@@ -0,0 +1,3 @@
+<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
diff --git a/browser/components/onionservices/content/onionlocationPreferences.inc.xhtml b/browser/components/onionservices/content/onionlocationPreferences.inc.xhtml
new file mode 100644
index 000000000000..c285f403f99b
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocationPreferences.inc.xhtml
@@ -0,0 +1,11 @@
+# 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>
diff --git a/browser/components/onionservices/content/onionlocationPreferences.js b/browser/components/onionservices/content/onionlocationPreferences.js
new file mode 100644
index 000000000000..aa569b54721c
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocationPreferences.js
@@ -0,0 +1,31 @@
+// 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,
+});
diff --git a/browser/components/onionservices/jar.mn b/browser/components/onionservices/jar.mn
index 9d6ce88d1841..f45b16dc5d29 100644
--- a/browser/components/onionservices/jar.mn
+++ b/browser/components/onionservices/jar.mn
@@ -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)
diff --git a/browser/components/onionservices/moz.build b/browser/components/onionservices/moz.build
index 815685322024..8027233d65a6 100644
--- a/browser/components/onionservices/moz.build
+++ b/browser/components/onionservices/moz.build
@@ -4,4 +4,6 @@ EXTRA_JS_MODULES += [
"ExtensionMessaging.jsm",
"HttpsEverywhereControl.jsm",
"OnionAliasStore.jsm",
+ "OnionLocationChild.jsm",
+ "OnionLocationParent.jsm",
]
diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml
index 7d3bc3a663ed..eb1cedfdd4c9 100644
--- a/browser/components/preferences/privacy.inc.xhtml
+++ b/browser/components/preferences/privacy.inc.xhtml
@@ -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>
diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js
index 932d4291e486..1985b5489fc1 100644
--- a/browser/components/preferences/privacy.js
+++ b/browser/components/preferences/privacy.js
@@ -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" },
@@ -333,6 +342,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.
*/
@@ -530,6 +546,7 @@ var gPrivacyPane = {
this._initTrackingProtectionExtensionControl();
OnionServicesAuthPreferences.init();
this._initSecurityLevel();
+ this._initOnionLocation();
Services.telemetry.setEventRecordingEnabled("pwmgr", true);
diff --git a/browser/themes/shared/notification-icons.inc.css b/browser/themes/shared/notification-icons.inc.css
index 67dd640baf16..83248f71c60d 100644
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -449,3 +449,5 @@
-moz-context-properties: fill;
fill: var(--panel-banner-item-warning-icon-bgcolor);
}
+
+%include ../../components/onionservices/content/onionlocation-notification-icons.css
diff --git a/browser/themes/shared/urlbar-searchbar.inc.css b/browser/themes/shared/urlbar-searchbar.inc.css
index 82675dae2041..0158597991ec 100644
--- a/browser/themes/shared/urlbar-searchbar.inc.css
+++ b/browser/themes/shared/urlbar-searchbar.inc.css
@@ -745,3 +745,5 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left::after {
.searchbar-textbox::placeholder {
opacity: 0.69;
}
+
+%include ../../components/onionservices/content/onionlocation-urlbar.css
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
index a58e76cb5258..b00399e2eccb 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -2779,6 +2779,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
@@ -6486,6 +6487,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");
@@ -6560,6 +6577,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,
@@ -10697,7 +10729,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.
diff --git a/dom/base/Document.h b/dom/base/Document.h
index 7165496397f3..c8de049526ea 100644
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -3365,6 +3365,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) {
@@ -4348,6 +4349,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;
diff --git a/dom/webidl/Document.webidl b/dom/webidl/Document.webidl
index a139ace11d4a..d934cf8da045 100644
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -711,3 +711,11 @@ partial interface Document {
[ChromeOnly]
void setNotifyFormOrPasswordRemoved(boolean aShouldNotify);
};
+
+/**
+ * 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;
+};
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
index 9d994989b896..2daf4f2d9fe0 100644
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -10469,6 +10469,11 @@
value: ""
mirror: never
+- name: privacy.prioritizeonions.enabled
+ type: RelaxedAtomicBool
+ value: false
+ mirror: always
+
#---------------------------------------------------------------------------
# Prefs starting with "prompts."
#---------------------------------------------------------------------------
diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py
index 2f5be143517b..f620f57a1213 100644
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -821,6 +821,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"),
1
0