This is an automated email from the git hooks/post-receive script.
richard pushed a commit to branch tor-browser-91.8.0esr-11.5-1 in repository tor-browser.
commit 849fa16538789024ffbb1a097ee769134b441cb5 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 - 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 | 2 +- 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 | 14 + browser/components/BrowserGlue.jsm | 14 + browser/components/about/AboutRedirector.cpp | 4 + browser/components/about/components.conf | 1 + browser/components/moz.build | 1 + browser/components/sessionstore/SessionStore.jsm | 4 + browser/components/torconnect/TorConnectChild.jsm | 9 + browser/components/torconnect/TorConnectParent.jsm | 147 ++++++++++ .../torconnect/content/aboutTorConnect.css | 180 +++++++++++++ .../torconnect/content/aboutTorConnect.js | 298 +++++++++++++++++++++ .../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 + browser/components/urlbar/UrlbarInput.jsm | 32 +++ browser/modules/TorProcessService.jsm | 12 + browser/modules/TorStrings.jsm | 80 ++++++ browser/modules/moz.build | 2 + browser/themes/shared/urlbar-searchbar.inc.css | 3 + dom/base/Document.cpp | 51 +++- dom/base/nsGlobalWindowOuter.cpp | 2 + toolkit/actors/AboutHttpsOnlyErrorParent.jsm | 5 + .../components/httpsonlyerror/content/errorpage.js | 19 +- .../processsingleton/MainProcessSingleton.jsm | 5 + toolkit/modules/RemotePageAccessManager.jsm | 17 ++ toolkit/mozapps/update/UpdateService.jsm | 68 ++++- .../lib/environments/browser-window.js | 4 + 37 files changed, 1257 insertions(+), 33 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 2846a1cb2fcfd..6901ce71814a3 100644 --- a/browser/base/content/browser-siteIdentity.js +++ b/browser/base/content/browser-siteIdentity.js @@ -57,7 +57,7 @@ var gIdentityHandler = { * RegExp used to decide if an about url should be shown as being part of * the browser UI. */ - _secureInternalPages: /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback)(?:[?#]|$)/i, + _secureInternalPages: /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|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 566976b6d7aa0..ef8a191987676 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -79,6 +79,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", UITour: "resource:///modules/UITour.jsm", UpdateUtils: "resource://gre/modules/UpdateUtils.jsm", @@ -633,6 +634,7 @@ var gPageIcons = {
var gInitialPages = [ "about:tor", + "about:torconnect", "about:blank", "about:newtab", "about:home", @@ -1837,6 +1839,8 @@ var gBrowserInit = { }
this._loadHandled = true; + + TorBootstrapUrlbar.init(); },
_cancelDelayedStartup() { @@ -2385,32 +2389,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; })()); },
@@ -2473,6 +2493,8 @@ var gBrowserInit = {
DownloadsButton.uninit();
+ TorBootstrapUrlbar.uninit(); + gAccessibilityServiceIndicator.uninit();
if (gToolbarKeyNavEnabled) { diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml index 8efb544918b8e..f163073657286 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 @@ -110,6 +111,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 31c4838a053da..edf97c2a5daf3 100644 --- a/browser/base/content/certerror/aboutNetError.js +++ b/browser/base/content/certerror/aboutNetError.js @@ -239,7 +239,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 @@ -258,6 +258,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 02636a6b46b5c..e7f63116ff39e 100644 --- a/browser/base/content/navigator-toolbox.inc.xhtml +++ b/browser/base/content/navigator-toolbox.inc.xhtml @@ -330,6 +330,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 3b14beaa5b1e3..a95717544b80f 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,19 @@ 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 + // + // fixes tor-browser#40752 + // new tabs also redirect to about:tor if browser.newtabpage.enabled is true + // otherwise they go to about:blank + if (TorConnect.shouldShowTorConnect) { + if (url === "about:tor" || + (url === "about:newtab" && Services.prefs.getBoolPref("browser.newtabpage.enabled", false))) { + url = TorConnect.getRedirectURL(url); + } + } + var params;
if (arguments.length == 3 && typeof arguments[2] == "object") { diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index 9dfad0358ed75..dc956bc796166 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -703,6 +703,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", diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp index 6d283fe67b206..21f673f601d26 100644 --- a/browser/components/about/AboutRedirector.cpp +++ b/browser/components/about/AboutRedirector.cpp @@ -122,6 +122,10 @@ static const RedirEntry kRedirMap[] = { nsIAboutModule::HIDE_FROM_ABOUTABOUT}, {"restartrequired", "chrome://browser/content/aboutRestartRequired.xhtml", nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::HIDE_FROM_ABOUTABOUT}, + {"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 8ce22e9cff519..733abef1a80f6 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 66de87290bd83..d15ff30515936 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -53,6 +53,7 @@ DIRS += [ "syncedtabs", "uitour", "urlbar", + "torconnect", "torpreferences", "translation", ] 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..2fbc2a5c7c7c7 --- /dev/null +++ b/browser/components/torconnect/TorConnectParent.jsm @@ -0,0 +1,147 @@ +// 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 { TorSettings, TorSettingsTopics, TorSettingsData } = ChromeUtils.import( + "resource:///modules/TorSettings.jsm" +); + +/* +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: TorSettings.quickstart.enabled, + }; + + // 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 TorSettingsTopics.SettingChanged:{ + if (aData === TorSettingsData.QuickStartEnabled) { + self.state.QuickStartEnabled = obj.value; + } else { + // this isn't a setting torconnect cares about + return; + } + 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.obs.addObserver(this.torConnectObserver, TorSettingsTopics.SettingChanged); + } + + willDestroy() { + // stop observing all of our torconnect:.* topics + for (const key in TorConnectTopics) { + const topic = TorConnectTopics[key]; + Services.obs.removeObserver(this.torConnectObserver, topic); + } + Services.obs.removeObserver(this.torConnectObserver, TorSettingsTopics.SettingChanged); + } + + receiveMessage(message) { + switch (message.name) { + case "torconnect:set-quickstart": + TorSettings.quickstart.enabled = message.data; + TorSettings.saveToPrefs().applySettings(); + 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..26b17afb69383 --- /dev/null +++ b/browser/components/torconnect/content/aboutTorConnect.js @@ -0,0 +1,298 @@ +// 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_AutoBootstrapping(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_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/urlbar/UrlbarInput.jsm b/browser/components/urlbar/UrlbarInput.jsm index b2f76bdee2f4a..db83e09109bf2 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", @@ -2418,6 +2446,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/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/TorStrings.jsm b/browser/modules/TorStrings.jsm index 1e08b168e4afb..96d3de8186e23 100644 --- a/browser/modules/TorStrings.jsm +++ b/browser/modules/TorStrings.jsm @@ -257,6 +257,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", @@ -364,6 +367,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"), + 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 bc543283d887b..a06914ccf8d9e 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -154,6 +154,8 @@ EXTRA_JS_MODULES += [ "TabsList.jsm", "TabUnloader.jsm", "ThemeVariableMap.jsm", + 'TorConnect.jsm', + 'TorProcessService.jsm', "TorProtocolService.jsm", "TorSettings.jsm", "TorStrings.jsm", diff --git a/browser/themes/shared/urlbar-searchbar.inc.css b/browser/themes/shared/urlbar-searchbar.inc.css index 82675dae2041b..f91278ce5ed3b 100644 --- a/browser/themes/shared/urlbar-searchbar.inc.css +++ b/browser/themes/shared/urlbar-searchbar.inc.css @@ -745,3 +745,6 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left::after { .searchbar-textbox::placeholder { opacity: 0.69; } + +%include ../../components/torconnect/content/torconnect-urlbar.css + diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index 05d2a91ad1e24..8beb7fa8e9f27 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -17105,9 +17105,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 41c93c51cf3b1..aab4a37e78a8f 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -6212,6 +6212,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/actors/AboutHttpsOnlyErrorParent.jsm b/toolkit/actors/AboutHttpsOnlyErrorParent.jsm index b949cb065a583..407e014ff6574 100644 --- a/toolkit/actors/AboutHttpsOnlyErrorParent.jsm +++ b/toolkit/actors/AboutHttpsOnlyErrorParent.jsm @@ -15,6 +15,8 @@ const { SessionStore } = ChromeUtils.import( "resource:///modules/sessionstore/SessionStore.jsm" );
+const { TorConnect } = ChromeUtils.import("resource:///modules/TorConnect.jsm"); + class AboutHttpsOnlyErrorParent extends JSWindowActorParent { get browser() { return this.browsingContext.top.embedderElement; @@ -28,7 +30,10 @@ class AboutHttpsOnlyErrorParent extends JSWindowActorParent { case "openInsecure": this.openWebsiteInsecure(this.browser, aMessage.data.inFrame); break; + case "ShouldShowTorConnect": + return TorConnect.shouldShowTorConnect; } + return undefined; }
goBackFromErrorPage(aWindow) { diff --git a/toolkit/components/httpsonlyerror/content/errorpage.js b/toolkit/components/httpsonlyerror/content/errorpage.js index 1dc8c827789b4..cdb269346eeb9 100644 --- a/toolkit/components/httpsonlyerror/content/errorpage.js +++ b/toolkit/components/httpsonlyerror/content/errorpage.js @@ -124,8 +124,17 @@ function addAutofocus(selector, position = "afterbegin") {
/* Initialize Page */
-initPage(); -// Dispatch this event so tests can detect that we finished loading the error page. -// We're using the same event name as neterror because BrowserTestUtils.jsm relies on that. -let event = new CustomEvent("AboutNetErrorLoad", { bubbles: true }); -document.dispatchEvent(event); +RPMSendQuery("ShouldShowTorConnect").then(shouldShow => { + if (shouldShow) { + // pass orginal destination as redirect param + const encodedRedirect = encodeURIComponent(document.location.href); + document.location.replace(`about:torconnect?redirect=${encodedRedirect}`); + return; + } + + initPage(); + // Dispatch this event so tests can detect that we finished loading the error page. + // We're using the same event name as neterror because BrowserTestUtils.jsm relies on that. + let event = new CustomEvent("AboutNetErrorLoad", { bubbles: true }); + document.dispatchEvent(event); +}); diff --git a/toolkit/components/processsingleton/MainProcessSingleton.jsm b/toolkit/components/processsingleton/MainProcessSingleton.jsm index 7bde782e54ce8..ba8cd0f3f97d3 100644 --- a/toolkit/components/processsingleton/MainProcessSingleton.jsm +++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm @@ -29,6 +29,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/RemotePageAccessManager.jsm b/toolkit/modules/RemotePageAccessManager.jsm index 50fb4ea8d4179..6912e27c20d0f 100644 --- a/toolkit/modules/RemotePageAccessManager.jsm +++ b/toolkit/modules/RemotePageAccessManager.jsm @@ -67,6 +67,7 @@ let RemotePageAccessManager = { RPMAddMessageListener: ["WWWReachable"], RPMTryPingSecureWWWLink: ["*"], RPMOpenSecureWWWLink: ["*"], + RPMSendQuery: ["ShouldShowTorConnect"], }, "about:certificate": { RPMSendQuery: ["getCertificates"], @@ -102,6 +103,7 @@ let RemotePageAccessManager = { RPMAddToHistogram: ["*"], RPMGetInnerMostURI: ["*"], RPMGetHttpResponseHeader: ["*"], + RPMSendQuery: ["ShouldShowTorConnect"], }, "about:plugins": { RPMSendQuery: ["RequestPlugins"], @@ -213,6 +215,21 @@ let RemotePageAccessManager = { RPMAddMessageListener: ["*"], RPMRemoveMessageListener: ["*"], }, + "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 4d1b1c59eff5c..cd87b21b0ff9c 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, @@ -228,6 +239,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; @@ -2613,6 +2625,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. @@ -3063,6 +3078,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); }, @@ -3082,6 +3126,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 @@ -5843,6 +5892,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; @@ -5961,7 +6011,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 @@ -6083,7 +6144,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); }); @@ -6269,6 +6330,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/"