commit 96d9c18343a6247004de4069478106e0113b5f41 parent 1b34a13fdffbac49baf9323a9dc854544159d34f Author: Kathy Brade <brade@pearlcrescent.com> Date: Tue, 12 Nov 2019 16:11:05 -0500 TB 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. Diffstat:
37 files changed, 1359 insertions(+), 2 deletions(-)
diff --git a/browser/app/profile/000-tor-browser.js b/browser/app/profile/000-tor-browser.js @@ -141,3 +141,4 @@ pref("browser.tordomainisolator.loglevel", "Warn"); pref("browser.torcircuitpanel.loglevel", "Log"); pref("browser.tor_android.log_level", "Info"); pref("browser.dragdropfilter.log_level", "Warn"); +pref("browser.onionAuthPrompt.loglevel", "Warn"); diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js @@ -249,6 +249,9 @@ var gBrowserInit = { gTorConnectUrlbarButton.init(); gTorConnectTitlebarStatus.init(); + // Init the OnionAuthPrompt + OnionAuthPrompt.init(); + gTorCircuitPanel.init(); // Certain kinds of automigration rely on this notification to complete @@ -1117,6 +1120,8 @@ var gBrowserInit = { gTorConnectUrlbarButton.uninit(); gTorConnectTitlebarStatus.uninit(); + OnionAuthPrompt.uninit(); + gTorCircuitPanel.uninit(); if (gToolbarKeyNavEnabled) { diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js @@ -284,6 +284,11 @@ XPCOMUtils.defineLazyScriptGetter( ); XPCOMUtils.defineLazyScriptGetter( this, + ["OnionAuthPrompt"], + "chrome://browser/content/onionservices/authPrompt.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, "gEditItemOverlay", "chrome://browser/content/places/editBookmark.js" ); diff --git a/browser/base/content/browser.js.globals b/browser/base/content/browser.js.globals @@ -249,5 +249,6 @@ "TorConnectTopics", "TorConnectParent", "gTorConnectUrlbarButton", - "gTorConnectTitlebarStatus" + "gTorConnectTitlebarStatus", + "OnionAuthPrompt" ] diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml @@ -59,6 +59,7 @@ <link rel="stylesheet" href="chrome://browser/content/securitylevel/securityLevelButton.css" /> <link rel="stylesheet" href="chrome://browser/content/torCircuitPanel.css" /> <link rel="stylesheet" href="chrome://global/content/torconnect/torConnectTitlebarStatus.css" /> + <link rel="stylesheet" href="chrome://browser/content/onionservices/onionservices.css" /> <link rel="localization" href="branding/brand.ftl"/> <link rel="localization" href="browser/allTabsMenu.ftl"/> diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml @@ -690,6 +690,7 @@ #include ../../components/tabbrowser/content/browser-allTabsMenu.inc.xhtml #include ../../components/torcircuit/content/torCircuitPanel.inc.xhtml #include ../../components/securitylevel/content/securityLevelPanel.inc.xhtml +#include ../../components/onionservices/content/authPopup.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 @@ -311,6 +311,7 @@ data-l10n-id="urlbar-indexed-db-notification-anchor"/> <image id="password-notification-icon" class="notification-anchor-icon" role="button" data-l10n-id="urlbar-password-notification-anchor"/> +#include ../../components/onionservices/content/authNotificationIcon.inc.xhtml <image id="web-notifications-notification-icon" class="notification-anchor-icon desktop-notification-icon" role="button" data-l10n-id="urlbar-web-notification-anchor"/> <image id="webRTC-shareDevices-notification-icon" class="notification-anchor-icon camera-icon" role="button" diff --git a/browser/components/moz.build b/browser/components/moz.build @@ -50,6 +50,7 @@ DIRS += [ "multilineeditor", "newidentity", # Exclude newtab component. tor-browser#43886. + "onionservices", "originattributes", "pagedata", "permissions", diff --git a/browser/components/onionservices/content/authNotificationIcon.inc.xhtml 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" + data-l10n-id="onion-site-authentication-urlbar-button"/> diff --git a/browser/components/onionservices/content/authPopup.inc.xhtml b/browser/components/onionservices/content/authPopup.inc.xhtml @@ -0,0 +1,41 @@ +# Copyright (c) 2020, The Tor Project, Inc. + +<popupnotification id="tor-clientauth-notification" hidden="true"> + <popupnotificationcontent orient="vertical"> + <description id="tor-clientauth-notification-desc" /> + <label + class="text-link popup-notification-learnmore-link" + is="text-link" + href="about:manual#onion-services_onion-service-authentication" + useoriginprincipal="true" + data-l10n-id="onion-site-authentication-prompt-learn-more" + /> + <html:div> + <!-- NOTE: Orca 46.2 will not say "invalid" for "type=password". See + - https://gitlab.gnome.org/GNOME/orca/-/issues/550 + - Moreover, it will ignore the aria-errormessage relation when we are + - not in a document context. See related bugzilla bug 1820765. --> + <html:input + id="tor-clientauth-notification-key" + type="password" + data-l10n-id="onion-site-authentication-prompt-key-input" + aria-errormessage="tor-clientauth-warning" + /> + <html:div + id="tor-clientauth-warning" + role="alert" + aria-labelledby="tor-clientauth-warning-text" + > + <!-- NOTE: Orca 46.2 treats this notification as non-document context. + - As such it seems to only read out the alert content if it contains + - a <xul:label>, <html:label> or if it has an accessible name. + - We use aria-labelledby here. --> + <html:span id="tor-clientauth-warning-text"></html:span> + </html:div> + <checkbox + id="tor-clientauth-persistkey-checkbox" + data-l10n-id="onion-site-authentication-prompt-remember-checkbox" + /> + </html:div> + </popupnotificationcontent> +</popupnotification> diff --git a/browser/components/onionservices/content/authPreferences.css b/browser/components/onionservices/content/authPreferences.css @@ -0,0 +1,37 @@ +/* Copyright (c) 2020, The Tor Project, Inc. */ + +#onionservices-savedkeys-dialog { + min-width: 45em; +} + +#onionservices-savedkeys-tree treechildren::-moz-tree-cell-text { + font-size: var(--font-size-small); +} + +#onionservices-savedkeys-errorContainer { + margin-block-start: 4px; + min-height: 3em; +} + +#onionservices-savedkeys-errorContainer:not(.show-error) { + visibility: hidden; +} + +#onionservices-savedkeys-errorIcon { + margin-inline-end: 4px; + list-style-image: url("chrome://global/skin/icons/warning.svg"); + -moz-context-properties: fill; + fill: var(--icon-color-warning); +} + +/* Make a button appear disabled, whilst still allowing it to keep keyboard + * focus. + * Duplicate of rule in torPreferences.css. + * TODO: Replace with moz-button when it handles this for us. See + * tor-browser#43275. */ +button.spoof-button-disabled { + /* Borrow the :disabled rule from common-shared.css */ + opacity: 0.4; + /* Also ensure it does not get hover or active styling. */ + pointer-events: none; +} diff --git a/browser/components/onionservices/content/authPreferences.inc.xhtml b/browser/components/onionservices/content/authPreferences.inc.xhtml @@ -0,0 +1,34 @@ +# Copyright (c) 2020, The Tor Project, Inc. + +<groupbox id="torOnionServiceKeys" orient="vertical" + data-category="panePrivacy" hidden="true"> + <label><html:h2 + data-l10n-id="onion-site-authentication-preferences-heading" + ></html:h2></label> + <hbox> + <description + class="description-deemphasized description-with-side-element" + flex="1" + > + <html:span + id="torOnionServiceKeys-overview" + data-l10n-id="onion-site-authentication-preferences-overview" + ></html:span> + <label + id="torOnionServiceKeys-learnMore" + class="learnMore text-link" + is="text-link" + href="about:manual#onion-services_onion-service-authentication" + useoriginprincipal="true" + data-l10n-id="onion-site-authentication-preferences-learn-more" + /> + </description> + <vbox align="end"> + <html:button + id="torOnionServiceKeys-savedKeys" + class="accessory-button" + data-l10n-id="onion-site-authentication-preferences-saved-keys-button" + ></html:button> + </vbox> + </hbox> +</groupbox> diff --git a/browser/components/onionservices/content/authPreferences.js b/browser/components/onionservices/content/authPreferences.js @@ -0,0 +1,20 @@ +// Copyright (c) 2020, The Tor Project, Inc. + +"use strict"; + +/* import-globals-from /browser/components/preferences/preferences.js */ + +/** + * Onion site preferences. + */ +var OnionServicesAuthPreferences = { + init() { + document + .getElementById("torOnionServiceKeys-savedKeys") + .addEventListener("click", () => { + gSubDialog.open( + "chrome://browser/content/onionservices/savedKeysDialog.xhtml" + ); + }); + }, +}; diff --git a/browser/components/onionservices/content/authPrompt.js b/browser/components/onionservices/content/authPrompt.js @@ -0,0 +1,433 @@ +"use strict"; + +var OnionAuthPrompt = { + // Only import to our internal scope, rather than the global scope of + // browser.xhtml. + _lazy: {}, + + /** + * The topics to listen to. + * + * @type {{[key: string]: string}} + */ + _topics: { + clientAuthMissing: "tor-onion-services-clientauth-missing", + clientAuthIncorrect: "tor-onion-services-clientauth-incorrect", + }, + + /** + * @typedef {object} PromptDetails + * + * @property {Browser} browser - The browser this prompt is for. + * @property {string} cause - The notification that cause this prompt. + * @property {string} onionHost - The onion host name. + * @property {nsIURI} uri - The browser URI when the notification was + * triggered. + * @property {string} onionServiceId - The onion service ID for this host. + * @property {Notification} [notification] - The notification instance for + * this prompt. + */ + + /** + * The currently shown details in the prompt. + * + * @type {?PromptDetails} + */ + _shownDetails: null, + + /** + * Used for logging to represent PromptDetails. + * + * @param {PromptDetails} details - The details to represent. + * @returns {string} - The representation of these details. + */ + _detailsRepr(details) { + if (!details) { + return "none"; + } + return `${details.browser.browserId}:${details.onionHost}`; + }, + + /** + * Show a new prompt, using the given details. + * + * @param {PromptDetails} details - The details to show. + */ + show(details) { + this._logger.debug(`New Notification: ${this._detailsRepr(details)}`); + + // NOTE: PopupNotifications currently requires the accesskey and label to be + // set for all actions, and does not accept fluent IDs in their place. + // Moreover, there doesn't appear to be a simple way to work around this, so + // we have to fetch the strings here before calling the show() method. + // NOTE: We avoid using the async formatMessages because we don't want to + // race against the browser's location changing. + // In principle, we could check that the details.browser.currentURI still + // matches details.uri or use a LocationChange listener. However, we expect + // that PopupNotifications will eventually change to accept fluent IDs, so + // we won't have to use formatMessages here at all. + // Moreover, we do not expect this notification to be common, so this + // shouldn't be too expensive. + // NOTE: Once we call PopupNotifications.show, PopupNotifications should + // take care of listening for changes in locations for us and remove the + // notification. + let [okButtonMsg, cancelButtonMsg] = this._lazy.SyncL10n.formatMessagesSync( + [ + "onion-site-authentication-prompt-ok-button", + "onion-site-authentication-prompt-cancel-button", + ] + ); + + // Get an attribute string from a L10nMessage. + // We wrap the return value as a String to prevent the notification from + // throwing (and not showing) if a locale is unexpectedly missing a value. + const msgAttribute = (msg, name) => + String((msg.attributes ?? []).find(attr => attr.name === name)?.value); + + let mainAction = { + label: msgAttribute(okButtonMsg, "label"), + accessKey: msgAttribute(okButtonMsg, "accesskey"), + leaveOpen: true, // Callback is responsible for closing the notification. + callback: () => this._onDone(), + }; + + // The first secondarybuttoncommand (cancelAction) should be triggered when + // the user presses "Escape". + let cancelAction = { + label: msgAttribute(cancelButtonMsg, "label"), + accessKey: msgAttribute(cancelButtonMsg, "accesskey"), + callback: () => this._onCancel(), + }; + + let options = { + autofocus: true, + hideClose: true, + persistent: true, + removeOnDismissal: false, + eventCallback: topic => { + if (topic === "showing") { + this._onPromptShowing(details); + } else if (topic === "shown") { + this._onPromptShown(); + } else if (topic === "removed") { + this._onPromptRemoved(details); + } + }, + }; + + details.notification = PopupNotifications.show( + details.browser, + "tor-clientauth", + "", + "tor-clientauth-notification-icon", + mainAction, + [cancelAction], + options + ); + }, + + /** + * Callback when the prompt is about to be shown. + * + * @param {PromptDetails?} details - The details to show, or null to shown + * none. + */ + _onPromptShowing(details) { + if (details === this._shownDetails) { + // The last shown details match this one exactly. + // This happens when we switch tabs to a page that has no prompt and then + // switch back. + // We don't want to reset the current state in this case. + // In particular, we keep the current _keyInput value and _persistCheckbox + // the same. + this._logger.debug(`Already showing: ${this._detailsRepr(details)}`); + return; + } + + this._logger.debug(`Now showing: ${this._detailsRepr(details)}`); + + this._shownDetails = details; + + // Clear the key input. + // In particular, clear the input when switching tabs. + this._keyInput.value = ""; + this._persistCheckbox.checked = false; + + document.l10n.setAttributes( + this._descriptionEl, + "onion-site-authentication-prompt-description", + { + onionsite: TorUIUtils.shortenOnionAddress( + this._shownDetails?.onionHost ?? "" + ), + } + ); + + this._showWarning(null); + }, + + /** + * Callback after the prompt is shown. + */ + _onPromptShown() { + this._keyInput.focus(); + }, + + /** + * Callback when a Notification is removed. + * + * @param {PromptDetails} details - The details for the removed notification. + */ + _onPromptRemoved(details) { + if (details !== this._shownDetails) { + // Removing the notification for some other page. + // For example, closing another tab that also requires authentication. + this._logger.debug(`Removed not shown: ${this._detailsRepr(details)}`); + return; + } + this._logger.debug(`Removed shown: ${this._detailsRepr(details)}`); + // Reset the prompt as a precaution. + // In particular, we want to clear the input so that the entered key does + // not persist. + this._onPromptShowing(null); + }, + + /** + * Callback when the user submits the key. + */ + async _onDone() { + this._logger.debug( + `Sumbitting key: ${this._detailsRepr(this._shownDetails)}` + ); + + // Grab the details before they might change as we await. + const details = this._shownDetails; + const { browser, onionServiceId, notification } = details; + const isPermanent = this._persistCheckbox.checked; + + const base64key = this._keyToBase64(this._keyInput.value); + if (!base64key) { + this._showWarning("onion-site-authentication-prompt-invalid-key"); + return; + } + + try { + const provider = await this._lazy.TorProviderBuilder.build(); + await provider.onionAuthAdd(onionServiceId, base64key, isPermanent); + } catch (e) { + this._logger.error(`Failed to set key for ${onionServiceId}`, e); + if (details === this._shownDetails) { + // Notification has not been replaced. + this._showWarning( + "onion-site-authentication-prompt-setting-key-failed" + ); + } + return; + } + + notification.remove(); + // Success! Reload the page. + browser.reload(); + }, + + /** + * Callback when the user dismisses the prompt. + */ + _onCancel() { + // Arrange for an error page to be displayed: + // we build a short script calling docShell.displayError() + // and we pass it as a data: URI to loadFrameScript(), + // which runs it in the content frame which triggered + // this authentication prompt. + this._logger.debug(`Cancelling: ${this._detailsRepr(this._shownDetails)}`); + + const { browser, cause, uri } = this._shownDetails; + const errorCode = + cause === this._topics.clientAuthMissing + ? Cr.NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH + : Cr.NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH; + browser.messageManager.loadFrameScript( + `data:application/javascript,${encodeURIComponent( + `docShell.displayLoadError(${errorCode}, Services.io.newURI(${JSON.stringify( + uri.spec + )}), undefined, undefined);` + )}`, + false + ); + }, + + /** + * Show a warning message to the user or clear the warning. + * + * @param {?string} warningMessageId - The l10n ID for the message to show, or + * null to clear the current message. + */ + _showWarning(warningMessageId) { + this._logger.debug(`Showing warning: ${warningMessageId}`); + + if (warningMessageId) { + document.l10n.setAttributes(this._warningTextEl, warningMessageId); + this._warningEl.removeAttribute("hidden"); + this._keyInput.classList.add("invalid"); + this._keyInput.setAttribute("aria-invalid", "true"); + } else { + this._warningTextEl.removeAttribute("data-l10n-id"); + this._warningTextEl.textContent = ""; + this._warningEl.setAttribute("hidden", "true"); + this._keyInput.classList.remove("invalid"); + this._keyInput.removeAttribute("aria-invalid"); + } + }, + + /** + * Convert the user-entered key into base64. + * + * @param {string} keyString - The key to convert. + * @returns {?string} - The base64 representation, or undefined if the given + * key was not the correct format. + */ + _keyToBase64(keyString) { + if (!keyString) { + return undefined; + } + + let base64key; + if (keyString.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 = this._lazy.CommonUtils.decodeBase32(keyString.toUpperCase()); + } catch (e) {} + + if (rawKey) { + try { + base64key = btoa(rawKey); + } catch (e) {} + } + } else if ( + keyString.length === 44 && + /^[a-zA-Z0-9+/]*=*$/.test(keyString) + ) { + // 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 = keyString; + } + + return base64key; + }, + + /** + * Initialize the authentication prompt. + */ + init() { + this._logger = console.createInstance({ + prefix: "OnionAuthPrompt", + maxLogLevelPref: "browser.onionAuthPrompt.loglevel", + }); + + ChromeUtils.defineESModuleGetters(this._lazy, { + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", + CommonUtils: "resource://services-common/utils.sys.mjs", + }); + // Allow synchornous access to the localized strings. Used only for the + // button actions, which is currently a hard requirement for + // PopupNotifications.show. Hopefully, PopupNotifications will accept fluent + // ids in their place, or get replaced with something else that does. + ChromeUtils.defineLazyGetter(this._lazy, "SyncL10n", () => { + return new Localization(["toolkit/global/tor-browser.ftl"], true); + }); + + this._keyInput = document.getElementById("tor-clientauth-notification-key"); + this._persistCheckbox = document.getElementById( + "tor-clientauth-persistkey-checkbox" + ); + this._warningEl = document.getElementById("tor-clientauth-warning"); + this._warningTextEl = document.getElementById( + "tor-clientauth-warning-text" + ); + this._descriptionEl = document.getElementById( + "tor-clientauth-notification-desc" + ); + + this._keyInput.addEventListener("keydown", event => { + if (event.key === "Enter") { + event.preventDefault(); + this._onDone(); + } + }); + this._keyInput.addEventListener("input", () => { + // Remove the warning. + this._showWarning(null); + }); + + // Force back focus on click: tor-browser#41856 + document + .getElementById("tor-clientauth-notification") + .addEventListener("click", () => { + window.focus(); + }); + + Services.obs.addObserver(this, this._topics.clientAuthMissing); + Services.obs.addObserver(this, this._topics.clientAuthIncorrect); + }, + + /** + * Un-initialize the authentication prompt. + */ + uninit() { + Services.obs.removeObserver(this, this._topics.clientAuthMissing); + Services.obs.removeObserver(this, this._topics.clientAuthIncorrect); + }, + + observe(subject, topic, data) { + if ( + topic !== this._topics.clientAuthMissing && + topic !== this._topics.clientAuthIncorrect + ) { + return; + } + + // "subject" is the DOM window or browser where the prompt should be shown. + let browser; + if (subject instanceof Ci.nsIDOMWindow) { + let contentWindow = subject.QueryInterface(Ci.nsIDOMWindow); + browser = contentWindow.docShell.chromeEventHandler; + } else { + browser = subject.QueryInterface(Ci.nsIBrowser); + } + + if (!gBrowser.browsers.includes(browser)) { + // This window does not contain the subject browser. + this._logger.debug( + `Window ${window.docShell.outerWindowID}: Ignoring ${topic}` + ); + return; + } + this._logger.debug( + `Window ${window.docShell.outerWindowID}: Handling ${topic}` + ); + + const onionHost = data; + // ^(subdomain.)*onionserviceid.onion$ (case-insensitive) + const onionServiceId = onionHost + .match(/^(.*\.)?(?<onionServiceId>[a-z2-7]{56})\.onion$/i) + ?.groups.onionServiceId.toLowerCase(); + if (!onionServiceId) { + this._logger.error(`Malformed onion address: ${onionHost}`); + return; + } + + const details = { + browser, + cause: topic, + onionHost, + uri: browser.currentURI, + onionServiceId, + }; + this.show(details); + }, +}; diff --git a/browser/components/onionservices/content/onionservices.css b/browser/components/onionservices/content/onionservices.css @@ -0,0 +1,67 @@ +/* Copyright (c) 2020, The Tor Project, Inc. */ + +#tor-clientauth-notification-desc { + font-weight: var(--font-weight-bold); +} + +#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). + */ +#tor-clientauth-notification-key.invalid { + border: 1px solid var(--outline-color-error); +} + +#tor-clientauth-warning { + display: inline-block; + animation: fade-up-tt 450ms; + /* FIXME: This warning block does not follow upstream's styling. */ + background: var(--button-background-color-destructive); + color: var(--button-text-color-destructive); + border-radius: var(--border-radius-xsmall); + inset-inline-start: 3px; + padding: 5px 12px; + position: relative; + top: 6px; + z-index: 1; +} + +#tor-clientauth-warning[hidden] { + display: none; +} + +#tor-clientauth-warning::before { + background: var(--button-background-color-destructive); + 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 @@ -0,0 +1,291 @@ +// Copyright (c) 2020, The Tor Project, Inc. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", +}); + +var gOnionServicesSavedKeysDialog = { + _tree: undefined, + _busyCount: 0, + get _isBusy() { + // true when loading data, deleting a key, etc. + return this._busyCount > 0; + }, + + /** + * Whether the "remove selected" button is disabled. + * + * @type {boolean} + */ + _removeSelectedDisabled: true, + + /** + * Whether the "remove all" button is disabled. + * + * @type {boolean} + */ + _removeAllDisabled: true, + + async _deleteSelectedKeys() { + this._showError(null); + this._withBusy(async () => { + 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) { + const provider = await TorProviderBuilder.build(); + try { + // Remove in reverse index order to avoid issues caused by index + // changes. + for (let i = indexesToDelete.length - 1; i >= 0; --i) { + await this._deleteOneKey(provider, indexesToDelete[i]); + } + // If successful and the user focus is still on the buttons move focus + // to the table with the updated state. We do this before calling + // _updateButtonState and potentially making the buttons disabled. + if ( + this._removeButton.contains(document.activeElement) || + this._removeAllButton.contains(document.activeElement) + ) { + this._tree.focus(); + } + } catch (e) { + console.error("Removing a saved key failed", e); + this._showError( + "onion-site-saved-keys-dialog-remove-keys-error-message" + ); + } + } + }); + }, + + async _deleteAllKeys() { + this._tree.view.selection.selectAll(); + await this._deleteSelectedKeys(); + }, + + /** + * Show the given button as being disabled or enabled. + * + * @param {Button} button - The button to change. + * @param {boolean} disable - Whether to show the button as disabled or + * enabled. + */ + _disableButton(button, disable) { + // If we are disabled we show the button as disabled, and we also remove it + // from the tab focus cycle using `tabIndex = -1`. + // This is similar to using the `disabled` attribute, except that + // `tabIndex = -1` still allows the button to be focusable. I.e. not part of + // the focus cycle but can *keep* existing focus when the button becomes + // disabled to avoid loosing focus to the top of the dialog. + // TODO: Replace with moz-button when it handles this for us. See + // tor-browser#43275. + button.classList.toggle("spoof-button-disabled", disable); + button.tabIndex = disable ? -1 : 0; + if (disable) { + this._removeButton.setAttribute("aria-disabled", "true"); + } else { + this._removeButton.removeAttribute("aria-disabled"); + } + }, + + _updateButtonsState() { + const haveSelection = this._tree.view.selection.getRangeCount() > 0; + this._removeSelectedDisabled = this._isBusy || !haveSelection; + this._removeAllDisabled = this._isBusy || this.rowCount === 0; + this._disableButton(this._removeButton, this._removeSelectedDisabled); + this._disableButton(this._removeAllButton, this._removeAllDisabled); + }, + + // Private functions. + _onLoad() { + document.mozSubdialogReady = this._init(); + }, + + _init() { + this._populateXUL(); + window.addEventListener("keypress", this._onWindowKeyPress.bind(this)); + this._loadSavedKeys(); + }, + + _populateXUL() { + this._errorMessageContainer = document.getElementById( + "onionservices-savedkeys-errorContainer" + ); + this._errorMessageEl = document.getElementById( + "onionservices-savedkeys-errorMessage" + ); + this._removeButton = document.getElementById( + "onionservices-savedkeys-remove" + ); + this._removeButton.addEventListener("click", () => { + if (this._removeSelectedDisabled) { + return; + } + this._deleteSelectedKeys(); + }); + this._removeAllButton = document.getElementById( + "onionservices-savedkeys-removeall" + ); + this._removeAllButton.addEventListener("click", () => { + if (this._removeAllDisabled) { + return; + } + this._deleteAllKeys(); + }); + + this._tree = document.getElementById("onionservices-savedkeys-tree"); + this._tree.addEventListener("select", () => { + this._updateButtonsState(); + }); + }, + + async _loadSavedKeys() { + this._showError(null); + this._withBusy(async () => { + try { + this._tree.view = this; + + const provider = await TorProviderBuilder.build(); + const keyInfoList = await provider.onionAuthViewKeys(); + if (keyInfoList) { + // Filter out temporary keys. + this._keyInfoList = keyInfoList.filter(aKeyInfo => + aKeyInfo.flags?.includes("Permanent") + ); + // Sort by the .onion address. + this._keyInfoList.sort((aObj1, aObj2) => { + const hsAddr1 = aObj1.address.toLowerCase(); + const hsAddr2 = aObj2.address.toLowerCase(); + if (hsAddr1 < hsAddr2) { + return -1; + } + return hsAddr1 > hsAddr2 ? 1 : 0; + }); + } + + // Render the tree content. + this._tree.rowCountChanged(0, this.rowCount); + } catch (e) { + console.error("Failed to load keys", e); + this._showError( + "onion-site-saved-keys-dialog-fetch-keys-error-message" + ); + } + }); + }, + + // This method may throw; callers should catch errors. + async _deleteOneKey(provider, aIndex) { + const keyInfoObj = this._keyInfoList[aIndex]; + await provider.onionAuthRemove(keyInfoObj.address); + this._tree.view.selection.clearRange(aIndex, aIndex); + this._keyInfoList.splice(aIndex, 1); + this._tree.rowCountChanged(aIndex + 1, -1); + }, + + async _withBusy(func) { + this._busyCount++; + if (this._busyCount === 1) { + this._updateButtonsState(); + } + try { + await func(); + } finally { + this._busyCount--; + if (this._busyCount === 0) { + this._updateButtonsState(); + } + } + }, + + _onWindowKeyPress(event) { + if (this._isBusy) { + return; + } + if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) { + window.close(); + } else if (event.keyCode === KeyEvent.DOM_VK_DELETE) { + this._deleteSelectedKeys(); + } + }, + + /** + * Show an error, or clear it. + * + * @param {?string} messageId - The l10n ID of the message to show, or null to + * clear it. + */ + _showError(messageId) { + this._errorMessageContainer.classList.toggle("show-error", !!messageId); + if (messageId) { + document.l10n.setAttributes(this._errorMessageEl, messageId); + } else { + // Clean up. + this._errorMessageEl.removeAttribute("data-l10n-id"); + this._errorMessageEl.textContent = ""; + } + }, + + // XUL tree widget view implementation. + get rowCount() { + return this._keyInfoList?.length ?? 0; + }, + + getCellText(aRow, aCol) { + if (this._keyInfoList && aRow < this._keyInfoList.length) { + const keyInfo = this._keyInfoList[aRow]; + if (aCol.id.endsWith("-siteCol")) { + return keyInfo.address; + } else if (aCol.id.endsWith("-keyCol")) { + // keyType is always "x25519", so do not show it. + return keyInfo.keyBlob; + } + } + return ""; + }, + + 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 @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<!-- Copyright (c) 2020, The Tor Project, Inc. --> + +<?csp default-src chrome: ?> + +<window + id="onionservices-savedkeys-dialog" + windowtype="OnionServices:SavedKeys" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="onion-site-saved-keys-dialog-title" +> + <linkset> + <html:link rel="stylesheet" href="chrome://global/skin/global.css" /> + <html:link + rel="stylesheet" + href="chrome://browser/skin/preferences/preferences.css" + /> + <html:link + rel="stylesheet" + href="chrome://browser/content/onionservices/authPreferences.css" + /> + + <html:link rel="localization" href="toolkit/global/tor-browser.ftl" /> + </linkset> + + <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" + data-l10n-id="onion-site-saved-keys-dialog-intro" + /> + <separator class="thin" /> + <tree id="onionservices-savedkeys-tree" flex="1" hidecolumnpicker="true"> + <treecols> + <treecol + id="onionservices-savedkeys-siteCol" + flex="1" + persist="width" + data-l10n-id="onion-site-saved-keys-dialog-table-header-site" + /> + <splitter class="tree-splitter" /> + <treecol + id="onionservices-savedkeys-keyCol" + flex="1" + persist="width" + data-l10n-id="onion-site-saved-keys-dialog-table-header-key" + /> + </treecols> + <treechildren /> + </tree> + <hbox + id="onionservices-savedkeys-errorContainer" + align="center" + role="alert" + > + <image id="onionservices-savedkeys-errorIcon" /> + <description id="onionservices-savedkeys-errorMessage" flex="1" /> + </hbox> + <separator class="thin" /> + <hbox id="onionservices-savedkeys-buttons"> + <html:button + id="onionservices-savedkeys-remove" + data-l10n-id="onion-site-saved-keys-dialog-remove-button" + ></html:button> + <html:button + id="onionservices-savedkeys-removeall" + data-l10n-id="onion-site-saved-keys-dialog-remove-all-button" + ></html:button> + </hbox> + </vbox> +</window> diff --git a/browser/components/onionservices/jar.mn b/browser/components/onionservices/jar.mn @@ -0,0 +1,7 @@ +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/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 @@ -0,0 +1 @@ +JAR_MANIFESTS += ["jar.mn"] diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml @@ -48,6 +48,7 @@ <link rel="stylesheet" href="chrome://browser/content/securitylevel/securityLevelPreferences.css" /> <link rel="stylesheet" href="chrome://browser/content/torpreferences/torPreferences.css" /> + <link rel="stylesheet" href="chrome://browser/content/onionservices/authPreferences.css" /> <link rel="localization" href="branding/brand.ftl"/> <link rel="localization" href="browser/browser.ftl"/> diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml @@ -562,6 +562,8 @@ <html:setting-group groupid="passwords" hidden="true" data-category="panePrivacy" /> +#include ../onionservices/content/authPreferences.inc.xhtml + <groupbox id="paymentsGroupBox" data-category="panePrivacy" data-subcategory="payment-methods-autofill" hidden="true"> diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js @@ -62,6 +62,12 @@ ChromeUtils.defineLazyGetter(lazy, "gParentalControlsService", () => : null ); +XPCOMUtils.defineLazyScriptGetter( + this, + ["OnionServicesAuthPreferences"], + "chrome://browser/content/onionservices/authPreferences.js" +); + // TODO: module import via ChromeUtils.defineModuleGetter XPCOMUtils.defineLazyScriptGetter( this, @@ -3877,6 +3883,7 @@ var gPrivacyPane = { this._initTrackingProtectionExtensionControl(); this._ensureTrackingProtectionExceptionListMigration(); this._initProfilesInfo(); + OnionServicesAuthPreferences.init(); this._initSecurityLevel(); Preferences.get("privacy.trackingprotection.enabled").on( diff --git a/browser/themes/shared/notification-icons.css b/browser/themes/shared/notification-icons.css @@ -137,6 +137,8 @@ list-style-image: url(chrome://browser/skin/notification-icons/persistent-storage.svg); } +.popup-notification-icon[popupid="tor-clientauth"], +.tor-clientauth-icon, #password-notification-icon { list-style-image: url(chrome://browser/skin/login.svg); diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp @@ -3666,6 +3666,8 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI, } } else { // Errors requiring simple formatting + bool isOnionAuthError = false; + bool isOnionError = false; switch (aError) { case NS_ERROR_MALFORMED_URI: // URI is malformed @@ -3759,9 +3761,61 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI, case NS_ERROR_BASIC_HTTP_AUTH_DISABLED: error = "basicHttpAuthDisabled"; break; + case NS_ERROR_TOR_ONION_SVC_NOT_FOUND: + error = "onionServices.descNotFound"; + isOnionError = true; + break; + case NS_ERROR_TOR_ONION_SVC_IS_INVALID: + error = "onionServices.descInvalid"; + isOnionError = true; + break; + case NS_ERROR_TOR_ONION_SVC_INTRO_FAILED: + error = "onionServices.introFailed"; + isOnionError = true; + break; + case NS_ERROR_TOR_ONION_SVC_REND_FAILED: + error = "onionServices.rendezvousFailed"; + isOnionError = true; + break; + case NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH: + error = "onionServices.clientAuthMissing"; + isOnionError = true; + isOnionAuthError = true; + break; + case NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH: + error = "onionServices.clientAuthIncorrect"; + isOnionError = true; + isOnionAuthError = true; + break; + case NS_ERROR_TOR_ONION_SVC_BAD_ADDRESS: + error = "onionServices.badAddress"; + isOnionError = true; + break; + case NS_ERROR_TOR_ONION_SVC_INTRO_TIMEDOUT: + error = "onionServices.introTimedOut"; + isOnionError = true; + 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:neterror with a style emulating about:blank while the + // Tor client auth prompt is open. Do not use about:blank directly: it + // will mess with the failed channel information persistence! + cssClass.AssignLiteral("onionAuthPrompt"); + } + if (isOnionError) { + // DisplayLoadError requires a non-empty messageStr to proceed and call + // LoadErrorPage. We use a blank space. + if (messageStr.IsEmpty()) { + messageStr.AssignLiteral(u" "); + } + } } nsresult delegateErrorCode = aError; @@ -6423,6 +6477,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; @@ -8202,6 +8257,35 @@ nsresult nsDocShell::CreateDocumentViewer(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 @@ -4111,6 +4111,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, const uint64_t& aBrowserId) { diff --git a/dom/ipc/BrowserParent.h b/dom/ipc/BrowserParent.h @@ -742,6 +742,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 @@ -567,6 +567,15 @@ parent: */ async RequestPointerLock() returns (nsCString error); + /** + * 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); + both: /** * informs that a pointer lock has released. diff --git a/eslint-file-globals.config.mjs b/eslint-file-globals.config.mjs @@ -138,6 +138,7 @@ export default [ "browser/components/torcircuit/content/torCircuitPanel.js", "toolkit/components/torconnect/content/torConnectTitlebarStatus.js", "toolkit/components/torconnect/content/torConnectUrlbarButton.js", + "browser/components/onionservices/content/authPrompt.js", ], languageOptions: { globals: mozilla.environments["browser-window"].globals, diff --git a/js/xpconnect/src/xpc.msg b/js/xpconnect/src/xpc.msg @@ -261,6 +261,16 @@ XPC_MSG_DEF(NS_ERROR_SOCIALTRACKING_URI , "The URI is social track XPC_MSG_DEF(NS_ERROR_EMAILTRACKING_URI , "The URI is email tracking") XPC_MSG_DEF(NS_ERROR_HARMFULADDON_URI , "The URI is not available for add-ons") +/* 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 @@ -220,6 +220,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 @@ -958,6 +958,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/content/.eslintrc.mjs b/toolkit/content/.eslintrc.mjs @@ -9,7 +9,7 @@ export default [ rules: { // XXX Bug 1358949 - This should be reduced down - probably to 20 or to // be removed & synced with the mozilla/recommended value. - complexity: ["error", 49], + complexity: ["error", 51], }, }, { diff --git a/toolkit/content/aboutNetError.html b/toolkit/content/aboutNetError.html @@ -21,6 +21,7 @@ <link rel="localization" href="toolkit/neterror/certError.ftl" /> <link rel="localization" href="toolkit/neterror/netError.ftl" /> <link rel="localization" href="toolkit/neterror/nsserrors.ftl" /> + <link rel="localization" href="toolkit/global/tor-browser.ftl" /> </head> <body> <div class="container"> diff --git a/toolkit/content/aboutNetError.mjs b/toolkit/content/aboutNetError.mjs @@ -290,6 +290,92 @@ function setResponseStatus(shortDesc) { } } +/** + * Initialize the onion error page. + * + * @return {boolean} Whether the page was initialized as an onion error page. + */ +function initOnionError() { + const docTitle = document.querySelector("title"); + + if (getCSSClass() === "onionAuthPrompt") { + // Only showing the authorization prompt. The page will be blank. + document.l10n.setAttributes(docTitle, "onion-neterror-authorization-title"); + return true; + } + + const onionErrors = { + // Tor SOCKS error 0xF0: + "onionServices.descNotFound": { + headerId: "onion-neterror-not-found-header", + descriptionId: "onion-neterror-not-found-description", + }, + // Tor SOCKS error 0xF1: + "onionServices.descInvalid": { + headerId: "onion-neterror-unreachable-header", + descriptionId: "onion-neterror-unreachable-description", + }, + // Tor SOCKS error 0xF2: + "onionServices.introFailed": { + headerId: "onion-neterror-disconnected-header", + descriptionId: "onion-neterror-disconnected-description", + }, + // Tor SOCKS error 0xF3: + "onionServices.rendezvousFailed": { + headerId: "onion-neterror-connection-failed-header", + descriptionId: "onion-neterror-connection-failed-description", + }, + // Tor SOCKS error 0xF4: + "onionServices.clientAuthMissing": { + headerId: "onion-neterror-missing-authentication-header", + descriptionId: "onion-neterror-missing-authentication-description", + }, + // Tor SOCKS error 0xF5: + "onionServices.clientAuthIncorrect": { + headerId: "onion-neterror-incorrect-authentication-header", + descriptionId: "onion-neterror-incorrect-authetication-description", + }, + // Tor SOCKS error 0xF6: + "onionServices.badAddress": { + headerId: "onion-neterror-invalid-address-header", + descriptionId: "onion-neterror-invalid-address-description", + }, + // Tor SOCKS error 0xF7: + "onionServices.introTimedOut": { + headerId: "onion-neterror-timed-out-header", + descriptionId: "onion-neterror-timed-out-description", + }, + }; + + if (!Object.hasOwn(onionErrors, gErrorCode)) { + return false; + } + + document.body.classList.add("onion-error"); + + document.l10n.setAttributes(docTitle, "onion-neterror-page-title"); + document.l10n.setAttributes( + document.querySelector(".title-text"), + onionErrors[gErrorCode].headerId + ); + document.l10n.setAttributes( + document.getElementById("errorShortDesc"), + onionErrors[gErrorCode].descriptionId + ); + + const tryAgain = document.getElementById("netErrorButtonContainer"); + tryAgain.hidden = false; + + const learnMore = document.getElementById("learnMoreContainer"); + learnMore.hidden = false; + const learnMoreLink = document.getElementById("learnMoreLink"); + learnMoreLink.href = "about:manual#onion-services"; + + setFocus("#netErrorButtonContainer > .try-again"); + + return true; +} + // Returns pageTitleId, bodyTitle, bodyTitleId, and longDesc as an object async function initTitleAndBodyIds(baseURL, isTRROnlyFailure) { let bodyTitle = document.querySelector(".title-text"); @@ -465,6 +551,10 @@ async function initPage() { document.body.classList.add("neterror"); + if (initOnionError()) { + return; + } + const tryAgain = document.getElementById("netErrorButtonContainer"); tryAgain.hidden = false; const learnMoreLink = document.getElementById("learnMoreLink"); diff --git a/toolkit/modules/PopupNotifications.sys.mjs b/toolkit/modules/PopupNotifications.sys.mjs @@ -442,6 +442,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. * If null, the notification will have a default "OK" action button * that can be used to dismiss the popup and secondaryActions will be ignored. * @param secondaryActions @@ -1970,6 +1972,10 @@ PopupNotifications.prototype = { this._dismiss(); return; } + + if (action.leaveOpen) { + return; + } } this._remove(notification); diff --git a/toolkit/themes/shared/aboutNetError.css b/toolkit/themes/shared/aboutNetError.css @@ -8,6 +8,13 @@ body { --warning-color: #ffa436; } +/** + * Blank page whilst we show the prompt. + */ +body.onionAuthPrompt { + display: none !important; +} + @media (prefers-color-scheme: dark) { body { --warning-color: #ffbd4f; @@ -39,6 +46,13 @@ body.certerror .title { fill: var(--warning-color); } +body.onion-error .title { + background-image: url("chrome://global/skin/icons/onion-warning.svg"); + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: var(--warning-color); +} + body.blocked .title { background-image: url("chrome://global/skin/icons/blocked.svg"); } diff --git a/xpcom/base/ErrorList.py b/xpcom/base/ErrorList.py @@ -91,6 +91,7 @@ modules["ERRORRESULT"] = Mod(43) modules["WIN32"] = Mod(44) modules["WDBA"] = Mod(45) modules["DOM_QM"] = Mod(46) +modules["TOR"] = Mod(47) # NS_ERROR_MODULE_GENERAL should be used by modules that do not # care if return code values overlap. Callers of methods that @@ -1253,6 +1254,29 @@ with modules["WDBA"]: with modules["DOM_QM"]: errors["NS_ERROR_DOM_QM_CLIENT_INIT_ORIGIN_UNINITIALIZED"] = FAILURE(1) + +# ======================================================================= +# 47: 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 # =======================================================================