This is an automated email from the git hooks/post-receive script.
richard pushed a commit to branch tor-browser-91.10.0esr-11.0-1 in repository tor-browser.
commit 9c9d344d2aaf518f54c6bc5a6b191ff7b5ca92d3 Author: Richard Pospesel richard@torproject.org AuthorDate: Wed Apr 28 23:09:34 2021 -0500
Bug 27476: Implement about:torconnect captive portal within Tor Browser
- implements new about:torconnect page as tor-launcher replacement - adds tor connection status to url bar and tweaks UX when not online - adds new torconnect component to browser - tor process management functionality remains implemented in tor-launcher through the TorProtocolService module - the onion pattern from about:tor migrated to an .inc.xhtml file now used by both about:tor and about:torconnect - various design tweaks and resusability fixes to onion pattern - adds warning/error box to about:preferences#tor when not connected to tor - explicitly allows about:torconnect URIs to ignore Resist Fingerprinting (RFP) - various tweaks to info-pages.inc.css for about:torconnect (also affects other firefox info pages) --- browser/actors/NetErrorParent.jsm | 8 + browser/base/content/browser-siteIdentity.js | 5 +- browser/base/content/browser.js | 66 ++- browser/base/content/browser.xhtml | 2 + browser/base/content/certerror/aboutNetError.js | 12 +- browser/base/content/navigator-toolbox.inc.xhtml | 1 + browser/base/content/utilityOverlay.js | 8 + browser/branding/alpha/content/jar.mn | 2 + browser/branding/alpha/content/tor-styles.css | 13 + browser/branding/nightly/content/jar.mn | 1 + browser/branding/nightly/content/tor-styles.css | 13 + browser/branding/official/content/jar.mn | 1 + browser/branding/official/content/tor-styles.css | 14 + browser/branding/tor-styles.inc.css | 87 ++++ browser/components/BrowserGlue.jsm | 37 +- browser/components/about/AboutRedirector.cpp | 4 + browser/components/about/components.conf | 1 + browser/components/moz.build | 1 + .../onionservices/HttpsEverywhereControl.jsm | 17 +- browser/components/sessionstore/SessionStore.jsm | 4 + browser/components/torconnect/TorConnectChild.jsm | 9 + browser/components/torconnect/TorConnectParent.jsm | 144 ++++++ .../torconnect/content/aboutTorConnect.css | 180 ++++++++ .../torconnect/content/aboutTorConnect.js | 302 ++++++++++++ .../torconnect/content/aboutTorConnect.xhtml | 45 ++ .../components/torconnect/content/onion-slash.svg | 5 + browser/components/torconnect/content/onion.svg | 4 + .../torconnect/content/torBootstrapUrlbar.js | 93 ++++ .../torconnect/content/torconnect-urlbar.css | 57 +++ .../torconnect/content/torconnect-urlbar.inc.xhtml | 10 + browser/components/torconnect/jar.mn | 7 + browser/components/torconnect/moz.build | 6 + .../components/torpreferences/content/torPane.js | 123 +++++ .../torpreferences/content/torPane.xhtml | 34 ++ .../torpreferences/content/torPreferences.css | 112 +++++ browser/components/urlbar/UrlbarInput.jsm | 32 ++ browser/modules/TorConnect.jsm | 506 +++++++++++++++++++++ browser/modules/TorProcessService.jsm | 12 + browser/modules/TorProtocolService.jsm | 58 ++- browser/modules/TorStrings.jsm | 80 ++++ browser/modules/moz.build | 2 + .../shared/identity-block/identity-block.inc.css | 7 +- browser/themes/shared/jar.inc.mn | 2 + browser/themes/shared/onionPattern.css | 31 ++ browser/themes/shared/onionPattern.inc.xhtml | 12 + browser/themes/shared/onionPattern.svg | 22 + browser/themes/shared/urlbar-searchbar.inc.css | 2 + dom/base/Document.cpp | 51 ++- dom/base/nsGlobalWindowOuter.cpp | 2 + .../processsingleton/MainProcessSingleton.jsm | 5 + toolkit/modules/AsyncPrefs.jsm | 1 + toolkit/modules/RemotePageAccessManager.jsm | 16 + toolkit/mozapps/update/UpdateService.jsm | 68 ++- .../lib/environments/browser-window.js | 4 + 54 files changed, 2299 insertions(+), 42 deletions(-)
diff --git a/browser/actors/NetErrorParent.jsm b/browser/actors/NetErrorParent.jsm index 3472c68f664a4..13afbbbfd4a8b 100644 --- a/browser/actors/NetErrorParent.jsm +++ b/browser/actors/NetErrorParent.jsm @@ -21,6 +21,10 @@ const { TelemetryController } = ChromeUtils.import( "resource://gre/modules/TelemetryController.jsm" );
+const { TorConnect } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" +); + const PREF_SSL_IMPACT_ROOTS = [ "security.tls.version.", "security.ssl3.", @@ -350,6 +354,10 @@ class NetErrorParent extends JSWindowActorParent { break; } } + break; + case "ShouldShowTorConnect": + return TorConnect.shouldShowTorConnect; } + return undefined; } } diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js index 1b2c7bcb22cf5..6dcfbd1e491fc 100644 --- a/browser/base/content/browser-siteIdentity.js +++ b/browser/base/content/browser-siteIdentity.js @@ -57,7 +57,10 @@ var gIdentityHandler = { * RegExp used to decide if an about url should be shown as being part of * the browser UI. */ - _secureInternalPages: (AppConstants.TOR_BROWSER_UPDATE ? /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion|tor|tbupdate)(?:[?#]|$)/i : /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion|tor)(?:[?#]|$)/i), + _secureInternalPages: (AppConstants.TOR_BROWSER_UPDATE ? + /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion|tor|torconnect|tbupdate)(?:[?#]|$)/i : + /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion|tor|torconnect)(?:[?#]|$)/i), +
/** * Whether the established HTTPS connection is considered "broken". diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index ac429023ab35d..16123f02ff49b 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -80,6 +80,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { TabModalPrompt: "chrome://global/content/tabprompts.jsm", TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm", TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm", + TorConnect: "resource:///modules/TorConnect.jsm", Translation: "resource:///modules/translation/TranslationParent.jsm", OnionAliasStore: "resource:///modules/OnionAliasStore.jsm", UITour: "resource:///modules/UITour.jsm", @@ -645,6 +646,7 @@ var gPageIcons = {
var gInitialPages = [ "about:tor", + "about:torconnect", "about:blank", "about:newtab", "about:home", @@ -1859,6 +1861,8 @@ var gBrowserInit = { }
this._loadHandled = true; + + TorBootstrapUrlbar.init(); },
_cancelDelayedStartup() { @@ -2409,32 +2413,48 @@ var gBrowserInit = {
let defaultArgs = BrowserHandler.defaultArgs;
- // If the given URI is different from the homepage, we want to load it. - if (uri != defaultArgs) { - AboutNewTab.noteNonDefaultStartup(); + // figure out which URI to actually load (or a Promise to get the uri) + uri = ((uri) => { + // If the given URI is different from the homepage, we want to load it. + if (uri != defaultArgs) { + AboutNewTab.noteNonDefaultStartup(); + + if (uri instanceof Ci.nsIArray) { + // Transform the nsIArray of nsISupportsString's into a JS Array of + // JS strings. + return Array.from( + uri.enumerate(Ci.nsISupportsString), + supportStr => supportStr.data + ); + } else if (uri instanceof Ci.nsISupportsString) { + return uri.data; + } + return uri; + }
- if (uri instanceof Ci.nsIArray) { - // Transform the nsIArray of nsISupportsString's into a JS Array of - // JS strings. - return Array.from( - uri.enumerate(Ci.nsISupportsString), - supportStr => supportStr.data - ); - } else if (uri instanceof Ci.nsISupportsString) { - return uri.data; + // The URI appears to be the the homepage. We want to load it only if + // session restore isn't about to override the homepage. + let willOverride = SessionStartup.willOverrideHomepage; + if (typeof willOverride == "boolean") { + return willOverride ? null : uri; } - return uri; - } + return willOverride.then(willOverrideHomepage => + willOverrideHomepage ? null : uri + ); + })(uri); + + // if using TorConnect, convert these uris to redirects + if (TorConnect.shouldShowTorConnect) { + return Promise.resolve(uri).then((uri) => { + if (uri == null) { + uri = []; + }
- // The URI appears to be the the homepage. We want to load it only if - // session restore isn't about to override the homepage. - let willOverride = SessionStartup.willOverrideHomepage; - if (typeof willOverride == "boolean") { - return willOverride ? null : uri; + uri = TorConnect.getURIsToLoad(uri); + return uri; + }); } - return willOverride.then(willOverrideHomepage => - willOverrideHomepage ? null : uri - ); + return uri; })()); },
@@ -2501,6 +2521,8 @@ var gBrowserInit = {
OnionAuthPrompt.uninit();
+ TorBootstrapUrlbar.uninit(); + gAccessibilityServiceIndicator.uninit();
if (gToolbarKeyNavEnabled) { diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml index 65445a0991488..394a464140186 100644 --- a/browser/base/content/browser.xhtml +++ b/browser/base/content/browser.xhtml @@ -10,6 +10,7 @@ override rules using selectors with the same specificity. This applies to both "content" and "skin" packages, which bug 1385444 will unify later. --> <?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://branding/content/tor-styles.css" type="text/css"?>
<!-- While these stylesheets are defined in Toolkit, they are only used in the main browser window, so we can load them here. Bug 1474241 is on file to @@ -113,6 +114,7 @@ Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this); Services.scriptloader.loadSubScript("chrome://torbutton/content/tor-circuit-display.js", this); Services.scriptloader.loadSubScript("chrome://torbutton/content/torbutton.js", this); + Services.scriptloader.loadSubScript("chrome://browser/content/torconnect/torBootstrapUrlbar.js", this);
window.onload = gBrowserInit.onLoad.bind(gBrowserInit); window.onunload = gBrowserInit.onUnload.bind(gBrowserInit); diff --git a/browser/base/content/certerror/aboutNetError.js b/browser/base/content/certerror/aboutNetError.js index e5b223025a8b3..60f602ba65303 100644 --- a/browser/base/content/certerror/aboutNetError.js +++ b/browser/base/content/certerror/aboutNetError.js @@ -240,7 +240,7 @@ function setErrorPageStrings(err) { document.l10n.setAttributes(titleElement, title); }
-function initPage() { +async function initPage() { // We show an offline support page in case of a system-wide error, // when a user cannot connect to the internet and access the SUMO website. // For example, clock error, which causes certerrors across the web or @@ -259,6 +259,16 @@ function initPage() { }
var err = getErrorCode(); + + // proxyConnectFailure because no-tor running daemon would return this error + if ( + (err === "proxyConnectFailure") && + (await RPMSendQuery("ShouldShowTorConnect")) + ) { + // pass orginal destination as redirect param + const encodedRedirect = encodeURIComponent(document.location.href); + document.location.replace(`about:torconnect?redirect=${encodedRedirect}`); + } // List of error pages with an illustration. let illustratedErrors = [ "malformedURI", diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml index 1aad36ab3bfc7..b881492864ae9 100644 --- a/browser/base/content/navigator-toolbox.inc.xhtml +++ b/browser/base/content/navigator-toolbox.inc.xhtml @@ -331,6 +331,7 @@ data-l10n-id="urlbar-go-button"/> <hbox id="page-action-buttons" context="pageActionContextMenu"> <toolbartabstop/> +#include ../../components/torconnect/content/torconnect-urlbar.inc.xhtml <hbox id="contextual-feature-recommendation" role="button" hidden="true"> <hbox id="cfr-label-container"> <label id="cfr-label"/> diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js index 7b6dc5b921de6..298c8d85f5609 100644 --- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -21,6 +21,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { ExtensionSettingsStore: "resource://gre/modules/ExtensionSettingsStore.jsm", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", ShellService: "resource:///modules/ShellService.jsm", + TorConnect: "resource:///modules/TorConnect.jsm", });
XPCOMUtils.defineLazyGetter(this, "ReferrerInfo", () => @@ -258,6 +259,13 @@ function openUILinkIn( aPostData, aReferrerInfo ) { + + // make sure users are not faced with the scary red 'tor isn't working' screen + // if they navigate to about:tor before bootstrapped + if (url === "about:tor" && TorConnect.shouldShowTorConnect) { + url = `about:torconnect?redirect=${encodeURIComponent("about:tor")}`; + } + var params;
if (arguments.length == 3 && typeof arguments[2] == "object") { diff --git a/browser/branding/alpha/content/jar.mn b/browser/branding/alpha/content/jar.mn index 6db01f74fd204..93ff6ecf736b3 100644 --- a/browser/branding/alpha/content/jar.mn +++ b/browser/branding/alpha/content/jar.mn @@ -18,4 +18,6 @@ browser.jar: content/branding/icon128.png (../default128.png) content/branding/icon256.png (../default256.png) content/branding/icon512.png (../default512.png) + content/branding/identity-icons-brand.svg content/branding/aboutDialog.css +* content/branding/tor-styles.css diff --git a/browser/branding/alpha/content/tor-styles.css b/browser/branding/alpha/content/tor-styles.css new file mode 100644 index 0000000000000..14c1915ef871d --- /dev/null +++ b/browser/branding/alpha/content/tor-styles.css @@ -0,0 +1,13 @@ +%include ../../tor-styles.inc.css + +/* default theme*/ +:root, +/* light theme*/ +:root:-moz-lwtheme-darktext { + --tor-branding-color: var(--teal-70); +} + +/* dark theme */ +:root:-moz-lwtheme-brighttext { + --tor-branding-color: var(--teal-60); +} \ No newline at end of file diff --git a/browser/branding/nightly/content/jar.mn b/browser/branding/nightly/content/jar.mn index 713d4c4924756..93ff6ecf736b3 100644 --- a/browser/branding/nightly/content/jar.mn +++ b/browser/branding/nightly/content/jar.mn @@ -20,3 +20,4 @@ browser.jar: content/branding/icon512.png (../default512.png) content/branding/identity-icons-brand.svg content/branding/aboutDialog.css +* content/branding/tor-styles.css diff --git a/browser/branding/nightly/content/tor-styles.css b/browser/branding/nightly/content/tor-styles.css new file mode 100644 index 0000000000000..52e1761e54598 --- /dev/null +++ b/browser/branding/nightly/content/tor-styles.css @@ -0,0 +1,13 @@ +%include ../../tor-styles.inc.css + +/* default theme*/ +:root, +/* light theme*/ +:root:-moz-lwtheme-darktext { + --tor-branding-color: var(--blue-60); +} + +/* dark theme */ +:root:-moz-lwtheme-brighttext { + --tor-branding-color: var(--blue-40); +} \ No newline at end of file diff --git a/browser/branding/official/content/jar.mn b/browser/branding/official/content/jar.mn index 713d4c4924756..93ff6ecf736b3 100644 --- a/browser/branding/official/content/jar.mn +++ b/browser/branding/official/content/jar.mn @@ -20,3 +20,4 @@ browser.jar: content/branding/icon512.png (../default512.png) content/branding/identity-icons-brand.svg content/branding/aboutDialog.css +* content/branding/tor-styles.css diff --git a/browser/branding/official/content/tor-styles.css b/browser/branding/official/content/tor-styles.css new file mode 100644 index 0000000000000..e4ccb5c767a90 --- /dev/null +++ b/browser/branding/official/content/tor-styles.css @@ -0,0 +1,14 @@ +%include ../../tor-styles.inc.css + +/* default theme*/ +:root, +/* light theme*/ +:root:-moz-lwtheme-darktext { + --tor-branding-color: var(--purple-60); +} + +/* dark theme */ +:root:-moz-lwtheme-brighttext { + --tor-branding-color: var(--purple-30); +} + diff --git a/browser/branding/tor-styles.inc.css b/browser/branding/tor-styles.inc.css new file mode 100644 index 0000000000000..55dc9b6238b3a --- /dev/null +++ b/browser/branding/tor-styles.inc.css @@ -0,0 +1,87 @@ +:root { + /* photon colors, not all of them are available for whatever reason + in firefox, so here they are */ + + --magenta-50: #ff1ad9; + --magenta-60: #ed00b5; + --magenta-70: #b5007f; + --magenta-80: #7d004f; + --magenta-90: #440027; + + --purple-30: #c069ff; + --purple-40: #ad3bff; + --purple-50: #9400ff; + --purple-60: #8000d7; + --purple-70: #6200a4; + --purple-80: #440071; + --purple-90: #25003e; + + --blue-40: #45a1ff; + --blue-50: #0a84ff; + --blue-50-a30: rgba(10, 132, 255, 0.3); + --blue-60: #0060df; + --blue-70: #003eaa; + --blue-80: #002275; + --blue-90: #000f40; + + --teal-50: #00feff; + --teal-60: #00c8d7; + --teal-70: #008ea4; + --teal-80: #005a71; + --teal-90: #002d3e; + + --green-50: #30e60b; + --green-60: #12bc00; + --green-70: #058b00; + --green-80: #006504; + --green-90: #003706; + + --yellow-50: #ffe900; + --yellow-60: #d7b600; + --yellow-70: #a47f00; + --yellow-80: #715100; + --yellow-90: #3e2800; + + --red-50: #ff0039; + --red-60: #d70022; + --red-70: #a4000f; + --red-80: #5a0002; + --red-90: #3e0200; + + --orange-50: #ff9400; + --orange-60: #d76e00; + --orange-70: #a44900; + --orange-80: #712b00; + --orange-90: #3e1300; + + --grey-10: #f9f9fa; + --grey-10-a10: rgba(249, 249, 250, 0.1); + --grey-10-a20: rgba(249, 249, 250, 0.2); + --grey-10-a40: rgba(249, 249, 250, 0.4); + --grey-10-a60: rgba(249, 249, 250, 0.6); + --grey-10-a80: rgba(249, 249, 250, 0.8); + --grey-20: #ededf0; + --grey-30: #d7d7db; + --grey-40: #b1b1b3; + --grey-50: #737373; + --grey-60: #4a4a4f; + --grey-70: #38383d; + --grey-80: #2a2a2e; + --grey-90: #0c0c0d; + --grey-90-a05: rgba(12, 12, 13, 0.05); + --grey-90-a10: rgba(12, 12, 13, 0.1); + --grey-90-a20: rgba(12, 12, 13, 0.2); + --grey-90-a30: rgba(12, 12, 13, 0.3); + --grey-90-a40: rgba(12, 12, 13, 0.4); + --grey-90-a50: rgba(12, 12, 13, 0.5); + --grey-90-a60: rgba(12, 12, 13, 0.6); + --grey-90-a70: rgba(12, 12, 13, 0.7); + --grey-90-a80: rgba(12, 12, 13, 0.8); + --grey-90-a90: rgba(12, 12, 13, 0.9); + + --ink-70: #363959; + --ink-80: #202340; + --ink-90: #0f1126; + + --white-100: #ffffff; +} \ No newline at end of file diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index 62ae1f5595635..e2824bffdf070 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -725,6 +725,20 @@ let JSWINDOWACTORS = { allFrames: true, },
+ TorConnect: { + parent: { + moduleURI: "resource:///modules/TorConnectParent.jsm", + }, + child: { + moduleURI: "resource:///modules/TorConnectChild.jsm", + events: { + DOMWindowCreated: {}, + }, + }, + + matches: ["about:torconnect","about:torconnect?*"], + }, + Translation: { parent: { moduleURI: "resource:///modules/translation/TranslationParent.jsm", @@ -2522,7 +2536,28 @@ BrowserGlue.prototype = {
{ task: () => { - OnionAliasStore.init(); + const { TorConnect, TorConnectTopics } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" + ); + if (!TorConnect.shouldShowTorConnect) { + // we will take this path when the user is using the legacy tor launcher or + // when Tor Browser didn't launch its own tor. + OnionAliasStore.init(); + } else { + // this path is taken when using about:torconnect, we wait to init + // after we are bootstrapped and connected to tor + const topic = TorConnectTopics.BootstrapComplete; + let bootstrapObserver = { + observe(aSubject, aTopic, aData) { + if (aTopic === topic) { + OnionAliasStore.init(); + // we only need to init once, so remove ourselves as an obvserver + Services.obs.removeObserver(this, topic); + } + } + }; + Services.obs.addObserver(bootstrapObserver, topic); + } }, },
diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp index 323c1b6fb6531..fd828a630c92a 100644 --- a/browser/components/about/AboutRedirector.cpp +++ b/browser/components/about/AboutRedirector.cpp @@ -128,6 +128,10 @@ static const RedirEntry kRedirMap[] = { nsIAboutModule::URI_MUST_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::HIDE_FROM_ABOUTABOUT}, #endif + {"torconnect", "chrome://browser/content/torconnect/aboutTorConnect.xhtml", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT | + nsIAboutModule::HIDE_FROM_ABOUTABOUT}, };
static nsAutoCString GetAboutModuleName(nsIURI* aURI) { diff --git a/browser/components/about/components.conf b/browser/components/about/components.conf index 67f178ee23fff..0916bb75e1d57 100644 --- a/browser/components/about/components.conf +++ b/browser/components/about/components.conf @@ -26,6 +26,7 @@ pages = [ 'robots', 'sessionrestore', 'tabcrashed', + 'torconnect', 'welcome', 'welcomeback', ] diff --git a/browser/components/moz.build b/browser/components/moz.build index 0ea2969e60b06..c304973749122 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -55,6 +55,7 @@ DIRS += [ "syncedtabs", "uitour", "urlbar", + "torconnect", "torpreferences", "translation", ] diff --git a/browser/components/onionservices/HttpsEverywhereControl.jsm b/browser/components/onionservices/HttpsEverywhereControl.jsm index 525ed5233be7c..d673de4cd6e57 100644 --- a/browser/components/onionservices/HttpsEverywhereControl.jsm +++ b/browser/components/onionservices/HttpsEverywhereControl.jsm @@ -41,6 +41,7 @@ const SECUREDROP_TOR_ONION_CHANNEL = { class HttpsEverywhereControl { constructor() { this._extensionMessaging = null; + this._init(); }
async _sendMessage(type, object) { @@ -61,7 +62,6 @@ class HttpsEverywhereControl { * Installs the .tor.onion update channel in https-everywhere */ async installTorOnionUpdateChannel(retries = 5) { - this._init();
// TODO: https-everywhere store is initialized asynchronously, so sending a message // immediately results in a `store.get is undefined` error. @@ -143,5 +143,20 @@ class HttpsEverywhereControl { if (!this._extensionMessaging) { this._extensionMessaging = new ExtensionMessaging(); } + + // update all of the existing https-everywhere channels + setTimeout(async () => { + let pinnedChannels = await this._sendMessage("get_pinned_update_channels"); + for(let channel of pinnedChannels.update_channels) { + this._sendMessage("update_update_channel", channel); + } + + let storedChannels = await this._sendMessage("get_stored_update_channels"); + for(let channel of storedChannels.update_channels) { + this._sendMessage("update_update_channel", channel); + } + }, 0); + + } } diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm index 2150c424d8b83..ddeb923784329 100644 --- a/browser/components/sessionstore/SessionStore.jsm +++ b/browser/components/sessionstore/SessionStore.jsm @@ -186,6 +186,10 @@ ChromeUtils.defineModuleGetter( "resource://gre/modules/sessionstore/SessionHistory.jsm" );
+const { TorProtocolService } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); + XPCOMUtils.defineLazyServiceGetters(this, { gScreenManager: ["@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"], }); diff --git a/browser/components/torconnect/TorConnectChild.jsm b/browser/components/torconnect/TorConnectChild.jsm new file mode 100644 index 0000000000000..bd6dd549f156d --- /dev/null +++ b/browser/components/torconnect/TorConnectChild.jsm @@ -0,0 +1,9 @@ +// Copyright (c) 2021, The Tor Project, Inc. + +var EXPORTED_SYMBOLS = ["TorConnectChild"]; + +const { RemotePageChild } = ChromeUtils.import( + "resource://gre/actors/RemotePageChild.jsm" +); + +class TorConnectChild extends RemotePageChild {} diff --git a/browser/components/torconnect/TorConnectParent.jsm b/browser/components/torconnect/TorConnectParent.jsm new file mode 100644 index 0000000000000..526c588a423ef --- /dev/null +++ b/browser/components/torconnect/TorConnectParent.jsm @@ -0,0 +1,144 @@ +// Copyright (c) 2021, The Tor Project, Inc. + +var EXPORTED_SYMBOLS = ["TorConnectParent"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); +const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" +); + +const TorLauncherPrefs = Object.freeze({ + quickstart: "extensions.torlauncher.quickstart", +}); + +/* +This object is basically a marshalling interface between the TorConnect module +and a particular about:torconnect page +*/ + +class TorConnectParent extends JSWindowActorParent { + constructor(...args) { + super(...args); + + const self = this; + + this.state = { + State: TorConnect.state, + StateChanged: false, + ErrorMessage: TorConnect.errorMessage, + ErrorDetails: TorConnect.errorDetails, + BootstrapProgress: TorConnect.bootstrapProgress, + BootstrapStatus: TorConnect.bootstrapStatus, + ShowCopyLog: TorConnect.logHasWarningOrError, + QuickStartEnabled: Services.prefs.getBoolPref(TorLauncherPrefs.quickstart, false), + }; + + // JSWindowActiveParent derived objects cannot observe directly, so create a member + // object to do our observing for us + // + // This object converts the various lifecycle events from the TorConnect module, and + // maintains a state object which we pass down to our about:torconnect page, which uses + // the state object to update its UI + this.torConnectObserver = { + observe(aSubject, aTopic, aData) { + let obj = aSubject?.wrappedJSObject; + + // update our state struct based on received torconnect topics and forward on + // to aboutTorConnect.js + self.state.StateChanged = false; + switch(aTopic) { + case TorConnectTopics.StateChange: { + self.state.State = obj.state; + self.state.StateChanged = true; + // clear any previous error information if we are bootstrapping + if (self.state.State === TorConnectState.Bootstrapping) { + self.state.ErrorMessage = null; + self.state.ErrorDetails = null; + } + break; + } + case TorConnectTopics.BootstrapProgress: { + self.state.BootstrapProgress = obj.progress; + self.state.BootstrapStatus = obj.status; + self.state.ShowCopyLog = obj.hasWarnings; + break; + } + case TorConnectTopics.BootstrapComplete: { + // noop + break; + } + case TorConnectTopics.BootstrapError: { + self.state.ErrorMessage = obj.message; + self.state.ErrorDetails = obj.details; + self.state.ShowCopyLog = true; + break; + } + case TorConnectTopics.FatalError: { + // TODO: handle + break; + } + case "nsPref:changed": { + if (aData === TorLauncherPrefs.quickstart) { + self.state.QuickStartEnabled = Services.prefs.getBoolPref(TorLauncherPrefs.quickstart); + } + break; + } + default: { + console.log(`TorConnect: unhandled observe topic '${aTopic}'`); + } + } + + self.sendAsyncMessage("torconnect:state-change", self.state); + }, + }; + + // observe all of the torconnect:.* topics + for (const key in TorConnectTopics) { + const topic = TorConnectTopics[key]; + Services.obs.addObserver(this.torConnectObserver, topic); + } + Services.prefs.addObserver(TorLauncherPrefs.quickstart, this.torConnectObserver); + } + + willDestroy() { + // stop observing all of our torconnect:.* topics + for (const key in TorConnectTopics) { + const topic = TorConnectTopics[key]; + Services.obs.removeObserver(this.torConnectObserver, topic); + } + Services.prefs.removeObserver(TorLauncherPrefs.quickstart, this.torConnectObserver); + } + + receiveMessage(message) { + switch (message.name) { + case "torconnect:set-quickstart": + Services.prefs.setBoolPref(TorLauncherPrefs.quickstart, message.data); + break; + case "torconnect:open-tor-preferences": + TorConnect.openTorPreferences(); + break; + case "torconnect:copy-tor-logs": + return TorConnect.copyTorLogs(); + case "torconnect:cancel-bootstrap": + TorConnect.cancelBootstrap(); + break; + case "torconnect:begin-bootstrap": + TorConnect.beginBootstrap(); + break; + case "torconnect:get-init-args": + // called on AboutTorConnect.init(), pass down all state data it needs to init + + // pretend this is a state transition on init + // so we always get fresh UI + this.state.StateChanged = true; + return { + TorStrings: TorStrings, + TorConnectState: TorConnectState, + Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr", + State: this.state, + }; + } + return undefined; + } +} diff --git a/browser/components/torconnect/content/aboutTorConnect.css b/browser/components/torconnect/content/aboutTorConnect.css new file mode 100644 index 0000000000000..14a3df2a59be3 --- /dev/null +++ b/browser/components/torconnect/content/aboutTorConnect.css @@ -0,0 +1,180 @@ + +/* Copyright (c) 2021, The Tor Project, Inc. */ + +@import url("chrome://browser/skin/error-pages.css"); +@import url("chrome://branding/content/tor-styles.css"); + +:root { + --onion-opacity: 1; + --onion-color: var(--card-outline-color); + --onion-radius: 75px; +} + +/* override firefox's default blue focus coloring */ +:focus { + outline: none!important; + box-shadow: 0 0 0 3px var(--purple-30) !important; + border: 1px var(--purple-80) solid !important; +} + +@media (prefers-color-scheme: dark) +{ + :focus { + box-shadow: 0 0 0 3px var(--purple-50)!important; + } +} + +#connectButton { + background-color: var(--purple-60)!important; + color: white; + fill: white; +} + +#connectButton:hover { + background-color: var(--purple-70)!important; + color: white; + fill: white; +} + +#connectButton:active { + background-color: var(--purple-80)!important; + color: white; + fill: white; +} + +/* checkbox css */ +input[type="checkbox"]:not(:disabled) { + background-color: var(--grey-20)!important; +} + +input[type="checkbox"]:not(:disabled):checked { + background-color: var(--purple-60)!important; + color: white; + fill: white; +} + +input[type="checkbox"]:not(:disabled):hover { + /* override firefox's default blue border on hover */ + border-color: var(--purple-70); + background-color: var(--grey-30)!important; +} + +input[type="checkbox"]:not(:disabled):hover:checked { + background-color: var(--purple-70)!important; +} + +input[type="checkbox"]:not(:disabled):active { + background-color: var(--grey-40)!important; +} + +input[type="checkbox"]:not(:disabled):active:checked { + background-color: var(--purple-80)!important; +} + +#progressBackground { + position:fixed; + padding:0; + margin:0; + top:0; + left:0; + width: 0%; + height: 7px; + background-image: linear-gradient(90deg, rgb(20, 218, 221) 0%, rgb(128, 109, 236) 100%); + border-radius: 0; +} + +#connectPageContainer { + margin-top: 10vh; + width: 50%; +} + +#quickstartCheckbox, #quickstartCheckboxLabel { + vertical-align: middle; +} + +#copyLogButton { + position: relative; +} + +/* mirrors p element spacing */ +#copyLogContainer { + margin: 1em 0; + height: 1.2em; + min-height: 1.2em; +} + +#copyLogLink { + position: relative; + display: inline-block; + color: var(--in-content-link-color); +} + +/* hidden apparently only works if no display is set; who knew? */ +#copyLogLink[hidden="true"] { + display: none; +} + +#copyLogLink:hover { + cursor:pointer; +} + +/* This div: + - is centered over its parent + - centers its child + - has z-index above parent + - ignores mouse events from parent +*/ +#copyLogTooltip { + pointer-events: none; + visibility: hidden; + display: flex; + justify-content: center; + white-space: nowrap; + width: 0; + position: absolute; + + z-index: 1; + left: 50%; + bottom: calc(100% + 0.25em); +} + +/* tooltip content (any content could go here) */ +#copyLogTooltipText { + background-color: var(--green-50); + color: var(--green-90); + border-radius: 2px; + padding: 4px; + line-height: 13px; + font: 11px sans-serif; + font-weight: 400; +} + +/* our speech bubble tail */ +#copyLogTooltipText::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -4px; + border-width: 4px; + border-style: solid; + border-color: var(--green-50) transparent transparent transparent; +} + +body { + padding: 0px !important; + justify-content: space-between; + background-color: var(--in-content-page-background); +} + +.title { + background-image: url("chrome://browser/content/torconnect/onion.svg"); + -moz-context-properties: fill, fill-opacity; + fill-opacity: var(--onion-opacity); + fill: var(--onion-color); +} + +.title.error { + background-image: url("chrome://browser/content/torconnect/onion-slash.svg"); +} + diff --git a/browser/components/torconnect/content/aboutTorConnect.js b/browser/components/torconnect/content/aboutTorConnect.js new file mode 100644 index 0000000000000..b53f8b13cb800 --- /dev/null +++ b/browser/components/torconnect/content/aboutTorConnect.js @@ -0,0 +1,302 @@ +// Copyright (c) 2021, The Tor Project, Inc. + +/* eslint-env mozilla/frame-script */ + +// populated in AboutTorConnect.init() +let TorStrings = {}; +let TorConnectState = {}; + +class AboutTorConnect { + selectors = Object.freeze({ + textContainer: { + title: "div.title", + titleText: "h1.title-text", + }, + progress: { + description: "p#connectShortDescText", + meter: "div#progressBackground", + }, + copyLog: { + link: "span#copyLogLink", + tooltip: "div#copyLogTooltip", + tooltipText: "span#copyLogTooltipText", + }, + quickstart: { + checkbox: "input#quickstartCheckbox", + label: "label#quickstartCheckboxLabel", + }, + buttons: { + connect: "button#connectButton", + cancel: "button#cancelButton", + advanced: "button#advancedButton", + }, + }) + + elements = Object.freeze({ + title: document.querySelector(this.selectors.textContainer.title), + titleText: document.querySelector(this.selectors.textContainer.titleText), + progressDescription: document.querySelector(this.selectors.progress.description), + progressMeter: document.querySelector(this.selectors.progress.meter), + copyLogLink: document.querySelector(this.selectors.copyLog.link), + copyLogTooltip: document.querySelector(this.selectors.copyLog.tooltip), + copyLogTooltipText: document.querySelector(this.selectors.copyLog.tooltipText), + quickstartCheckbox: document.querySelector(this.selectors.quickstart.checkbox), + quickstartLabel: document.querySelector(this.selectors.quickstart.label), + connectButton: document.querySelector(this.selectors.buttons.connect), + cancelButton: document.querySelector(this.selectors.buttons.cancel), + advancedButton: document.querySelector(this.selectors.buttons.advanced), + }) + + // a redirect url can be passed as a query parameter for the page to + // forward us to once bootstrap completes (otherwise the window will just close) + redirect = null + + beginBootstrap() { + this.hide(this.elements.connectButton); + this.show(this.elements.cancelButton); + this.elements.cancelButton.focus(); + RPMSendAsyncMessage("torconnect:begin-bootstrap"); + } + + cancelBootstrap() { + RPMSendAsyncMessage("torconnect:cancel-bootstrap"); + } + + /* + Element helper methods + */ + + show(element) { + element.removeAttribute("hidden"); + } + + hide(element) { + element.setAttribute("hidden", "true"); + } + + setTitle(title, error) { + this.elements.titleText.textContent = title; + document.title = title; + + if (error) { + this.elements.title.classList.add("error"); + } else { + this.elements.title.classList.remove("error"); + } + } + + setProgress(description, visible, percent) { + this.elements.progressDescription.textContent = description; + if (visible) { + this.show(this.elements.progressMeter); + this.elements.progressMeter.style.width = `${percent}%`; + } else { + this.hide(this.elements.progressMeter); + } + } + + /* + These methods update the UI based on the current TorConnect state + */ + + updateUI(state) { + console.log(state); + + // calls update_$state() + this[`update_${state.State}`](state); + this.elements.quickstartCheckbox.checked = state.QuickStartEnabled; + } + + /* Per-state updates */ + + update_Initial(state) { + const hasError = false; + const showProgressbar = false; + + this.setTitle(TorStrings.torConnect.torConnect, hasError); + this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar); + this.hide(this.elements.copyLogLink); + this.hide(this.elements.connectButton); + this.hide(this.elements.advancedButton); + this.hide(this.elements.cancelButton); + } + + update_Configuring(state) { + const hasError = state.ErrorMessage != null; + const showProgressbar = false; + + if (hasError) { + this.setTitle(state.ErrorMessage, hasError); + this.setProgress(state.ErrorDetails, showProgressbar); + this.show(this.elements.copyLogLink); + this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain; + } else { + this.setTitle(TorStrings.torConnect.torConnect, hasError); + this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar); + this.hide(this.elements.copyLogLink); + this.elements.connectButton.textContent = TorStrings.torConnect.torConnectButton; + } + this.show(this.elements.connectButton); + if (state.StateChanged) { + this.elements.connectButton.focus(); + } + this.show(this.elements.advancedButton); + this.hide(this.elements.cancelButton); + } + + update_AutoConfiguring(state) { + // TODO: noop until this state is used + } + + update_Bootstrapping(state) { + const hasError = false; + const showProgressbar = true; + + this.setTitle(state.BootstrapStatus ? state.BootstrapStatus : TorStrings.torConnect.torConnecting, hasError); + this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar, state.BootstrapProgress); + if (state.ShowCopyLog) { + this.show(this.elements.copyLogLink); + } else { + this.hide(this.elements.copyLogLink); + } + this.hide(this.elements.connectButton); + this.hide(this.elements.advancedButton); + this.show(this.elements.cancelButton); + if (state.StateChanged) { + this.elements.cancelButton.focus(); + } + } + + update_Error(state) { + const hasError = true; + const showProgressbar = false; + + this.setTitle(state.ErrorMessage, hasError); + this.setProgress(state.ErrorDetails, showProgressbar); + this.show(this.elements.copyLogLink); + this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain; + this.show(this.elements.connectButton); + this.show(this.elements.advancedButton); + this.hide(this.elements.cancelButton); + } + + update_FatalError(state) { + // TODO: noop until this state is used + } + + update_Bootstrapped(state) { + const hasError = false; + const showProgressbar = true; + + this.setTitle(TorStrings.torConnect.torConnected, hasError); + this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar, 100); + this.hide(this.elements.connectButton); + this.hide(this.elements.advancedButton); + this.hide(this.elements.cancelButton); + + // redirects page to the requested redirect url, removes about:torconnect + // from the page stack, so users cannot accidentally go 'back' to the + // now unresponsive page + window.location.replace(this.redirect); + } + + update_Disabled(state) { + // TODO: we should probably have some UX here if a user goes to about:torconnect when + // it isn't in use (eg using tor-launcher or system tor) + } + + async initElements(direction) { + + document.documentElement.setAttribute("dir", direction); + + // sets the text content while keeping the child elements intact + this.elements.copyLogLink.childNodes[0].nodeValue = + TorStrings.torConnect.copyLog; + this.elements.copyLogLink.addEventListener("click", async (event) => { + const copiedMessage = await RPMSendQuery("torconnect:copy-tor-logs"); + this.elements.copyLogTooltipText.textContent = copiedMessage; + this.elements.copyLogTooltipText.style.visibility = "visible"; + + // clear previous timeout if one already exists + if (this.copyLogTimeoutId) { + clearTimeout(this.copyLogTimeoutId); + } + + // hide tooltip after X ms + const TOOLTIP_TIMEOUT = 2000; + this.copyLogTimeoutId = setTimeout(() => { + this.elements.copyLogTooltipText.style.visibility = "hidden"; + this.copyLogTimeoutId = 0; + }, TOOLTIP_TIMEOUT); + }); + + this.elements.quickstartCheckbox.addEventListener("change", () => { + const quickstart = this.elements.quickstartCheckbox.checked; + RPMSendAsyncMessage("torconnect:set-quickstart", quickstart); + }); + this.elements.quickstartLabel.textContent = TorStrings.settings.quickstartCheckbox; + + this.elements.connectButton.textContent = + TorStrings.torConnect.torConnectButton; + this.elements.connectButton.addEventListener("click", () => { + this.beginBootstrap(); + }); + + this.elements.advancedButton.textContent = TorStrings.torConnect.torConfigure; + this.elements.advancedButton.addEventListener("click", () => { + RPMSendAsyncMessage("torconnect:open-tor-preferences"); + }); + + this.elements.cancelButton.textContent = TorStrings.torConnect.cancel; + this.elements.cancelButton.addEventListener("click", () => { + this.cancelBootstrap(); + }); + } + + initObservers() { + // TorConnectParent feeds us state blobs to we use to update our UI + RPMAddMessageListener("torconnect:state-change", ({ data }) => { + this.updateUI(data); + }); + } + + initKeyboardShortcuts() { + document.onkeydown = (evt) => { + // unfortunately it looks like we still haven't standardized keycodes to + // integers, so we must resort to a string compare here :( + // see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for relevant documentation + if (evt.code === "Escape") { + this.cancelBootstrap(); + } + }; + } + + async init() { + // see if a user has a final destination after bootstrapping + let params = new URLSearchParams(new URL(document.location.href).search); + if (params.has("redirect")) { + const encodedRedirect = params.get("redirect"); + this.redirect = decodeURIComponent(encodedRedirect); + } else { + // if the user gets here manually or via the button in the urlbar + // then we will redirect to about:tor + this.redirect = "about:tor"; + } + + let args = await RPMSendQuery("torconnect:get-init-args"); + + // various constants + TorStrings = Object.freeze(args.TorStrings); + TorConnectState = Object.freeze(args.TorConnectState); + + this.initElements(args.Direction); + this.initObservers(); + this.initKeyboardShortcuts(); + + // populate UI based on current state + this.updateUI(args.State); + } +} + +const aboutTorConnect = new AboutTorConnect(); +aboutTorConnect.init(); diff --git a/browser/components/torconnect/content/aboutTorConnect.xhtml b/browser/components/torconnect/content/aboutTorConnect.xhtml new file mode 100644 index 0000000000000..595bbdf9a70a1 --- /dev/null +++ b/browser/components/torconnect/content/aboutTorConnect.xhtml @@ -0,0 +1,45 @@ +<!-- Copyright (c) 2021, The Tor Project, Inc. --> +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" /> + <link rel="stylesheet" href="chrome://browser/skin/onionPattern.css" type="text/css" media="all" /> + <link rel="stylesheet" href="chrome://browser/content/torconnect/aboutTorConnect.css" type="text/css" media="all" /> + </head> + <body> + <div id="progressBackground"></div> + <div id="connectPageContainer" class="container"> + <div id="text-container"> + <div class="title"> + <h1 class="title-text"/> + </div> + <div id="connectLongContent"> + <div id="connectShortDesc"> + <p id="connectShortDescText" /> + </div> + </div> + + <div id="copyLogContainer"> + <span id="copyLogLink" hidden="true"> + <div id="copyLogTooltip"> + <span id="copyLogTooltipText"/> + </div> + </span> + </div> + + <div id="quickstartContainer"> + <input id="quickstartCheckbox" type="checkbox" /> + <label id="quickstartCheckboxLabel" for="quickstartCheckbox"/> + </div> + + <div id="connectButtonContainer" class="button-container"> + <button id="advancedButton" hidden="true"></button> + <button id="cancelButton" hidden="true"></button> + <button id="connectButton" class="primary try-again" hidden="true"></button> + </div> + </div> + </div> +#include ../../../themes/shared/onionPattern.inc.xhtml + </body> + <script src="chrome://browser/content/torconnect/aboutTorConnect.js"/> +</html> diff --git a/browser/components/torconnect/content/onion-slash.svg b/browser/components/torconnect/content/onion-slash.svg new file mode 100644 index 0000000000000..93eb24b039055 --- /dev/null +++ b/browser/components/torconnect/content/onion-slash.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg"> + <path d="m14.1161 15.6245c-.0821.0001-.1634-.016-.2393-.0474-.0758-.0314-.1447-.0775-.2027-.1356l-12.749984-12.749c-.109266-.11882-.168406-.27526-.165071-.43666.003335-.16139.068886-.31525.182967-.42946.114078-.11421.267868-.17994.429258-.18345.16139-.00352.3179.05544.43685.16457l12.74998 12.75c.1168.1176.1824.2767.1824.4425s-.0656.3249-.1824.4425c-.058.058-.1269.1039-.2028.1352-.0759.0312-.1571.0471-.2392.0468z" fill-opacity="context-fill-opacity" fill="#ff0039" /> + <path d="m 8,0.5000002 c -1.61963,0 -3.1197431,0.5137987 -4.3457031,1.3867188 l 0.84375,0.8417968 0.7792969,0.78125 0.8613281,0.8613282 0.8164062,0.8164062 0.9863281,0.984375 h 0.058594 c 1.00965,0 1.828125,0.818485 1.828125,1.828125 0,0.01968 6.2e-4,0.039074 0,0.058594 L 10.8125,9.0449221 C 10.9334,8.7195921 11,8.3674002 11,8.0000002 c 0,-1.65685 -1.34314,-3 -3,-3 v -1.078125 c 2.25231,0 4.078125,1.825845 4.078125,4.078125 0,0.67051 -0.162519,1.3033281 -0.449219,1.8613281 l 0.861328,0 [...] +</svg> diff --git a/browser/components/torconnect/content/onion.svg b/browser/components/torconnect/content/onion.svg new file mode 100644 index 0000000000000..7655a800d9eec --- /dev/null +++ b/browser/components/torconnect/content/onion.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg"> + <path d="M 8 0.5 C 3.85786 0.5 0.5 3.85786 0.5 8 C 0.5 12.1421 3.85786 15.5 8 15.5 C 12.1421 15.5 15.5 12.1421 15.5 8 C 15.5 3.85786 12.1421 0.5 8 0.5 z M 8 1.671875 C 11.4949 1.671875 14.328125 4.50507 14.328125 8 C 14.328125 11.4949 11.4949 14.328125 8 14.328125 L 8 13.25 C 10.89951 13.25 13.25 10.89951 13.25 8 C 13.25 5.10051 10.89951 2.75 8 2.75 L 8 1.671875 z M 8 3.921875 C 10.25231 3.921875 12.078125 5.74772 12.078125 8 C 12.078125 10.25231 10.25231 12.078125 8 12.078125 L 8 11 C [...] +</svg> diff --git a/browser/components/torconnect/content/torBootstrapUrlbar.js b/browser/components/torconnect/content/torBootstrapUrlbar.js new file mode 100644 index 0000000000000..e6a88490f33d4 --- /dev/null +++ b/browser/components/torconnect/content/torBootstrapUrlbar.js @@ -0,0 +1,93 @@ +// Copyright (c) 2021, The Tor Project, Inc. + +"use strict"; + +const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" +); +const { TorStrings } = ChromeUtils.import( + "resource:///modules/TorStrings.jsm" +); + +var TorBootstrapUrlbar = { + selectors: Object.freeze({ + torConnect: { + box: "hbox#torconnect-box", + label: "label#torconnect-label", + }, + }), + + elements: null, + + updateTorConnectBox: function(state) { + switch(state) + { + case TorConnectState.Initial: + case TorConnectState.Configuring: + case TorConnectState.AutoConfiguring: + case TorConnectState.Error: + case TorConnectState.FatalError: { + this.elements.torConnectBox.removeAttribute("hidden"); + this.elements.torConnectLabel.textContent = + TorStrings.torConnect.torNotConnectedConcise; + this.elements.inputContainer.setAttribute("torconnect", "offline"); + break; + } + case TorConnectState.Bootstrapping: { + this.elements.torConnectBox.removeAttribute("hidden"); + this.elements.torConnectLabel.textContent = + TorStrings.torConnect.torConnectingConcise; + this.elements.inputContainer.setAttribute("torconnect", "connecting"); + break; + } + case TorConnectState.Bootstrapped: { + this.elements.torConnectBox.removeAttribute("hidden"); + this.elements.torConnectLabel.textContent = + TorStrings.torConnect.torConnectedConcise; + this.elements.inputContainer.setAttribute("torconnect", "connected"); + // hide torconnect box after 5 seconds + setTimeout(() => { + this.elements.torConnectBox.setAttribute("hidden", "true"); + }, 5000); + break; + } + case TorConnectState.Disabled: { + this.elements.torConnectBox.setAttribute("hidden", "true"); + break; + } + default: + break; + } + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic === TorConnectTopics.StateChange) { + const obj = aSubject?.wrappedJSObject; + this.updateTorConnectBox(obj?.state); + } + }, + + init: function() { + if (TorConnect.shouldShowTorConnect) { + // browser isn't populated until init + this.elements = Object.freeze({ + torConnectBox: browser.ownerGlobal.document.querySelector(this.selectors.torConnect.box), + torConnectLabel: browser.ownerGlobal.document.querySelector(this.selectors.torConnect.label), + inputContainer: gURLBar._inputContainer, + }) + this.elements.torConnectBox.addEventListener("click", () => { + TorConnect.openTorConnect(); + }); + Services.obs.addObserver(this, TorConnectTopics.StateChange); + this.observing = true; + this.updateTorConnectBox(TorConnect.state); + } + }, + + uninit: function() { + if (this.observing) { + Services.obs.removeObserver(this, TorConnectTopics.StateChange); + } + }, +}; + diff --git a/browser/components/torconnect/content/torconnect-urlbar.css b/browser/components/torconnect/content/torconnect-urlbar.css new file mode 100644 index 0000000000000..5aabcffedbd02 --- /dev/null +++ b/browser/components/torconnect/content/torconnect-urlbar.css @@ -0,0 +1,57 @@ +/* + ensure our torconnect button is always visible (same rule as for the bookmark button) +*/ +hbox.urlbar-page-action#torconnect-box { + display: -moz-inline-box!important; + height: 28px; +} + +label#torconnect-label { + line-height: 28px; + margin: 0; + opacity: 0.6; + padding: 0 0.5em; +} + +/* set appropriate sizes for the non-standard ui densities */ +:root[uidensity=compact] hbox.urlbar-page-action#torconnect-box { + height: 24px; +} +:root[uidensity=compact] label#torconnect-label { + line-height: 24px; +} + + +:root[uidensity=touch] hbox.urlbar-page-action#torconnect-box { + height: 30px; +} +:root[uidensity=touch] label#torconnect-label { + line-height: 30px; +} + + +/* hide when hidden attribute is set */ +hbox.urlbar-page-action#torconnect-box[hidden="true"], +/* hide when user is typing in URL bar */ +#urlbar[usertyping] > #urlbar-input-container > #page-action-buttons > #torconnect-box { + display: none!important; +} + +/* hide urlbar's placeholder text when not connectd to tor */ +hbox#urlbar-input-container[torconnect="offline"] input#urlbar-input::placeholder, +hbox#urlbar-input-container[torconnect="connecting"] input#urlbar-input::placeholder { + opacity: 0; +} + +/* hide search suggestions when not connected to tor */ +hbox#urlbar-input-container[torconnect="offline"] + vbox.urlbarView, +hbox#urlbar-input-container[torconnect="connecting"] + vbox.urlbarView { + display: none!important; +} + +/* hide search icon when we are not connected to tor */ +hbox#urlbar-input-container[torconnect="offline"] > #identity-box[pageproxystate="invalid"] > #identity-icon, +hbox#urlbar-input-container[torconnect="connecting"] > #identity-box[pageproxystate="invalid"] > #identity-icon +{ + display: none!important; +} diff --git a/browser/components/torconnect/content/torconnect-urlbar.inc.xhtml b/browser/components/torconnect/content/torconnect-urlbar.inc.xhtml new file mode 100644 index 0000000000000..60e985a726910 --- /dev/null +++ b/browser/components/torconnect/content/torconnect-urlbar.inc.xhtml @@ -0,0 +1,10 @@ +# Copyright (c) 2021, The Tor Project, Inc. + +<hbox id="torconnect-box" + class="urlbar-icon-wrapper urlbar-page-action" + role="status" + hidden="true"> + <hbox id="torconnect-container"> + <label id="torconnect-label"/> + </hbox> +</hbox> \ No newline at end of file diff --git a/browser/components/torconnect/jar.mn b/browser/components/torconnect/jar.mn new file mode 100644 index 0000000000000..ed8a4de299b2b --- /dev/null +++ b/browser/components/torconnect/jar.mn @@ -0,0 +1,7 @@ +browser.jar: + content/browser/torconnect/torBootstrapUrlbar.js (content/torBootstrapUrlbar.js) + content/browser/torconnect/aboutTorConnect.css (content/aboutTorConnect.css) +* content/browser/torconnect/aboutTorConnect.xhtml (content/aboutTorConnect.xhtml) + content/browser/torconnect/aboutTorConnect.js (content/aboutTorConnect.js) + content/browser/torconnect/onion.svg (content/onion.svg) + content/browser/torconnect/onion-slash.svg (content/onion-slash.svg) diff --git a/browser/components/torconnect/moz.build b/browser/components/torconnect/moz.build new file mode 100644 index 0000000000000..eb29c31a42439 --- /dev/null +++ b/browser/components/torconnect/moz.build @@ -0,0 +1,6 @@ +JAR_MANIFESTS += ['jar.mn'] + +EXTRA_JS_MODULES += [ + 'TorConnectChild.jsm', + 'TorConnectParent.jsm', +] diff --git a/browser/components/torpreferences/content/torPane.js b/browser/components/torpreferences/content/torPane.js index 49054b5dac6a9..b81a238efc6fc 100644 --- a/browser/components/torpreferences/content/torPane.js +++ b/browser/components/torpreferences/content/torPane.js @@ -1,9 +1,15 @@ "use strict";
+/* global Services */ + const { TorProtocolService } = ChromeUtils.import( "resource:///modules/TorProtocolService.jsm" );
+const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" +); + const { TorBridgeSource, TorBridgeSettings, @@ -51,6 +57,10 @@ const { parsePort, parseBridgeStrings, parsePortList } = ChromeUtils.import( "chrome://browser/content/torpreferences/parseFunctions.jsm" );
+const TorLauncherPrefs = { + quickstart: "extensions.torlauncher.quickstart", +} + /* Tor Pane
@@ -62,11 +72,21 @@ const gTorPane = (function() { category: { title: "label#torPreferences-labelCategory", }, + messageBox: { + box: "div#torPreferences-connectMessageBox", + message: "td#torPreferences-connectMessageBox-message", + button: "button#torPreferences-connectMessageBox-button", + }, torPreferences: { header: "h1#torPreferences-header", description: "span#torPreferences-description", learnMore: "label#torPreferences-learnMore", }, + quickstart: { + header: "h2#torPreferences-quickstart-header", + description: "span#torPreferences-quickstart-description", + enableQuickstartCheckbox: "checkbox#torPreferences-quickstart-toggle", + }, bridges: { header: "h2#torPreferences-bridges-header", description: "span#torPreferences-bridges-description", @@ -112,6 +132,10 @@ const gTorPane = (function() {
let retval = { // cached frequently accessed DOM elements + _messageBox: null, + _messageBoxMessage: null, + _messageBoxButton: null, + _enableQuickstartCheckbox: null, _useBridgeCheckbox: null, _bridgeSelectionRadiogroup: null, _builtinBridgeOption: null, @@ -161,6 +185,51 @@ const gTorPane = (function() {
let prefpane = document.getElementById("mainPrefPane");
+ // 'Connect to Tor' Message Bar + + this._messageBox = prefpane.querySelector(selectors.messageBox.box); + this._messageBoxMessage = prefpane.querySelector(selectors.messageBox.message); + this._messageBoxButton = prefpane.querySelector(selectors.messageBox.button); + // wire up connect button + this._messageBoxButton.addEventListener("click", () => { + TorConnect.beginBootstrap(); + TorConnect.openTorConnect(); + }); + + this._populateMessagebox = () => { + if (TorConnect.shouldShowTorConnect && + TorConnect.state === TorConnectState.Configuring) { + // set messagebox style and text + if (TorProtocolService.torBootstrapErrorOccurred()) { + this._messageBox.parentNode.style.display = null; + this._messageBox.className = "error"; + this._messageBoxMessage.innerText = TorStrings.torConnect.tryAgainMessage; + this._messageBoxButton.innerText = TorStrings.torConnect.tryAgain; + } else { + this._messageBox.parentNode.style.display = null; + this._messageBox.className = "warning"; + this._messageBoxMessage.innerText = TorStrings.torConnect.connectMessage; + this._messageBoxButton.innerText = TorStrings.torConnect.torConnectButton; + } + } else { + // we need to explicitly hide the groupbox, as switching between + // the tor pane and other panes will 'unhide' (via the 'hidden' + // attribute) the groupbox, offsetting all of the content down + // by the groupbox's margin (even if content is 0 height) + this._messageBox.parentNode.style.display = "none"; + this._messageBox.className = "hidden"; + this._messageBoxMessage.innerText = ""; + this._messageBoxButton.innerText = ""; + } + } + this._populateMessagebox(); + Services.obs.addObserver(this, TorConnectTopics.StateChange); + + // update the messagebox whenever we come back to the page + window.addEventListener("focus", val => { + this._populateMessagebox(); + }); + // Heading prefpane.querySelector(selectors.torPreferences.header).innerText = TorStrings.settings.torPreferencesHeading; @@ -177,6 +246,26 @@ const gTorPane = (function() { ); }
+ // Quickstart + prefpane.querySelector(selectors.quickstart.header).innerText = + TorStrings.settings.quickstartHeading; + prefpane.querySelector(selectors.quickstart.description).textContent = + TorStrings.settings.quickstartDescription; + + this._enableQuickstartCheckbox = prefpane.querySelector( + selectors.quickstart.enableQuickstartCheckbox + ); + this._enableQuickstartCheckbox.setAttribute( + "label", + TorStrings.settings.quickstartCheckbox + ); + this._enableQuickstartCheckbox.addEventListener("command", e => { + const checked = this._enableQuickstartCheckbox.checked; + Services.prefs.setBoolPref(TorLauncherPrefs.quickstart, checked); + }); + this._enableQuickstartCheckbox.checked = Services.prefs.getBoolPref(TorLauncherPrefs.quickstart); + Services.prefs.addObserver(TorLauncherPrefs.quickstart, this); + // Bridge setup prefpane.querySelector(selectors.bridges.header).innerText = TorStrings.settings.bridgesHeading; @@ -526,6 +615,18 @@ const gTorPane = (function() {
init() { this._populateXUL(); + + let onUnload = () => { + window.removeEventListener("unload", onUnload); + gTorPane.uninit(); + }; + window.addEventListener("unload", onUnload); + }, + + uninit() { + // unregister our observer topics + Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged); + Services.obs.removeObserver(this, TorConnectTopics.StateChange); },
// whether the page should be present in about:preferences @@ -537,6 +638,28 @@ const gTorPane = (function() { // Callbacks //
+ observe(subject, topic, data) { + switch (topic) { + // triggered when a TorSettings param has changed + case TorSettingsTopics.SettingChanged: { + let obj = subject?.wrappedJSObject; + switch(data) { + case TorSettingsData.QuickStartEnabled: { + this._enableQuickstartCheckbox.checked = obj.value; + break; + } + } + break; + } + // triggered when tor connect state changes and we may + // need to update the messagebox + case TorConnectTopics.StateChange: { + this._populateMessagebox(); + break; + } + } + }, + // callback when using bridges toggled onToggleBridge(enabled) { this._useBridgeCheckbox.checked = enabled; diff --git a/browser/components/torpreferences/content/torPane.xhtml b/browser/components/torpreferences/content/torPane.xhtml index 3c966b2b3726d..7c8071f2cf106 100644 --- a/browser/components/torpreferences/content/torPane.xhtml +++ b/browser/components/torpreferences/content/torPane.xhtml @@ -3,6 +3,29 @@ <script type="application/javascript" src="chrome://browser/content/torpreferences/torPane.js"/> <html:template id="template-paneTor"> + +<!-- Tor Connect Message Box --> +<groupbox data-category="paneTor" hidden="true"> + <html:div id="torPreferences-connectMessageBox" + class="subcategory" + data-category="paneTor" + hidden="true"> + <html:table > + html:tr + html:td + <html:div id="torPreferences-connectMessageBox-icon"/> + </html:td> + <html:td id="torPreferences-connectMessageBox-message"> + </html:td> + html:td + <html:button id="torPreferences-connectMessageBox-button"> + </html:button> + </html:td> + </html:tr> + </html:table> + </html:div> +</groupbox> + <hbox id="torPreferencesCategory" class="subcategory" data-category="paneTor" @@ -18,6 +41,17 @@ </description> </groupbox>
+<!-- Quickstart --> +<groupbox id="torPreferences-quickstart-group" + data-category="paneTor" + hidden="true"> + <html:h2 id="torPreferences-quickstart-header"/> + <description flex="1"> + <html:span id="torPreferences-quickstart-description"/> + </description> + <checkbox id="torPreferences-quickstart-toggle"/> +</groupbox> + <!-- Bridges --> <groupbox id="torPreferences-bridges-group" data-category="paneTor" diff --git a/browser/components/torpreferences/content/torPreferences.css b/browser/components/torpreferences/content/torPreferences.css index 4dac2c4578237..b6eb0a740e5e4 100644 --- a/browser/components/torpreferences/content/torPreferences.css +++ b/browser/components/torpreferences/content/torPreferences.css @@ -1,7 +1,119 @@ +@import url("chrome://branding/content/tor-styles.css"); + #category-tor > .category-icon { list-style-image: url("chrome://browser/content/torpreferences/torPreferencesIcon.svg"); }
+/* Connect Message Box */ + +#torPreferences-connectMessageBox { + display: block; + position: relative; + + width: auto; + min-height: 32px; + border-radius: 4px; + padding: 8px; +} + +#torPreferences-connectMessageBox.hidden { + display: none; +} + +#torPreferences-connectMessageBox.error { + background-color: var(--red-60); + color: white; +} + +#torPreferences-connectMessageBox.warning { + background-color: var(--purple-50); + color: white; +} + +#torPreferences-connectMessageBox table { + border-collapse: collapse; +} + +#torPreferences-connectMessageBox td { + vertical-align: middle; +} + +#torPreferences-connectMessageBox td:first-child { + width: 16px; +} + +#torPreferences-connectMessageBox-icon { + width: 16px; + height: 16px; + + mask-repeat: no-repeat !important; + mask-size: 16px !important; +} + +#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-icon +{ + mask: url("chrome://browser/skin/onion-slash.svg"); + background-color: white; +} + +#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-icon +{ + mask: url("chrome://browser/skin/onion.svg"); + background-color: white; +} + +#torPreferences-connectMessageBox-message { + line-height: 16px; + font-size: 1.11em; + padding-left: 8px!important; +} + +#torPreferences-connectMessageBox-button { + display: block; + width: auto; + + border-radius: 4px; + border: 0; + + padding-inline: 18px; + padding-block: 8px; + margin-block: 0px; + margin-inline-start: 8px; + margin-inline-end: 0px; + + font-size: 1.0em; + font-weight: 600; + white-space: nowrap; + + color: white; +} + +#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-button { + background-color: var(--red-70); +} + +#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-button:hover { + background-color: var(--red-80); +} + +#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-button:active { + background-color: var(--red-90); +} + +#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-button { + background-color: var(--purple-70); +} + +#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-button:hover { + background-color: var(--purple-80); +} + +#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-button:active { + background-color: var(--purple-90); +} + +/* Advanced Settings */ + #torPreferences-advanced-grid { display: grid; grid-template-columns: auto 1fr; diff --git a/browser/components/urlbar/UrlbarInput.jsm b/browser/components/urlbar/UrlbarInput.jsm index 45c784351c8f0..29ee12914719b 100644 --- a/browser/components/urlbar/UrlbarInput.jsm +++ b/browser/components/urlbar/UrlbarInput.jsm @@ -10,6 +10,34 @@ const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" );
+const { TorConnect } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" +); + +// in certain scenarios we want user input uris to open in a new tab if they do so from the +// about:torconnect tab +function maybeUpdateOpenLocationForTorConnect(openUILinkWhere, currentURI, destinationURI) { + try { + // only open in new tab if: + if (// user is navigating away from about:torconnect + currentURI === "about:torconnect" && + // we are trying to open in same tab + openUILinkWhere === "current" && + // only if user still has not bootstrapped + TorConnect.shouldShowTorConnect && + // and user is not just navigating to about:torconnect + destinationURI !== "about:torconnect") { + return "tab"; + } + } catch (e) { + // swallow exception and fall through returning original so we don't accidentally break + // anything if an exception is thrown + console.log(e?.message ? e.message : e); + } + + return openUILinkWhere; +}; + XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm", @@ -2427,6 +2455,10 @@ class UrlbarInput { this.selectionStart = this.selectionEnd = 0; }
+ openUILinkWhere = maybeUpdateOpenLocationForTorConnect( + openUILinkWhere, + this.window.gBrowser.currentURI.asciiSpec, + url); if (openUILinkWhere != "current") { this.handleRevert(); } diff --git a/browser/modules/TorConnect.jsm b/browser/modules/TorConnect.jsm new file mode 100644 index 0000000000000..4be220108d730 --- /dev/null +++ b/browser/modules/TorConnect.jsm @@ -0,0 +1,506 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["TorConnect", "TorConnectTopics", "TorConnectState"]; + +const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" +); + +const { BrowserWindowTracker } = ChromeUtils.import( + "resource:///modules/BrowserWindowTracker.jsm" +); + +const { TorProtocolService, TorProcessStatus } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); + +const { TorLauncherUtil } = ChromeUtils.import( + "resource://torlauncher/modules/tl-util.jsm" +); + +/* Browser observer topis */ +const BrowserTopics = Object.freeze({ + ProfileAfterChange: "profile-after-change", +}); + +/* tor-launcher observer topics */ +const TorTopics = Object.freeze({ + ProcessIsReady: "TorProcessIsReady", + BootstrapStatus: "TorBootstrapStatus", + BootstrapError: "TorBootstrapError", + ProcessExited: "TorProcessExited", + LogHasWarnOrErr: "TorLogHasWarnOrErr", +}); + +/* Relevant prefs used by tor-launcher */ +const TorLauncherPrefs = Object.freeze({ + quickstart: "extensions.torlauncher.quickstart", + prompt_at_startup: "extensions.torlauncher.prompt_at_startup", +}); + +const TorConnectState = Object.freeze({ + /* Our initial state */ + Initial: "Initial", + /* In-between initial boot and bootstrapping, users can change tor network settings during this state */ + Configuring: "Configuring", + /* Geo-location and setting bridges/etc */ + AutoConfiguring: "AutoConfiguring", + /* Tor is bootstrapping */ + Bootstrapping: "Bootstrapping", + /* Passthrough state back to Configuring or Fatal */ + Error: "Error", + /* An unrecoverable error */ + FatalError: "FatalError", + /* Final state, after successful bootstrap */ + Bootstrapped: "Bootstrapped", + /* If we are using System tor or the legacy Tor-Launcher */ + Disabled: "Disabled", +}); + +/* + + TorConnect State Transitions + + ┌──────────────────────┐ + │ Disabled │ + └──────────────────────┘ + ▲ + │ legacyOrSystemTor() + │ + ┌──────────────────────┐ + ┌────────────────────── │ Initial │ ───────────────────────────┐ + │ └──────────────────────┘ │ + │ │ │ + │ │ beginBootstrap() │ + │ ▼ │ +┌────────────────┐ │ bootstrapComplete() ┌────────────────────────────────────────────────┐ │ beginBootstrap() +│ Bootstrapped │ ◀──┼────────────────────── │ Bootstrapping │ ◀┼─────────────────┐ +└────────────────┘ │ └────────────────────────────────────────────────┘ │ │ + │ │ ▲ │ │ │ + │ │ cancelBootstrap() │ beginBootstrap() └────┼─────────────┐ │ + │ ▼ │ │ │ │ + │ beginConfigure() ┌────────────────────────────────────────────────┐ │ │ │ + └─────────────────────▶ │ │ │ │ │ + │ │ │ │ │ + beginConfigure() │ │ │ │ │ + ┌──────────────────────────▶ │ Configuring │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ │ + │ ┌─────────────────────▶ │ │ │ │ │ + │ │ └────────────────────────────────────────────────┘ │ │ │ + │ │ │ │ │ │ │ + │ │ cancelAutoconfigure() │ autoConfigure() │ ┌────┼─────────────┼───┘ + │ │ ▼ │ │ │ │ + │ │ ┌──────────────────────┐ │ │ │ │ + │ └────────────────────── │ AutoConfiguring │ ─┼────────────────────┘ │ │ + │ └──────────────────────┘ │ │ │ + │ │ │ │ onError() │ + │ │ onError() │ onError() │ │ + │ ▼ ▼ │ │ + │ ┌────────────────────────────────────────────────┐ │ │ + └─────────────────────────── │ Error │ ◀┘ │ + └────────────────────────────────────────────────┘ │ + │ ▲ onError() │ + │ onFatalError() └──────────────────┘ + ▼ + ┌──────────────────────┐ + │ FatalError │ + └──────────────────────┘ + +*/ + + +/* Maps allowed state transitions + TorConnectStateTransitions[state] maps to an array of allowed states to transition to +*/ +const TorConnectStateTransitions = + Object.freeze(new Map([ + [TorConnectState.Initial, + [TorConnectState.Disabled, + TorConnectState.Bootstrapping, + TorConnectState.Configuring, + TorConnectState.Error]], + [TorConnectState.Configuring, + [TorConnectState.AutoConfiguring, + TorConnectState.Bootstrapping, + TorConnectState.Error]], + [TorConnectState.AutoConfiguring, + [TorConnectState.Configuring, + TorConnectState.Bootstrapping, + TorConnectState.Error]], + [TorConnectState.Bootstrapping, + [TorConnectState.Configuring, + TorConnectState.Bootstrapped, + TorConnectState.Error]], + [TorConnectState.Error, + [TorConnectState.Configuring, + TorConnectState.FatalError]], + // terminal states + [TorConnectState.FatalError, []], + [TorConnectState.Bootstrapped, []], + [TorConnectState.Disabled, []], + ])); + +/* Topics Notified by the TorConnect module */ +const TorConnectTopics = Object.freeze({ + StateChange: "torconnect:state-change", + BootstrapProgress: "torconnect:bootstrap-progress", + BootstrapComplete: "torconnect:bootstrap-complete", + BootstrapError: "torconnect:bootstrap-error", + FatalError: "torconnect:fatal-error", +}); + +const TorConnect = (() => { + let retval = { + + _state: TorConnectState.Initial, + _bootstrapProgress: 0, + _bootstrapStatus: null, + _errorMessage: null, + _errorDetails: null, + _logHasWarningOrError: false, + + /* These functions are called after transitioning to a new state */ + _transitionCallbacks: Object.freeze(new Map([ + /* Initial is never transitioned to */ + [TorConnectState.Initial, null], + /* Configuring */ + [TorConnectState.Configuring, async (self, prevState) => { + // TODO move this to the transition function + if (prevState === TorConnectState.Bootstrapping) { + await TorProtocolService.torStopBootstrap(); + } + }], + /* AutoConfiguring */ + [TorConnectState.AutoConfiguring, async (self, prevState) => { + + }], + /* Bootstrapping */ + [TorConnectState.Bootstrapping, async (self, prevState) => { + let error = await TorProtocolService.connect(); + if (error) { + self.onError(error.message, error.details); + } else { + self._errorMessage = self._errorDetails = null; + } + }], + /* Bootstrapped */ + [TorConnectState.Bootstrapped, async (self,prevState) => { + // notify observers of bootstrap completion + Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete); + }], + /* Error */ + [TorConnectState.Error, async (self, prevState, errorMessage, errorDetails, fatal) => { + self._errorMessage = errorMessage; + self._errorDetails = errorDetails; + + Services.obs.notifyObservers({message: errorMessage, details: errorDetails}, TorConnectTopics.BootstrapError); + if (fatal) { + self.onFatalError(); + } else { + self.beginConfigure(); + } + }], + /* FatalError */ + [TorConnectState.FatalError, async (self, prevState) => { + Services.obs.notifyObservers(null, TorConnectTopics.FatalError); + }], + /* Disabled */ + [TorConnectState.Disabled, (self, prevState) => { + + }], + ])), + + _changeState: async function(newState, ...args) { + const prevState = this._state; + + // ensure this is a valid state transition + if (!TorConnectStateTransitions.get(prevState)?.includes(newState)) { + throw Error(`TorConnect: Attempted invalid state transition from ${prevState} to ${newState}`); + } + + console.log(`TorConnect: transitioning state from ${prevState} to ${newState}`); + + // set our new state first so that state transitions can themselves trigger + // a state transition + this._state = newState; + + // call our transition function and forward any args + await this._transitionCallbacks.get(newState)(this, prevState, ...args); + + Services.obs.notifyObservers({state: newState}, TorConnectTopics.StateChange); + }, + + // init should be called on app-startup in MainProcessingSingleton.jsm + init : function() { + console.log("TorConnect: Init"); + + // delay remaining init until after profile-after-change + Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange); + }, + + observe: async function(subject, topic, data) { + console.log(`TorConnect: observed ${topic}`); + + switch(topic) { + + /* Determine which state to move to from Initial */ + case BrowserTopics.ProfileAfterChange: { + if (TorLauncherUtil.useLegacyLauncher || !TorProtocolService.ownsTorDaemon) { + // Disabled + this.legacyOrSystemTor(); + } else { + // register the Tor topics we always care about + for (const topicKey in TorTopics) { + const topic = TorTopics[topicKey]; + Services.obs.addObserver(this, topic); + console.log(`TorConnect: observing topic '${topic}'`); + } + + if (TorProtocolService.torProcessStatus == TorProcessStatus.Running) { + if (this.shouldQuickStart) { + // Quickstart + this.beginBootstrap(); + } else { + // Configuring + this.beginConfigure(); + } + } + } + + Services.obs.removeObserver(this, topic); + break; + } + /* Transition out of Initial if Tor daemon wasn't running yet in BrowserTopics.ProfileAfterChange */ + case TorTopics.ProcessIsReady: { + if (this.state === TorConnectState.Initial) + { + if (this.shouldQuickStart) { + // Quickstart + this.beginBootstrap(); + } else { + // Configuring + this.beginConfigure(); + } + } + break; + } + /* Updates our bootstrap status */ + case TorTopics.BootstrapStatus: { + if (this._state != TorConnectState.Bootstrapping) { + console.log(`TorConnect: observed ${TorTopics.BootstrapStatus} topic while in state TorConnectState.${this._state}`); + break; + } + + const obj = subject?.wrappedJSObject; + if (obj) { + this._bootstrapProgress= obj.PROGRESS; + this._bootstrapStatus = TorLauncherUtil.getLocalizedBootstrapStatus(obj, "TAG"); + + console.log(`TorConnect: Bootstrapping ${this._bootstrapProgress}% complete (${this._bootstrapStatus})`); + Services.obs.notifyObservers({ + progress: this._bootstrapProgress, + status: this._bootstrapStatus, + hasWarnings: this._logHasWarningOrError + }, TorConnectTopics.BootstrapProgress); + + if (this._bootstrapProgress === 100) { + this.bootstrapComplete(); + } + } + break; + } + /* Handle bootstrap error*/ + case TorTopics.BootstrapError: { + const obj = subject?.wrappedJSObject; + await TorProtocolService.torStopBootstrap(); + this.onError(obj.message, obj.details); + break; + } + case TorTopics.LogHasWarnOrErr: { + this._logHasWarningOrError = true; + break; + } + default: + // ignore + break; + } + }, + + /* + Various getters + */ + + get shouldShowTorConnect() { + // TorBrowser must control the daemon + return (TorProtocolService.ownsTorDaemon && + // and we're not using the legacy launcher + !TorLauncherUtil.useLegacyLauncher && + // if we have succesfully bootstraped, then no need to show TorConnect + this.state != TorConnectState.Bootstrapped); + }, + + get shouldQuickStart() { + // quickstart must be enabled + return Services.prefs.getBoolPref(TorLauncherPrefs.quickstart, false) && + // and the previous bootstrap attempt must have succeeded + !Services.prefs.getBoolPref(TorLauncherPrefs.prompt_at_startup, true); + }, + + get state() { + return this._state; + }, + + get bootstrapProgress() { + return this._bootstrapProgress; + }, + + get bootstrapStatus() { + return this._bootstrapStatus; + }, + + get errorMessage() { + return this._errorMessage; + }, + + get errorDetails() { + return this._errorDetails; + }, + + get logHasWarningOrError() { + return this._logHasWarningOrError; + }, + + /* + These functions tell TorConnect to transition states + */ + + legacyOrSystemTor: function() { + console.log("TorConnect: legacyOrSystemTor()"); + this._changeState(TorConnectState.Disabled); + }, + + beginBootstrap: function() { + console.log("TorConnect: beginBootstrap()"); + this._changeState(TorConnectState.Bootstrapping); + }, + + beginConfigure: function() { + console.log("TorConnect: beginConfigure()"); + this._changeState(TorConnectState.Configuring); + }, + + autoConfigure: function() { + console.log("TorConnect: autoConfigure()"); + // TODO: implement + throw Error("TorConnect: not implemented"); + }, + + cancelAutoConfigure: function() { + console.log("TorConnect: cancelAutoConfigure()"); + // TODO: implement + throw Error("TorConnect: not implemented"); + }, + + cancelBootstrap: function() { + console.log("TorConnect: cancelBootstrap()"); + this._changeState(TorConnectState.Configuring); + }, + + bootstrapComplete: function() { + console.log("TorConnect: bootstrapComplete()"); + this._changeState(TorConnectState.Bootstrapped); + }, + + onError: function(message, details) { + console.log("TorConnect: onError()"); + this._changeState(TorConnectState.Error, message, details, false); + }, + + onFatalError: function() { + console.log("TorConnect: onFatalError()"); + // TODO: implement + throw Error("TorConnect: not implemented"); + }, + + /* + Further external commands and helper methods + */ + openTorPreferences: function() { + const win = BrowserWindowTracker.getTopWindow(); + win.switchToTabHavingURI("about:preferences#tor", true); + }, + + openTorConnect: function() { + const win = BrowserWindowTracker.getTopWindow(); + win.switchToTabHavingURI("about:torconnect", true, {ignoreQueryString: true}); + }, + + copyTorLogs: function() { + // Copy tor log messages to the system clipboard. + const chSvc = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + const countObj = { value: 0 }; + chSvc.copyString(TorProtocolService.getLog(countObj)); + const count = countObj.value; + return TorLauncherUtil.getFormattedLocalizedString( + "copiedNLogMessagesShort", + [count], + 1 + ); + }, + + // called from browser.js on browser startup, passed in either the user's homepage(s) + // or uris passed via command-line; we want to replace them with about:torconnect uris + // which redirect after bootstrapping + getURIsToLoad: function(uriVariant) { + // convert the object we get from browser.js + let uriStrings = ((v) => { + // an interop array + if (v instanceof Ci.nsIArray) { + // Transform the nsIArray of nsISupportsString's into a JS Array of + // JS strings. + return Array.from( + v.enumerate(Ci.nsISupportsString), + supportStr => supportStr.data + ); + // an interop string + } else if (v instanceof Ci.nsISupportsString) { + return [v.data]; + // a js string + } else if (typeof v === "string") { + return v.split("|"); + // a js array of js strings + } else if (Array.isArray(v) && + v.reduce((allStrings, entry) => {return allStrings && (typeof entry === "string");}, true)) { + return v; + } + // about:tor as safe fallback + console.log(`TorConnect: getURIsToLoad() received unknown variant '${JSON.stringify(v)}'`); + return ["about:tor"]; + })(uriVariant); + + // will attempt to convert user-supplied string to a uri, fallback to about:tor if cannot convert + // to valid uri object + let uriStringToUri = (uriString) => { + const fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE; + let uri = Services.uriFixup.getFixupURIInfo(uriString, fixupFlags) + .preferredURI; + return uri ? uri : Services.io.newURI("about:tor"); + }; + let uris = uriStrings.map(uriStringToUri); + + // assume we have a valid uri and generate an about:torconnect redirect uri + let uriToRedirectUri = (uri) => { + return`about:torconnect?redirect=${encodeURIComponent(uri.spec)}`; + }; + let redirectUris = uris.map(uriToRedirectUri); + + console.log(`TorConnect: will load after bootstrap => [${uris.map((uri) => {return uri.spec;}).join(", ")}]`); + return redirectUris; + }, + }; + retval.init(); + return retval; +})(); /* TorConnect */ diff --git a/browser/modules/TorProcessService.jsm b/browser/modules/TorProcessService.jsm new file mode 100644 index 0000000000000..201e331b28066 --- /dev/null +++ b/browser/modules/TorProcessService.jsm @@ -0,0 +1,12 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["TorProcessService"]; + +var TorProcessService = { + get isBootstrapDone() { + const svc = Cc["@torproject.org/torlauncher-process-service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + return svc.mIsBootstrapDone; + }, +}; diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm index 409d6be60b830..b8678fbca9aa0 100644 --- a/browser/modules/TorProtocolService.jsm +++ b/browser/modules/TorProtocolService.jsm @@ -1,21 +1,60 @@ +// Copyright (c) 2021, The Tor Project, Inc. + "use strict";
-var EXPORTED_SYMBOLS = ["TorProtocolService"]; +var EXPORTED_SYMBOLS = ["TorProtocolService", "TorProcessStatus"];
-const { TorLauncherUtil } = ChromeUtils.import( - "resource://torlauncher/modules/tl-util.jsm" +const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" );
+// see tl-process.js +const TorProcessStatus = Object.freeze({ + Unknown: 0, + Starting: 1, + Running: 2, + Exited: 3, +}); + +/* Browser observer topis */ +const BrowserTopics = Object.freeze({ + ProfileAfterChange: "profile-after-change", +}); + var TorProtocolService = { - _tlps: Cc["@torproject.org/torlauncher-protocol-service;1"].getService( - Ci.nsISupports - ).wrappedJSObject, + _TorLauncherUtil: function() { + let { TorLauncherUtil } = ChromeUtils.import( + "resource://torlauncher/modules/tl-util.jsm" + ); + return TorLauncherUtil; + }(), + _TorLauncherProtocolService: null, + _TorProcessService: null,
// 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(),
+ init() { + Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange); + }, + + observe(subject, topic, data) { + if (topic === BrowserTopics.ProfileAfterChange) { + // we have to delay init'ing this or else the crypto service inits too early without a profile + // which breaks the password manager + this._TorLauncherProtocolService = Cc["@torproject.org/torlauncher-protocol-service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + this._TorProcessService = Cc["@torproject.org/torlauncher-process-service;1"].getService( + Ci.nsISupports + ).wrappedJSObject, + + Services.obs.removeObserver(this, topic); + } + }, + _typeof(aValue) { switch (typeof aValue) { case "boolean": @@ -199,9 +238,9 @@ var TorProtocolService = { await this.sendCommand("SAVECONF"); },
- getLog() { - let countObj = { value: 0 }; - let torLog = this._tlps.TorGetLog(countObj); + getLog(countObj) { + countObj = countObj || { value: 0 }; + let torLog = this._TorLauncherProtocolService.TorGetLog(countObj); return torLog; },
@@ -321,3 +360,4 @@ var TorProtocolService = { return TorProcessStatus.Unknown; }, }; +TorProtocolService.init(); \ No newline at end of file diff --git a/browser/modules/TorStrings.jsm b/browser/modules/TorStrings.jsm index cc4f6b340c5f4..73671c08693d9 100644 --- a/browser/modules/TorStrings.jsm +++ b/browser/modules/TorStrings.jsm @@ -261,6 +261,9 @@ var TorStrings = { "Tor Browser routes your traffic over the Tor Network, run by thousands of volunteers around the world." ), learnMore: getString("torPreferences.learnMore", "Learn More"), + quickstartHeading: getString("torPreferences.quickstart", "Quickstart"), + quickstartDescription: getString("torPreferences.quickstartDescription", "Quickstart allows Tor Browser to connect automatically."), + quickstartCheckbox : getString("torPreferences.quickstartCheckbox", "Always connect automatically"), bridgesHeading: getString("torPreferences.bridges", "Bridges"), bridgesDescription: getString( "torPreferences.bridgesDescription", @@ -368,6 +371,83 @@ var TorStrings = { return retval; })() /* Tor Network Settings Strings */,
+ torConnect: (() => { + const tsbNetwork = new TorDTDStringBundle( + ["chrome://torlauncher/locale/network-settings.dtd"], + "" + ); + const tsbLauncher = new TorPropertyStringBundle( + "chrome://torlauncher/locale/torlauncher.properties", + "torlauncher." + ); + const tsbCommon = new TorPropertyStringBundle( + "chrome://global/locale/commonDialogs.properties", + "" + ); + + const getStringNet = tsbNetwork.getString.bind(tsbNetwork); + const getStringLauncher = tsbLauncher.getString.bind(tsbLauncher); + const getStringCommon = tsbCommon.getString.bind(tsbCommon); + + return { + torConnect: getStringNet( + "torsettings.wizard.title.default", + "Connect to Tor" + ), + + torConnecting: getStringNet( + "torsettings.wizard.title.connecting", + "Establishing a Connection" + ), + + torNotConnectedConcise: getStringNet( + "torConnect.notConnectedConcise", + "Not Connected" + ), + + torConnectingConcise: getStringNet( + "torConnect.connectingConcise", + "Connecting…" + ), + + torBootstrapFailed: getStringLauncher( + "tor_bootstrap_failed", + "Tor failed to establish a Tor network connection." + ), + + torConfigure: getStringNet( + "torsettings.wizard.title.configure", + "Tor Network Settings" + ), + + copyLog: getStringNet( + "torConnect.copyLog", + "Copy Tor Logs" + ), + + torConnectButton: getStringNet("torSettings.connect", "Connect"), + + cancel: getStringCommon("Cancel", "Cancel"), + + torConnected: getStringLauncher( + "torlauncher.bootstrapStatus.done", + "Connected to the Tor network" + ), + + torConnectedConcise: getStringLauncher( + "torConnect.connectedConcise", + "Connected" + ), + + tryAgain: getStringNet("torConnect.tryAgain", "Try connecting again"), + offline: getStringNet("torConnect.offline", "Offline"), + + // tor connect strings for message box in about:preferences#tor + connectMessage: getStringNet("torConnect.connectMessage", "Changes to Tor Settings will not take effect until you connect to the Tor Network"), + tryAgainMessage: getStringNet("torConnect.tryAgainMessage", "Tor Browser has failed to establish a connection to the Tor Network"), + }; + })(), + /* Tor Onion Services Strings, e.g., for the authentication prompt. */ diff --git a/browser/modules/moz.build b/browser/modules/moz.build index 1f7c6bc4c67e3..1ea57aba1a93d 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -153,6 +153,8 @@ EXTRA_JS_MODULES += [ "TabsList.jsm", "TabUnloader.jsm", "ThemeVariableMap.jsm", + 'TorConnect.jsm', + 'TorProcessService.jsm', "TorProtocolService.jsm", "TorStrings.jsm", "TransientPrefs.jsm", diff --git a/browser/themes/shared/identity-block/identity-block.inc.css b/browser/themes/shared/identity-block/identity-block.inc.css index a863d1d7d20e3..145ea27328460 100644 --- a/browser/themes/shared/identity-block/identity-block.inc.css +++ b/browser/themes/shared/identity-block/identity-block.inc.css @@ -53,16 +53,15 @@ border-radius: var(--urlbar-icon-border-radius); }
-%ifdef MOZ_OFFICIAL_BRANDING #identity-box[pageproxystate="valid"].notSecureText #identity-icon-label, #identity-box[pageproxystate="valid"].chromeUI #identity-icon-label { - color: #420C5D; + color: var(--tor-branding-color); + opacity: 1; }
toolbar[brighttext] #identity-box[pageproxystate="valid"].chromeUI #identity-icon-label { color: #CC80FF; } -%endif
#identity-box[pageproxystate="valid"].chromeUI #identity-icon-label, #identity-box[pageproxystate="valid"].extensionPage #identity-icon-label, @@ -161,6 +160,8 @@ toolbar[brighttext] #identity-box[pageproxystate="valid"].chromeUI #identity-ico
#identity-box[pageproxystate="valid"].chromeUI #identity-icon { list-style-image: url(chrome://branding/content/identity-icons-brand.svg); + fill: var(--tor-branding-color); + fill-opacity: 1; }
#identity-box[pageproxystate="valid"].localResource #identity-icon { diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn index 3b11a9864cf86..b2e469b90aa80 100644 --- a/browser/themes/shared/jar.inc.mn +++ b/browser/themes/shared/jar.inc.mn @@ -9,6 +9,8 @@
skin/classic/browser/aboutNetError.css (../shared/aboutNetError.css) skin/classic/browser/offlineSupportPages.css (../shared/offlineSupportPages.css) + skin/classic/browser/onionPattern.css (../shared/onionPattern.css) + skin/classic/browser/onionPattern.svg (../shared/onionPattern.svg) skin/classic/browser/blockedSite.css (../shared/blockedSite.css) skin/classic/browser/error-pages.css (../shared/error-pages.css) skin/classic/browser/aboutRestartRequired.css (../shared/aboutRestartRequired.css) diff --git a/browser/themes/shared/onionPattern.css b/browser/themes/shared/onionPattern.css new file mode 100644 index 0000000000000..1852350d57f73 --- /dev/null +++ b/browser/themes/shared/onionPattern.css @@ -0,0 +1,31 @@ +/* Onion pattern */ + +.onion-pattern-container { + + flex: auto; /* grow to consume remaining space on the page */ + display: flex; + margin: 0 auto; + width: 100%; + /* two onions tall, 4x the radius */ + height: calc(4 * var(--onion-radius)); + max-height: calc(4 * var(--onion-radius)); + min-height: calc(4 * var(--onion-radius)); + direction: ltr; +} + +.onion-pattern-crop { + height: 100%; + width: 100%; + + -moz-context-properties: fill; + fill: var(--onion-color, currentColor); + /* opacity of the entire div, not context-opacity */ + opacity: var(--onion-opacity, 1); + + background-image: url("chrome://browser/skin/onionPattern.svg"); + background-repeat: repeat; + background-attachment: local; + background-position: center; + /* svg source is 6 onions wide and 2 onions tall */ + background-size: calc(6 * 2 * var(--onion-radius)) calc(2 * 2 * var(--onion-radius));; +} \ No newline at end of file diff --git a/browser/themes/shared/onionPattern.inc.xhtml b/browser/themes/shared/onionPattern.inc.xhtml new file mode 100644 index 0000000000000..de57b6ee301a3 --- /dev/null +++ b/browser/themes/shared/onionPattern.inc.xhtml @@ -0,0 +1,12 @@ +<!-- + Container div that holds onionPattern.svg + It is expected the includer of this xhtml file also includes onionPattern.css + and define the following vars: + onion-radius : radius of an onion + onion-color : the base color of the onion pattern + onion-opacity : the opacity of the entire repeating pattern +--> + +<div class="onion-pattern-container"> + <div class="onion-pattern-crop"/> +</div> \ No newline at end of file diff --git a/browser/themes/shared/onionPattern.svg b/browser/themes/shared/onionPattern.svg new file mode 100644 index 0000000000000..e2937b1753414 --- /dev/null +++ b/browser/themes/shared/onionPattern.svg @@ -0,0 +1,22 @@ +<svg fill="context-fill" viewBox="0 0 900 300" width="900" height="300" xmlns="http://www.w3.org/2000/svg"> + <g> + <path d="m825 0c41.421 0 75 33.5786 75 75 0 41.421-33.579 75-75 75z" fill-opacity=".3"/> + <path d="m750 0c41.421 0 75 33.5786 75 75 0 41.421-33.579 75-75 75z" fill-opacity=".15"/> + <path d="m525 225c0-41.421-33.579-75-75-75s-75 33.579-75 75z" fill-opacity=".3"/> + <path d="m525 300c0-41.421-33.579-75-75-75s-75 33.579-75 75z" fill-opacity=".15"/> + <path d="m300 0c0 41.4214-33.579 75-75 75s-75-33.5786-75-75z" fill-opacity=".3"/> + <path d="m300 75c0 41.421-33.579 75-75 75s-75-33.579-75-75z" fill-opacity=".15"/> + <g clip-rule="evenodd" fill-opacity=".3" fill-rule="evenodd"> + <path d="m525 .25c-.176 0-.351.000606-.527.001817-.966.006671-1.744.795563-1.737 1.762033.006.96648.795 1.74455 1.762 1.73788.167-.00115.334-.00173.502-.00173s.335.00058.502.00173c.967.00667 1.756-.7714 1.762-1.73788.007-.96647-.771-1.755363-1.737-1.762033-.176-.001211-.351-.001817-.527-.001817zm7.849.407251c-.962-.100329-1.822.597609-1.923 1.558879-.1.96128.598 1.82188 1.559 1.92221.333.03473.665.07174.996.11103.96.11381 1.83-.57199 1.944-1.53176s-.572-1.830084-1.532-1.94389 [...] + <path d="m3.75 75c0-39.3503 31.8997-71.25 71.25-71.25 39.35 0 71.25 31.8997 71.25 71.25 0 39.35-31.9 71.25-71.25 71.25-39.3503 0-71.25-31.9-71.25-71.25zm71.25-74.75c-41.2833 0-74.75 33.4667-74.75 74.75 0 41.283 33.4667 74.75 74.75 74.75 41.283 0 74.75-33.467 74.75-74.75 0-41.2833-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.5137 24.7363-55.25 55.25-55.25 30.514 0 55.25 24.7363 55.25 55.25 0 30.514-24.736 55.25-55.25 55.25-30.5137 0-55.25-24.736-55.25-55.25zm55.25-58.75c-32.446 [...] + <path d="m228.75 225c0-39.35 31.9-71.25 71.25-71.25s71.25 31.9 71.25 71.25-31.9 71.25-71.25 71.25-71.25-31.9-71.25-71.25zm71.25-74.75c-41.283 0-74.75 33.467-74.75 74.75s33.467 74.75 74.75 74.75 74.75-33.467 74.75-74.75-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.514 24.736-55.25 55.25-55.25s55.25 24.736 55.25 55.25-24.736 55.25-55.25 55.25-55.25-24.736-55.25-55.25zm55.25-58.75c-32.447 0-58.75 26.303-58.75 58.75s26.303 58.75 58.75 58.75 58.75-26.303 58.75-58.75-26.303-58.75-58 [...] + <path d="m828.75 225c0-39.35 31.9-71.25 71.25-71.25v-3.5c-41.283 0-74.75 33.467-74.75 74.75s33.467 74.75 74.75 74.75v-3.5c-39.35 0-71.25-31.9-71.25-71.25zm16 0c0-30.514 24.736-55.25 55.25-55.25v-3.5c-32.447 0-58.75 26.303-58.75 58.75s26.303 58.75 58.75 58.75v-3.5c-30.514 0-55.25-24.736-55.25-55.25zm55.25-39.25c-21.677 0-39.25 17.573-39.25 39.25s17.573 39.25 39.25 39.25v3.5c-23.61 0-42.75-19.14-42.75-42.75s19.14-42.75 42.75-42.75zm-22.25 39.25c0-12.288 9.962-22.25 22.25-22.25v [...] + <path d="m71.25 225c0-39.35-31.8997-71.25-71.25-71.25v-3.5c41.2833 0 74.75 33.467 74.75 74.75s-33.4667 74.75-74.75 74.75v-3.5c39.3503 0 71.25-31.9 71.25-71.25zm-16 0c0-30.514-24.7363-55.25-55.25-55.25v-3.5c32.4467 0 58.75 26.303 58.75 58.75s-26.3033 58.75-58.75 58.75v-3.5c30.5137 0 55.25-24.736 55.25-55.25zm-55.25-39.25c21.6772 0 39.25 17.573 39.25 39.25s-17.5728 39.25-39.25 39.25v3.5c23.6102 0 42.75-19.14 42.75-42.75s-19.1398-42.75-42.75-42.75zm22.25 39.25c0-12.288-9.9617-22 [...] + <path d="m303.75 75c0-39.3503 31.9-71.25 71.25-71.25s71.25 31.8997 71.25 71.25c0 39.35-31.9 71.25-71.25 71.25s-71.25-31.9-71.25-71.25zm71.25-74.75c-41.283 0-74.75 33.4667-74.75 74.75 0 41.283 33.467 74.75 74.75 74.75s74.75-33.467 74.75-74.75c0-41.2833-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.5137 24.736-55.25 55.25-55.25s55.25 24.7363 55.25 55.25c0 30.514-24.736 55.25-55.25 55.25s-55.25-24.736-55.25-55.25zm55.25-58.75c-32.447 0-58.75 26.3033-58.75 58.75 0 32.447 26.303 58. [...] + <path d="m603.75 75c0-39.3503 31.9-71.25 71.25-71.25s71.25 31.8997 71.25 71.25c0 39.35-31.9 71.25-71.25 71.25s-71.25-31.9-71.25-71.25zm71.25-74.75c-41.283 0-74.75 33.4667-74.75 74.75 0 41.283 33.467 74.75 74.75 74.75s74.75-33.467 74.75-74.75c0-41.2833-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.5137 24.736-55.25 55.25-55.25s55.25 24.7363 55.25 55.25c0 30.514-24.736 55.25-55.25 55.25s-55.25-24.736-55.25-55.25zm55.25-58.75c-32.447 0-58.75 26.3033-58.75 58.75 0 32.447 26.303 58. [...] + <path d="m150 150.25c-.878 0-1.753.015-2.624.045-.966.034-1.722.844-1.689 1.81.033.965.843 1.721 1.809 1.688.831-.029 1.666-.043 2.504-.043s1.673.014 2.504.043c.966.033 1.776-.723 1.809-1.688.033-.966-.723-1.776-1.689-1.81-.871-.03-1.746-.045-2.624-.045zm-11.449 4.415c.954-.154 1.603-1.053 1.449-2.007s-1.053-1.603-2.007-1.449c-1.735.281-3.45.621-5.143 1.018-.941.221-1.525 1.163-1.304 2.104s1.163 1.524 2.104 1.303c1.613-.378 3.248-.702 4.901-.969zm23.456-3.456c-.954-.154-1.853 [...] + <path d="m750 150.25c-.878 0-1.753.015-2.624.045-.966.034-1.722.844-1.689 1.81.033.965.843 1.721 1.809 1.688.831-.029 1.666-.043 2.504-.043s1.673.014 2.504.043c.966.033 1.776-.723 1.809-1.688.033-.966-.723-1.776-1.689-1.81-.871-.03-1.746-.045-2.624-.045zm-11.449 4.415c.954-.154 1.603-1.053 1.449-2.007s-1.053-1.603-2.007-1.449c-1.735.281-3.45.621-5.143 1.018-.941.221-1.525 1.163-1.304 2.104s1.163 1.524 2.104 1.303c1.613-.378 3.248-.702 4.901-.969zm23.456-3.456c-.954-.154-1.853 [...] + <path d="m528.75 225c0-39.35 31.9-71.25 71.25-71.25s71.25 31.9 71.25 71.25-31.9 71.25-71.25 71.25-71.25-31.9-71.25-71.25zm71.25-74.75c-41.283 0-74.75 33.467-74.75 74.75s33.467 74.75 74.75 74.75 74.75-33.467 74.75-74.75-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.514 24.736-55.25 55.25-55.25s55.25 24.736 55.25 55.25-24.736 55.25-55.25 55.25-55.25-24.736-55.25-55.25zm55.25-58.75c-32.447 0-58.75 26.303-58.75 58.75s26.303 58.75 58.75 58.75 58.75-26.303 58.75-58.75-26.303-58.75-58 [...] + </g> + </g> +</svg> \ No newline at end of file diff --git a/browser/themes/shared/urlbar-searchbar.inc.css b/browser/themes/shared/urlbar-searchbar.inc.css index 0158597991ecb..d7dc7df17f19d 100644 --- a/browser/themes/shared/urlbar-searchbar.inc.css +++ b/browser/themes/shared/urlbar-searchbar.inc.css @@ -747,3 +747,5 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left::after { }
%include ../../components/onionservices/content/onionlocation-urlbar.css +%include ../../components/torconnect/content/torconnect-urlbar.css + diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index 97a985fd557ac..1ce9472ebae2c 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -17168,9 +17168,56 @@ void Document::RemoveToplevelLoadingDocument(Document* aDoc) {
StylePrefersColorScheme Document::PrefersColorScheme( IgnoreRFP aIgnoreRFP) const { + + // tor-browser#27476 + // should this document ignore resist finger-printing settings with regards to + // setting the color scheme + // currently only enabled for about:torconnect but we could expand to other non- + // SystemPrincipal pages if we wish + const auto documentUsesPreferredColorScheme = [](auto const* constDocument) -> bool { + if (auto* document = const_cast<Document*>(constDocument); document != nullptr) { + auto uri = document->GetDocBaseURI(); + + // try and extract out our prepath and filepath portions of the uri to C-strings + nsAutoCString prePathStr, filePathStr; + if(NS_FAILED(uri->GetPrePath(prePathStr)) || + NS_FAILED(uri->GetFilePath(filePathStr))) { + return false; + } + + // stick them in string view for easy comparisons + std::string_view prePath(prePathStr.get(), prePathStr.Length()), + filePath(filePathStr.get(), filePathStr.Length()); + + // these about URIs will have the user's preferred color scheme exposed to them + // we can place other URIs here in the future if we wish + // see nsIURI.idl for URI part definitions + constexpr struct { + std::string_view prePath; + std::string_view filePath; + } allowedURIs[] = { + { "about:", "torconnect" }, + }; + + // check each uri in the allow list against this document's uri + // verify the prepath and the file path match + for(auto const& uri : allowedURIs) { + if (prePath == uri.prePath && + filePath == uri.filePath) { + // positive match means we can apply dark-mode to the page + return true; + } + } + } + + // do not allow if no match or other error + return false; + }; + if (aIgnoreRFP == IgnoreRFP::No && - nsContentUtils::ShouldResistFingerprinting(this)) { - return StylePrefersColorScheme::Light; + nsContentUtils::ShouldResistFingerprinting(this) && + !documentUsesPreferredColorScheme(this)) { + return StylePrefersColorScheme::Light; }
if (auto* bc = GetBrowsingContext()) { diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index 4da5365f214d8..e981573e9822b 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -6213,6 +6213,8 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) { NS_ENSURE_SUCCESS_VOID(rv);
if (!StringBeginsWith(url, u"about:neterror"_ns) && + // we want about:torconnect pages to be able to close themselves after bootstrap + !StringBeginsWith(url, u"about:torconnect"_ns) && !mBrowsingContext->HadOriginalOpener() && !aTrustedCaller && !IsOnlyTopLevelDocumentInSHistory()) { bool allowClose = diff --git a/toolkit/components/processsingleton/MainProcessSingleton.jsm b/toolkit/components/processsingleton/MainProcessSingleton.jsm index ecdbf2a01d99a..62afa98e1ffc8 100644 --- a/toolkit/components/processsingleton/MainProcessSingleton.jsm +++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm @@ -24,6 +24,11 @@ MainProcessSingleton.prototype = { null );
+ ChromeUtils.import( + "resource:///modules/TorConnect.jsm", + null + ); + Services.ppmm.loadProcessScript( "chrome://global/content/process-content.js", true diff --git a/toolkit/modules/AsyncPrefs.jsm b/toolkit/modules/AsyncPrefs.jsm index 2834c484c9197..f7d867e47dc0d 100644 --- a/toolkit/modules/AsyncPrefs.jsm +++ b/toolkit/modules/AsyncPrefs.jsm @@ -20,6 +20,7 @@ const kAllowedPrefs = new Set([
"browser.contentblocking.report.hide_vpn_banner", "browser.contentblocking.report.show_mobile_app", + "extensions.torlauncher.quickstart",
"narrate.rate", "narrate.voice", diff --git a/toolkit/modules/RemotePageAccessManager.jsm b/toolkit/modules/RemotePageAccessManager.jsm index c12e71ac4d425..5125203866b80 100644 --- a/toolkit/modules/RemotePageAccessManager.jsm +++ b/toolkit/modules/RemotePageAccessManager.jsm @@ -103,6 +103,7 @@ let RemotePageAccessManager = { RPMGetInnerMostURI: ["*"], RPMGetHttpResponseHeader: ["*"], RPMGetTorStrings: ["*"], + RPMSendQuery: ["ShouldShowTorConnect"], }, "about:plugins": { RPMSendQuery: ["RequestPlugins"], @@ -219,6 +220,21 @@ let RemotePageAccessManager = { "FetchUpdateData", ], }, + "about:torconnect": { + RPMAddMessageListener: [ + "torconnect:state-change", + ], + RPMSendAsyncMessage: [ + "torconnect:open-tor-preferences", + "torconnect:begin-bootstrap", + "torconnect:cancel-bootstrap", + "torconnect:set-quickstart", + ], + RPMSendQuery: [ + "torconnect:get-init-args", + "torconnect:copy-tor-logs", + ], + }, },
/** diff --git a/toolkit/mozapps/update/UpdateService.jsm b/toolkit/mozapps/update/UpdateService.jsm index f4f9259920275..f0a48d0216386 100644 --- a/toolkit/mozapps/update/UpdateService.jsm +++ b/toolkit/mozapps/update/UpdateService.jsm @@ -12,6 +12,17 @@ const { AppConstants } = ChromeUtils.import( const { AUSTLMY } = ChromeUtils.import( "resource://gre/modules/UpdateTelemetry.jsm" ); + +const { TorProtocolService } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); + +function _shouldRegisterBootstrapObserver(errorCode) { + return errorCode == PROXY_SERVER_CONNECTION_REFUSED && + !TorProtocolService.isBootstrapDone() && + TorProtocolService.ownsTorDaemon; +}; + const { Bits, BitsRequest, @@ -232,6 +243,7 @@ const SERVICE_ERRORS = [ // Custom update error codes const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110; const NETWORK_ERROR_OFFLINE = 111; +const PROXY_SERVER_CONNECTION_REFUSED = 2152398920;
// Error codes should be < 1000. Errors above 1000 represent http status codes const HTTP_ERROR_OFFSET = 1000; @@ -2676,6 +2688,9 @@ UpdateService.prototype = { case "network:offline-status-changed": this._offlineStatusChanged(data); break; + case "torconnect:bootstrap-complete": + this._bootstrapComplete(); + break; case "nsPref:changed": if (data == PREF_APP_UPDATE_LOG || data == PREF_APP_UPDATE_LOG_FILE) { gLogEnabled; // Assigning this before it is lazy-loaded is an error. @@ -3169,6 +3184,35 @@ UpdateService.prototype = { this._attemptResume(); },
+ _registerBootstrapObserver: function AUS__registerBootstrapObserver() { + if (this._registeredBootstrapObserver) { + LOG( + "UpdateService:_registerBootstrapObserver - observer already registered" + ); + return; + } + + LOG( + "UpdateService:_registerBootstrapObserver - waiting for tor bootstrap to " + + "be complete, then forcing another check" + ); + + Services.obs.addObserver(this, "torconnect:bootstrap-complete"); + this._registeredBootstrapObserver = true; + }, + + _bootstrapComplete: function AUS__bootstrapComplete() { + Services.obs.removeObserver(this, "torconnect:bootstrap-complete"); + this._registeredBootstrapObserver = false; + + LOG( + "UpdateService:_bootstrapComplete - bootstrapping complete, forcing " + + "another background check" + ); + + this._attemptResume(); + }, + onCheckComplete: function AUS_onCheckComplete(request, updates) { this._selectAndInstallUpdate(updates); }, @@ -3188,6 +3232,11 @@ UpdateService.prototype = { AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE); } return; + } else if (_shouldRegisterBootstrapObserver(update.errorCode)) { + // Register boostrap observer to try again, but only when we own the + // tor process. + this._registerBootstrapObserver(); + return; }
// Send the error code to telemetry @@ -6011,6 +6060,7 @@ Downloader.prototype = { var state = this._patch.state; var shouldShowPrompt = false; var shouldRegisterOnlineObserver = false; + var shouldRegisterBootstrapObserver = false; var shouldRetrySoon = false; var deleteActiveUpdate = false; let migratedToReadyUpdate = false; @@ -6129,7 +6179,18 @@ Downloader.prototype = { ); shouldRegisterOnlineObserver = true; deleteActiveUpdate = false; - + } else if(_shouldRegisterBootstrapObserver(status)) { + // Register a bootstrap observer to try again. + // The bootstrap observer will continue the incremental download by + // calling downloadUpdate on the active update which continues + // downloading the file from where it was. + LOG("Downloader:onStopRequest - not bootstrapped, register bootstrap observer: true"); + AUSTLMY.pingDownloadCode( + this.isCompleteUpdate, + AUSTLMY.DWNLD_RETRY_OFFLINE + ); + shouldRegisterBootstrapObserver = true; + deleteActiveUpdate = false; // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED, // NS_ERROR_NET_RESET and NS_ERROR_DOCUMENT_NOT_CACHED can be returned // when disconnecting the internet while a download of a MAR is in @@ -6251,7 +6312,7 @@ Downloader.prototype = {
// Only notify listeners about the stopped state if we // aren't handling an internal retry. - if (!shouldRetrySoon && !shouldRegisterOnlineObserver) { + if (!shouldRetrySoon && !shouldRegisterOnlineObserver && !shouldRegisterBootstrapObserver) { this.updateService.forEachDownloadListener(listener => { listener.onStopRequest(request, status); }); @@ -6437,6 +6498,9 @@ Downloader.prototype = { if (shouldRegisterOnlineObserver) { LOG("Downloader:onStopRequest - Registering online observer"); this.updateService._registerOnlineObserver(); + } else if (shouldRegisterBootstrapObserver) { + LOG("Downloader:onStopRequest - Registering bootstrap observer"); + this.updateService._registerBootstrapObserver(); } else if (shouldRetrySoon) { LOG("Downloader:onStopRequest - Retrying soon"); this.updateService._consecutiveSocketErrors++; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js index 2ff107b553b2a..f8fa83574df70 100644 --- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js @@ -70,6 +70,10 @@ function getGlobalScriptIncludes(scriptPath) { let match = line.match(globalScriptsRegExp); if (match) { let sourceFile = match[1] + .replace( + "chrome://browser/content/torconnect/", + "browser/components/torconnect/content/" + ) .replace( "chrome://browser/content/search/", "browser/components/search/content/"