This is an automated email from the git hooks/post-receive script.
richard pushed a commit to branch tor-browser-102.2.0esr-12.0-3 in repository tor-browser.
commit 9a429ced6e033c96bdd1b1f82d1ea1be8b1fbb47 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)
Bug 40773: Update the about:torconnect frontend page to match additional UI flows --- browser/actors/NetErrorParent.jsm | 6 + 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 | 17 + browser/components/BrowserGlue.jsm | 14 + browser/components/about/AboutRedirector.cpp | 5 + browser/components/about/components.conf | 1 + browser/components/moz.build | 1 + browser/components/torconnect/TorConnectChild.jsm | 9 + browser/components/torconnect/TorConnectParent.jsm | 202 +++++ .../torconnect/content/aboutTorConnect.css | 324 ++++++++ .../torconnect/content/aboutTorConnect.js | 843 +++++++++++++++++++++ .../torconnect/content/aboutTorConnect.xhtml | 68 ++ .../components/torconnect/content/arrow-right.svg | 4 + browser/components/torconnect/content/bridge.svg | 5 + .../torconnect/content/connection-failure.svg | 5 + .../torconnect/content/connection-location.svg | 5 + .../torconnect/content/onion-slash-fillable.svg | 5 + .../components/torconnect/content/onion-slash.svg | 5 + browser/components/torconnect/content/onion.svg | 4 + .../torconnect/content/torBootstrapUrlbar.js | 95 +++ .../torconnect/content/torconnect-urlbar.css | 37 + .../torconnect/content/torconnect-urlbar.inc.xhtml | 10 + browser/components/torconnect/jar.mn | 13 + browser/components/torconnect/moz.build | 6 + browser/components/urlbar/UrlbarInput.jsm | 37 + browser/modules/TorProcessService.jsm | 12 + browser/modules/moz.build | 2 + browser/themes/shared/browser-shared.css | 1 + dom/base/Document.cpp | 49 +- dom/base/nsGlobalWindowOuter.cpp | 2 + toolkit/actors/AboutHttpsOnlyErrorParent.jsm | 5 + .../components/httpsonlyerror/content/errorpage.js | 19 +- .../processsingleton/MainProcessSingleton.jsm | 1 + toolkit/modules/RemotePageAccessManager.jsm | 26 + .../lib/environments/browser-window.js | 4 + 38 files changed, 1894 insertions(+), 29 deletions(-)
diff --git a/browser/actors/NetErrorParent.jsm b/browser/actors/NetErrorParent.jsm index 315957ff112e1..250b95b950592 100644 --- a/browser/actors/NetErrorParent.jsm +++ b/browser/actors/NetErrorParent.jsm @@ -18,6 +18,8 @@ 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.", @@ -341,6 +343,10 @@ class NetErrorParent extends JSWindowActorParent { break; } } + break; + case "ShouldShowTorConnect": + return TorConnect.shouldShowTorConnect; } + return undefined; } } diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 29ca57d1c728d..01b3b704a4037 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", @@ -646,6 +647,7 @@ var gPageIcons = {
var gInitialPages = [ "about:tor", + "about:torconnect", "about:blank", "about:home", ...(AppConstants.NIGHTLY_BUILD ? ["about:firefoxview"] : []), @@ -1874,6 +1876,8 @@ var gBrowserInit = { }
this._loadHandled = true; + + TorBootstrapUrlbar.init(); },
_cancelDelayedStartup() { @@ -2421,32 +2425,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 = (aUri => { + // If the given URI is different from the homepage, we want to load it. + if (aUri != defaultArgs) { + AboutNewTab.noteNonDefaultStartup(); + + if (aUri instanceof Ci.nsIArray) { + // Transform the nsIArray of nsISupportsString's into a JS Array of + // JS strings. + return Array.from( + aUri.enumerate(Ci.nsISupportsString), + supportStr => supportStr.data + ); + } else if (aUri instanceof Ci.nsISupportsString) { + return aUri.data; + } + return aUri; + }
- 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(aUri => { + if (aUri == null) { + aUri = []; + }
- // 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; + aUri = TorConnect.getURIsToLoad(aUri); + return aUri; + }); } - return willOverride.then(willOverrideHomepage => - willOverrideHomepage ? null : uri - ); + return uri; })()); },
@@ -2513,6 +2533,8 @@ var gBrowserInit = {
NewIdentityButton.uninit();
+ TorBootstrapUrlbar.uninit(); + gAccessibilityServiceIndicator.uninit();
if (gToolbarKeyNavEnabled) { diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml index cb29fa7bd2803..32d971c93d6de 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 @@ -122,6 +123,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 43a1777a8b414..1714b1b8a4be9 100644 --- a/browser/base/content/certerror/aboutNetError.js +++ b/browser/base/content/certerror/aboutNetError.js @@ -230,7 +230,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 @@ -253,6 +253,16 @@ function initPage() { document.body.classList.add("blocked"); }
+ // 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}`); + } + // Only worry about captive portals if this is a cert error. let showCaptivePortalUI = isCaptive() && gIsCertError; if (showCaptivePortalUI) { diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml index 3037bde088d8d..3cd9f35f5fb73 100644 --- a/browser/base/content/navigator-toolbox.inc.xhtml +++ b/browser/base/content/navigator-toolbox.inc.xhtml @@ -322,6 +322,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 ed750853a1d37..923f67335ba9f 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", () => @@ -259,6 +260,22 @@ 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 b1a0fc5ffe442..0092d25948e79 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -740,6 +740,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 7c915f2b45b0e..0a495a223e3eb 100644 --- a/browser/components/about/AboutRedirector.cpp +++ b/browser/components/about/AboutRedirector.cpp @@ -133,6 +133,11 @@ 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 | + nsIAboutModule::IS_SECURE_CHROME_UI}, };
static nsAutoCString GetAboutModuleName(nsIURI* aURI) { diff --git a/browser/components/about/components.conf b/browser/components/about/components.conf index 56226324bc326..6095653409cd8 100644 --- a/browser/components/about/components.conf +++ b/browser/components/about/components.conf @@ -28,6 +28,7 @@ pages = [ 'robots', 'sessionrestore', 'tabcrashed', + 'torconnect', 'unloads', 'welcome', 'welcomeback', diff --git a/browser/components/moz.build b/browser/components/moz.build index 0fa76a0e70382..ab1dac40dfabe 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -58,6 +58,7 @@ DIRS += [ "translation", "uitour", "urlbar", + "torconnect", "torpreferences", ]
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..cf3c1233dd622 --- /dev/null +++ b/browser/components/torconnect/TorConnectParent.jsm @@ -0,0 +1,202 @@ +// 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 { + InternetStatus, + TorConnect, + TorConnectTopics, + TorConnectState, +} = ChromeUtils.import("resource:///modules/TorConnect.jsm"); +const { TorSettings, TorSettingsTopics, TorSettingsData } = ChromeUtils.import( + "resource:///modules/TorSettings.jsm" +); + +const BroadcastTopic = "about-torconnect:broadcast"; + +/* +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, + PreviousState: TorConnectState.Initial, + ErrorMessage: TorConnect.errorMessage, + ErrorDetails: TorConnect.errorDetails, + BootstrapProgress: TorConnect.bootstrapProgress, + BootstrapStatus: TorConnect.bootstrapStatus, + InternetStatus: TorConnect.internetStatus, + DetectedLocation: TorConnect.detectedLocation, + ShowViewLog: TorConnect.logHasWarningOrError, + HasBootsrapEverFailed: TorConnect.hasBootstrapEverFailed, + QuickStartEnabled: TorSettings.quickstart.enabled, + UIState: TorConnect.uiState, + }; + + // 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.PreviousState = self.state.State; + 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; + } + self.state.HasBootsrapEverFailed = + TorConnect.hasBootstrapEverFailed; + break; + } + case TorConnectTopics.BootstrapProgress: { + self.state.BootstrapProgress = obj.progress; + self.state.BootstrapStatus = obj.status; + self.state.ShowViewLog = obj.hasWarnings; + break; + } + case TorConnectTopics.BootstrapComplete: { + // noop + break; + } + case TorConnectTopics.BootstrapError: { + self.state.ErrorMessage = obj.message; + self.state.ErrorDetails = obj.details; + self.state.InternetStatus = TorConnect.internetStatus; + self.state.DetectedLocation = TorConnect.detectedLocation; + self.state.ShowViewLog = 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 + ); + + this.userActionObserver = { + observe(aSubject, aTopic, aData) { + let obj = aSubject?.wrappedJSObject; + if (obj) { + obj.connState = self.state; + self.sendAsyncMessage("torconnect:user-action", obj); + } + }, + }; + Services.obs.addObserver(this.userActionObserver, BroadcastTopic); + } + + 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 + ); + Services.obs.removeObserver(this.userActionObserver, BroadcastTopic); + } + + async 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:cancel-bootstrap": + TorConnect.cancelBootstrap(); + break; + case "torconnect:begin-bootstrap": + TorConnect.beginBootstrap(); + break; + case "torconnect:begin-autobootstrap": + TorConnect.beginAutoBootstrap(message.data); + break; + case "torconnect:view-tor-logs": + TorConnect.viewTorLogs(); + break; + case "torconnect:restart": + Services.startup.quit( + Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit + ); + break; + case "torconnect:set-ui-state": + TorConnect.uiState = message.data; + this.state.UIState = TorConnect.uiState; + break; + case "torconnect:broadcast-user-action": + Services.obs.notifyObservers(message.data, BroadcastTopic); + 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; + this.state.UIState = TorConnect.uiState; + return { + TorStrings, + TorConnectState, + InternetStatus, + Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr", + State: this.state, + CountryNames: TorConnect.countryNames, + }; + case "torconnect:get-country-codes": + return TorConnect.getCountryCodes(); + } + return undefined; + } +} diff --git a/browser/components/torconnect/content/aboutTorConnect.css b/browser/components/torconnect/content/aboutTorConnect.css new file mode 100644 index 0000000000000..5930f0948291a --- /dev/null +++ b/browser/components/torconnect/content/aboutTorConnect.css @@ -0,0 +1,324 @@ + +/* 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; +} + +input[type="checkbox"]:focus, select: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) { + input[type="checkbox"]:focus, select:focus { + box-shadow: 0 0 0 3px var(--purple-50)!important; + } +} + +#breadcrumbs { + display: flex; + align-items: center; + margin: 0 0 24px 0; + color: var(--grey-40); +} + +#breadcrumbs.hidden { + visibility: hidden; +} + +.breadcrumb-item, .breadcrumb-separator { + display: flex; + margin: 0; + margin-inline-start: 20px; + padding: 8px; +} + +.breadcrumb-item { + align-items: center; + cursor: pointer; + color: var(--in-content-text-color); + border-radius: 4px; +} + +.breadcrumb-item:hover { + color: var(--in-content-accent-color); + background-color: var(--in-content-button-background-hover); +} + +.breadcrumb-item:active { + color: var(--in-content-accent-color-active); + background-color: var(--in-content-button-background-active); +} + +.breadcrumb-separator { + width: 15px; + list-style-image: url("chrome://browser/content/torconnect/arrow-right.svg"); +} + +.breadcrumb-separator:dir(rtl) { + scale: -1 1; +} + +.breadcrumb-icon { + display: inline list-item; + height: 16px; + list-style-position: inside; + fill: currentColor; + -moz-context-properties: fill; +} + +.breadcrumb-item.active { + color: var(--in-content-accent-color); +} + +.breadcrumb-item.disabled, .breadcrumb-item.disabled:hover, .breadcrumb-item.disabled:active { + color: var(--in-content-text-color); + opacity: 0.4; + cursor: default; +} + +.breadcrumb-item.error { + color: var(--in-content-danger-button-background); +} + +.breadcrumb-item.error:hover { + color: var(--in-content-danger-button-background-hover); +} + +.breadcrumb-item.error:active { + color: var(--in-content-danger-button-background-active); +} + +.breadcrumb-item.hidden, .breadcrumb-separator.hidden { + display: none; +} + +#connect-to-tor { + margin-inline-start: 0; +} + +#connect-to-tor-icon { + list-style-image: url("chrome://browser/content/torconnect/onion.svg"); +} + +#connection-assist-icon { + list-style-image: url("chrome://browser/content/torconnect/onion-slash-fillable.svg"); +} + +#location-settings-icon { + list-style-image: url("chrome://browser/content/torconnect/globe.svg"); +} + +#try-bridge { + cursor: default; +} + +#try-bridge-icon { + list-style-image: url("chrome://browser/content/torconnect/bridge.svg"); +} + +button { + --purple-button-text-color: rgb(251,251,254); + --in-content-primary-button-text-color: var(--purple-button-text-color); + --in-content-primary-button-background: var(--purple-60); + --in-content-primary-button-text-color-hover: var(--purple-button-text-color); + --in-content-primary-button-background-hover: var(--purple-70); + --in-content-primary-button-text-color-active: var(--purple-button-text-color); + --in-content-primary-button-background-active: var(--purple-80); + --in-content-focus-outline-color: var(--purple-60); + fill: white; +} + +#locationDropdownLabel { + margin-block: auto; + margin-inline: 4px; +} + +#locationDropdownLabel.error { + color: var(--in-content-danger-button-background) +} + +/* this follows similar css in error-pages.css for buttons */ +@media only screen and (min-width: 480px) { + form#locationDropdown { + margin-inline: 4px; + /* subtracting out the margin is needeed because by + default forms have different margins than buttons */ + max-width: calc(100% - 8px); + } +} + +@media only screen and (max-width: 480px) { + #tryAgainButton { + margin-top: 4px; + } +} + +form#locationDropdown { + width: 240px; +} + +form#locationDropdown select { + max-width: 100%; + padding-block: 0; + margin-inline: 0; + font-weight: 700; +} + +/* 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; +} + +:root { + --progressbar-shadow-start: rgba(255, 255, 255, 0.7); + --progressbar-gradient: linear-gradient(90deg, #FC00FF 0%, #00DBDE 50%, #FC00FF 100%); +} + +@media (prefers-color-scheme: dark) { + :root { + --progressbar-shadow-start: rgba(28, 27, 34, 0.7); + } +} + +#progressBar { + position: fixed; + top: 0; + inset-inline-start: 0; + width: 0%; + padding: 0; + margin: 0; + animation: progressAnimation 5s ease infinite; +} + +#progressBackground { + height: 66px; + margin-top: -26px; + background-image: + linear-gradient(var(--progressbar-shadow-start), var(--in-content-page-background) 100%), + var(--progressbar-gradient); + background-position: inherit; + filter: blur(5px); + border-end-end-radius: 33px; +} + +#progressSolid { + position: absolute; + top: 0; + width: 100%; + height: 7px; + background-image: var(--progressbar-gradient); + background-position: inherit; +} + +#progressBackground, #progressSolid { + background-size: 200% 100%; +} + +@keyframes progressAnimation { + 0% { + background-position: 200%; + } + 50% { + background-position: 100%; + } + 100% { + background-position: 0%; + } +} + +@keyframes progressAnimation { + 0% { + background-position: 200%; + } + 50% { + background-position: 100%; + } + 100% { + background-position: 0%; + } +} + +#connectPageContainer { + margin-top: 10vh; + width: 100%; + max-width: 45em; +} + +#quickstartCheckbox, #quickstartCheckboxLabel { + vertical-align: middle; +} + +/* mirrors p element spacing */ +#viewLogContainer { + margin: 1em 0; + height: 1.2em; + min-height: 1.2em; +} + +#viewLogLink { + position: relative; + display: inline-block; + color: var(--in-content-link-color); +} + +/* hidden apparently only works if no display is set; who knew? */ +#viewLogLink[hidden="true"] { + display: none; +} + +#viewLogLink:hover { + cursor:pointer; +} + +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.offline, .title.assist, .title.final { + background-image: url("chrome://browser/content/torconnect/connection-failure.svg"); +} + +.title.location { + background-image: url("chrome://browser/content/torconnect/connection-location.svg"); +} diff --git a/browser/components/torconnect/content/aboutTorConnect.js b/browser/components/torconnect/content/aboutTorConnect.js new file mode 100644 index 0000000000000..6b33442c7d02b --- /dev/null +++ b/browser/components/torconnect/content/aboutTorConnect.js @@ -0,0 +1,843 @@ +// Copyright (c) 2021, The Tor Project, Inc. + +/* eslint-env mozilla/frame-script */ + +// populated in AboutTorConnect.init() +let TorStrings = {}; +let TorConnectState = {}; +let InternetStatus = {}; + +const UIStates = Object.freeze({ + ConnectToTor: "ConnectToTor", + Offline: "Offline", + ConnectionAssist: "ConnectionAssist", + CouldNotLocate: "CouldNotLocate", + LocationConfirm: "LocationConfirm", + FinalError: "FinalError", +}); + +const BreadcrumbStatus = Object.freeze({ + Hidden: "hidden", + Disabled: "disabled", + Default: "default", + Active: "active", + Error: "error", +}); + +class AboutTorConnect { + selectors = Object.freeze({ + textContainer: { + title: "div.title", + titleText: "h1.title-text", + longContentText: "#connectLongContentText", + }, + progress: { + description: "p#connectShortDescText", + meter: "div#progressBar", + }, + breadcrumbs: { + container: "#breadcrumbs", + connectToTor: { + link: "#connect-to-tor", + label: "#connect-to-tor .breadcrumb-label", + }, + connectionAssist: { + separator: "#connection-assist-separator", + link: "#connection-assist", + label: "#connection-assist .breadcrumb-label", + }, + tryBridge: { + separator: "#try-bridge-separator", + link: "#try-bridge", + label: "#try-bridge .breadcrumb-label", + }, + }, + viewLog: { + container: "#viewLogContainer", + link: "span#viewLogLink", + }, + quickstart: { + container: "div#quickstartContainer", + checkbox: "input#quickstartCheckbox", + label: "label#quickstartCheckboxLabel", + }, + buttons: { + restart: "button#restartButton", + configure: "button#configureButton", + cancel: "button#cancelButton", + connect: "button#connectButton", + tryBridge: "button#tryBridgeButton", + locationDropdownLabel: "#locationDropdownLabel", + locationDropdown: "form#locationDropdown", + locationDropdownSelect: "form#locationDropdown select", + }, + }); + + elements = Object.freeze({ + title: document.querySelector(this.selectors.textContainer.title), + titleText: document.querySelector(this.selectors.textContainer.titleText), + longContentText: document.querySelector( + this.selectors.textContainer.longContentText + ), + progressDescription: document.querySelector( + this.selectors.progress.description + ), + progressMeter: document.querySelector(this.selectors.progress.meter), + breadcrumbContainer: document.querySelector( + this.selectors.breadcrumbs.container + ), + connectToTorLink: document.querySelector( + this.selectors.breadcrumbs.connectToTor.link + ), + connectToTorLabel: document.querySelector( + this.selectors.breadcrumbs.connectToTor.label + ), + connectionAssistSeparator: document.querySelector( + this.selectors.breadcrumbs.connectionAssist.separator + ), + connectionAssistLink: document.querySelector( + this.selectors.breadcrumbs.connectionAssist.link + ), + connectionAssistLabel: document.querySelector( + this.selectors.breadcrumbs.connectionAssist.label + ), + tryBridgeSeparator: document.querySelector( + this.selectors.breadcrumbs.tryBridge.separator + ), + tryBridgeLink: document.querySelector( + this.selectors.breadcrumbs.tryBridge.link + ), + tryBridgeLabel: document.querySelector( + this.selectors.breadcrumbs.tryBridge.label + ), + viewLogContainer: document.querySelector(this.selectors.viewLog.container), + viewLogLink: document.querySelector(this.selectors.viewLog.link), + quickstartContainer: document.querySelector( + this.selectors.quickstart.container + ), + quickstartCheckbox: document.querySelector( + this.selectors.quickstart.checkbox + ), + quickstartLabel: document.querySelector(this.selectors.quickstart.label), + restartButton: document.querySelector(this.selectors.buttons.restart), + configureButton: document.querySelector(this.selectors.buttons.configure), + cancelButton: document.querySelector(this.selectors.buttons.cancel), + connectButton: document.querySelector(this.selectors.buttons.connect), + locationDropdownLabel: document.querySelector( + this.selectors.buttons.locationDropdownLabel + ), + locationDropdown: document.querySelector( + this.selectors.buttons.locationDropdown + ), + locationDropdownSelect: document.querySelector( + this.selectors.buttons.locationDropdownSelect + ), + tryBridgeButton: document.querySelector(this.selectors.buttons.tryBridge), + }); + + // 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; + + uiState = { + currentState: UIStates.ConnectToTor, + connectIsTryAgain: false, + allowAutomaticLocation: true, + selectedLocation: "automatic", + bootstrapCause: UIStates.ConnectToTor, + }; + + locations = {}; + + constructor() { + this.uiStates = Object.freeze( + Object.fromEntries([ + [UIStates.ConnectToTor, this.showConnectToTor.bind(this)], + [UIStates.Offline, this.showOffline.bind(this)], + [UIStates.ConnectionAssist, this.showConnectionAssistant.bind(this)], + [UIStates.CouldNotLocate, this.showCouldNotLocate.bind(this)], + [UIStates.LocationConfirm, this.showLocationConfirmation.bind(this)], + [UIStates.FinalError, this.showFinalError.bind(this)], + ]) + ); + } + + beginBootstrap() { + RPMSendAsyncMessage("torconnect:begin-bootstrap"); + } + + beginAutoBootstrap(countryCode) { + if (countryCode === "automatic") { + countryCode = ""; + } + RPMSendAsyncMessage("torconnect:begin-autobootstrap", countryCode); + } + + cancelBootstrap() { + RPMSendAsyncMessage("torconnect:cancel-bootstrap"); + } + + transitionUIState(nextState, connState) { + if (nextState !== this.uiState.currentState) { + this.uiState.currentState = nextState; + this.saveUIState(); + } + this.uiStates[nextState](connState); + } + + saveUIState() { + RPMSendAsyncMessage("torconnect:set-ui-state", this.uiState); + } + + /* + Element helper methods + */ + + show(element, primary) { + element.classList.toggle("primary", primary !== undefined && primary); + element.removeAttribute("hidden"); + } + + hide(element) { + element.setAttribute("hidden", "true"); + } + + hideButtons() { + this.hide(this.elements.quickstartContainer); + this.hide(this.elements.restartButton); + this.hide(this.elements.configureButton); + this.hide(this.elements.cancelButton); + this.hide(this.elements.connectButton); + this.hide(this.elements.locationDropdownLabel); + this.hide(this.elements.locationDropdown); + this.hide(this.elements.tryBridgeButton); + } + + populateLocations() { + const selectCountryRegion = document.createElement("option"); + selectCountryRegion.textContent = TorStrings.torConnect.selectCountryRegion; + selectCountryRegion.value = ""; + + // get all codes and names from TorStrings + const locationNodes = []; + for (const [code, name] of Object.entries(this.locations)) { + let option = document.createElement("option"); + option.value = code; + option.textContent = name; + locationNodes.push(option); + } + // locale sort by name + locationNodes.sort((left, right) => + left.textContent.localeCompare(right.textContent) + ); + this.elements.locationDropdownSelect.append( + selectCountryRegion, + ...locationNodes + ); + } + + populateFrequentLocations(locations) { + this.removeFrequentLocations(); + if (!locations || !locations.length) { + return; + } + + const locationNodes = []; + for (const code of locations) { + const option = document.createElement("option"); + option.value = code; + option.className = "frequent-location"; + // codes (partially) come from rdsys service, so make sure we have a + // string defined for it + let name = this.locations[code]; + if (!name) { + name = code; + } + option.textContent = name; + locationNodes.push(option); + } + // locale sort by name + locationNodes.sort((left, right) => + left.textContent.localeCompare(right.textContent) + ); + + const frequentGroup = document.createElement("optgroup"); + frequentGroup.setAttribute( + "label", + TorStrings.torConnect.frequentLocations + ); + frequentGroup.className = "frequent-location"; + const locationGroup = document.createElement("optgroup"); + locationGroup.setAttribute("label", TorStrings.torConnect.otherLocations); + locationGroup.className = "frequent-location"; + // options[0] is either "Select Country or Region" or "Automatic" + this.elements.locationDropdownSelect.options[0].after( + frequentGroup, + ...locationNodes, + locationGroup + ); + } + + removeFrequentLocations() { + const select = this.elements.locationDropdownSelect; + for (const option of select.querySelectorAll(".frequent-location")) { + option.remove(); + } + } + + validateLocation() { + const selectedIndex = this.elements.locationDropdownSelect.selectedIndex; + const selectedOption = this.elements.locationDropdownSelect.options[ + selectedIndex + ]; + if (!selectedOption.value) { + this.elements.tryBridgeButton.setAttribute("disabled", "disabled"); + } else { + this.elements.tryBridgeButton.removeAttribute("disabled"); + } + } + + setTitle(title, className) { + this.elements.titleText.textContent = title; + this.elements.title.className = "title"; + if (className) { + this.elements.title.classList.add(className); + } + document.title = title; + } + + setLongText(...args) { + this.elements.longContentText.textContent = ""; + this.elements.longContentText.append(...args); + } + + 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); + } + } + + setBreadcrumbsStatus(connectToTor, connectionAssist, tryBridge) { + this.elements.breadcrumbContainer.classList.remove("hidden"); + const elems = [ + [this.elements.connectToTorLink, connectToTor, null], + [ + this.elements.connectionAssistLink, + connectionAssist, + this.elements.connectionAssistSeparator, + ], + [ + this.elements.tryBridgeLink, + tryBridge, + this.elements.tryBridgeSeparator, + ], + ]; + elems.forEach(([elem, status, separator]) => { + elem.classList.remove(BreadcrumbStatus.Hidden); + elem.classList.remove(BreadcrumbStatus.Disabled); + elem.classList.remove(BreadcrumbStatus.Active); + elem.classList.remove(BreadcrumbStatus.Error); + if (status !== "") { + elem.classList.add(status); + } + separator?.classList.toggle("hidden", status === BreadcrumbStatus.Hidden); + }); + } + + hideBreadcrumbs() { + this.elements.breadcrumbContainer.classList.add("hidden"); + } + + /* + These methods update the UI based on the current TorConnect state + */ + + updateUI(state) { + // calls update_$state() + this[`update_${state.State}`](state); + this.elements.quickstartCheckbox.checked = state.QuickStartEnabled; + } + + /* Per-state updates */ + + update_Initial(state) { + this.showConnectToTor(state); + } + + update_Configuring(state) { + if ( + state.StateChanged && + (state.PreviousState === TorConnectState.Bootstrapping || + state.PreviousState === TorConnectState.AutoBootstrapping) + ) { + // The bootstrap has been cancelled + this.transitionUIState(this.uiState.bootstrapCause, state); + } + } + + update_AutoBootstrapping(state) { + this.showBootstrapping(state); + } + + update_Bootstrapping(state) { + this.showBootstrapping(state); + } + + update_Error(state) { + if (!this.uiState.connectIsTryAgain) { + // TorConnect.hasBootstrapEverFailed remains false in case of Internet + // offline + this.uiState.connectIsTryAgain = true; + this.saveUIState(); + } + if (!state.StateChanged) { + return; + } + if (state.InternetStatus === InternetStatus.Offline) { + this.transitionUIState(UIStates.Offline, state); + } else if (state.PreviousState === TorConnectState.Bootstrapping) { + this.transitionUIState(UIStates.ConnectionAssist, state); + } else if (state.PreviousState === TorConnectState.AutoBootstrapping) { + if (this.uiState.bootstrapCause === UIStates.ConnectionAssist) { + if (this.getLocation() === "automatic") { + this.uiState.allowAutomaticLocation = false; + if (!state.DetectedLocation) { + this.transitionUIState(UIStates.CouldNotLocate, state); + return; + } + // Change the location only here, to avoid overriding any user change/ + // insisting with the detected location + this.setLocation(state.DetectedLocation); + } + this.transitionUIState(UIStates.LocationConfirm, state); + } else { + this.transitionUIState(UIStates.FinalError, state); + } + } else { + console.error( + "We received an error starting from an unexpected state", + state + ); + } + } + + update_Bootstrapped(state) { + const showProgressbar = true; + + this.setTitle(TorStrings.torConnect.torConnected, ""); + this.setLongText(TorStrings.settings.torPreferencesDescription); + this.setProgress("", showProgressbar, 100); + this.hideButtons(); + + // 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) + } + + showConnectToTor(state) { + this.setTitle(TorStrings.torConnect.torConnect, ""); + this.setLongText(TorStrings.settings.torPreferencesDescription); + this.setProgress("", false); + this.hide(this.elements.viewLogContainer); + this.hideButtons(); + this.show(this.elements.quickstartContainer); + this.show(this.elements.configureButton); + this.show(this.elements.connectButton, true); + if (state?.StateChanged) { + this.elements.connectButton.focus(); + } + if (this.uiState.connectIsTryAgain) { + this.setBreadcrumbsStatus( + BreadcrumbStatus.Active, + BreadcrumbStatus.Default, + BreadcrumbStatus.Disabled + ); + this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain; + } + this.uiState.bootstrapCause = UIStates.ConnectToTor; + this.saveUIState(); + } + + showBootstrapping(state) { + const showProgressbar = true; + let title = ""; + let description = ""; + const breadcrumbs = [ + BreadcrumbStatus.Disabled, + BreadcrumbStatus.Disabled, + BreadcrumbStatus.Disabled, + ]; + switch (this.uiState.bootstrapCause) { + case UIStates.ConnectToTor: + breadcrumbs[0] = BreadcrumbStatus.Active; + title = this.uiState.connectIsTryAgain + ? TorStrings.torConnect.tryAgain + : TorStrings.torConnect.torConnecting; + description = TorStrings.settings.torPreferencesDescription; + break; + case UIStates.ConnectionAssist: + breadcrumbs[2] = BreadcrumbStatus.Active; + title = TorStrings.torConnect.tryingBridge; + description = TorStrings.torConnect.assistDescription; + break; + case UIStates.CouldNotLocate: + breadcrumbs[2] = BreadcrumbStatus.Active; + title = TorStrings.torConnect.tryingBridgeAgain; + description = TorStrings.torConnect.errorLocationDescription; + break; + case UIStates.LocationConfirm: + breadcrumbs[2] = BreadcrumbStatus.Active; + title = TorStrings.torConnect.tryingBridgeAgain; + description = TorStrings.torConnect.isLocationCorrectDescription; + break; + } + this.setTitle(title, ""); + this.showConfigureConnectionLink(description); + this.setProgress("", showProgressbar, state.BootstrapProgress); + if (state.HasBootsrapEverFailed) { + this.setBreadcrumbsStatus(...breadcrumbs); + } else { + this.hideBreadcrumbs(); + } + this.hideButtons(); + if (state.ShowViewLog) { + this.show(this.elements.viewLogContainer); + } else { + this.hide(this.elements.viewLogContainer); + } + this.show(this.elements.cancelButton, true); + if (state.StateChanged) { + this.elements.cancelButton.focus(); + } + } + + showOffline(error) { + this.setTitle(TorStrings.torConnect.noInternet, "offline"); + this.setLongText(TorStrings.torConnect.noInternetDescription); + this.setProgress(error, false); + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Active, + BreadcrumbStatus.Hidden + ); + this.show(this.elements.viewLogContainer); + this.hideButtons(); + this.show(this.elements.configureButton); + this.show(this.elements.connectButton, true); + this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain; + } + + showConnectionAssistant(state) { + this.setTitle(TorStrings.torConnect.couldNotConnect, "assist"); + this.showConfigureConnectionLink(TorStrings.torConnect.assistDescription); + this.setProgress(state?.ErrorDetails, false); + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Active, + BreadcrumbStatus.Disabled + ); + this.showLocationForm(false, TorStrings.torConnect.tryBridge); + if (state?.StateChanged) { + this.elements.tryBridgeButton.focus(); + } + this.uiState.bootstrapCause = UIStates.ConnectionAssist; + this.saveUIState(); + } + + showCouldNotLocate(state) { + this.uiState.allowAutomaticLocation = false; + this.setTitle(TorStrings.torConnect.errorLocation, "location"); + this.showConfigureConnectionLink( + TorStrings.torConnect.errorLocationDescription + ); + this.setProgress(state.ErrorMessage, false); + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Active, + BreadcrumbStatus.Disabled + ); + this.show(this.elements.viewLogContainer); + this.showLocationForm(true, TorStrings.torConnect.tryBridge); + if (state.StateChanged) { + this.elements.tryBridgeButton.focus(); + } + this.uiState.bootstrapCause = UIStates.CouldNotLocate; + this.saveUIState(); + } + + showLocationConfirmation(state) { + this.setTitle(TorStrings.torConnect.isLocationCorrect, "location"); + this.showConfigureConnectionLink( + TorStrings.torConnect.isLocationCorrectDescription + ); + this.setProgress(state.ErrorMessage, false); + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Default, + BreadcrumbStatus.Active + ); + this.show(this.elements.viewLogContainer); + this.showLocationForm(true, TorStrings.torConnect.tryAgain); + if (state.StateChanged) { + this.elements.tryBridgeButton.focus(); + } + this.uiState.bootstrapCause = UIStates.LocationConfirm; + this.saveUIState(); + } + + showFinalError(state) { + this.setTitle(TorStrings.torConnect.finalError, "final"); + this.setLongText(TorStrings.torConnect.finalErrorDescription); + this.setProgress(state ? state.ErrorDetails : "", false); + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Default, + BreadcrumbStatus.Active + ); + this.hideButtons(); + this.show(this.elements.restartButton); + this.show(this.elements.configureButton, true); + } + + showConfigureConnectionLink(text) { + const pieces = text.split("#1"); + const link = document.createElement("a"); + link.textContent = TorStrings.torConnect.configureConnection; + link.setAttribute("href", "#"); + link.addEventListener("click", e => { + e.preventDefault(); + RPMSendAsyncMessage("torconnect:open-tor-preferences"); + }); + if (pieces.length > 1) { + const first = pieces.shift(); + this.setLongText(first, link, ...pieces); + } else { + this.setLongText(text); + } + } + + showLocationForm(isError, buttonLabel) { + this.hideButtons(); + RPMSendQuery("torconnect:get-country-codes").then(codes => { + if (codes && codes.length) { + this.populateFrequentLocations(codes); + this.setLocation(); + } + }); + let firstOpt = this.elements.locationDropdownSelect.options[0]; + if (this.uiState.allowAutomaticLocation) { + firstOpt.value = "automatic"; + firstOpt.textContent = TorStrings.torConnect.automatic; + } else { + firstOpt.value = ""; + firstOpt.textContent = TorStrings.torConnect.selectCountryRegion; + } + this.setLocation(); + this.validateLocation(); + this.show(this.elements.locationDropdownLabel); + this.show(this.elements.locationDropdown); + this.elements.locationDropdownLabel.classList.toggle("error", isError); + this.show(this.elements.tryBridgeButton, true); + this.elements.tryBridgeButton.classList.toggle("danger-button", isError); + if (buttonLabel !== undefined) { + this.elements.tryBridgeButton.textContent = buttonLabel; + } + } + + getLocation() { + const selectedIndex = this.elements.locationDropdownSelect.selectedIndex; + return this.elements.locationDropdownSelect.options[selectedIndex].value; + } + + setLocation(code) { + if (!code) { + code = this.uiState.selectedLocation; + } else { + this.uiState.selectedLocation = code; + } + if (this.getLocation() === code) { + return; + } + const options = this.elements.locationDropdownSelect.options; + // We need to do this way, because we have repeated values that break + // the .value way to select (which would however require the label, + // rather than the code)... + for (let i = 0; i < options.length; i++) { + if (options[i].value === code) { + this.elements.locationDropdownSelect.selectedIndex = i; + break; + } + } + this.validateLocation(); + } + + initElements(direction) { + document.documentElement.setAttribute("dir", direction); + + this.elements.connectToTorLink.addEventListener("click", event => { + if (this.uiState.currentState === UIStates.ConnectToTor) { + return; + } + this.transitionUIState(UIStates.ConnectToTor, null); + RPMSendAsyncMessage("torconnect:broadcast-user-action", { + uiState: UIStates.ConnectToTor, + }); + }); + this.elements.connectToTorLabel.textContent = + TorStrings.torConnect.torConnect; + this.elements.connectionAssistLink.addEventListener("click", event => { + if ( + this.elements.connectionAssistLink.classList.contains( + BreadcrumbStatus.Active + ) || + this.elements.connectionAssistLink.classList.contains( + BreadcrumbStatus.Disabled + ) + ) { + return; + } + this.transitionUIState(UIStates.ConnectionAssist, null); + RPMSendAsyncMessage("torconnect:broadcast-user-action", { + uiState: UIStates.ConnectionAssist, + }); + }); + this.elements.connectionAssistLabel.textContent = + TorStrings.torConnect.breadcrumbAssist; + this.elements.tryBridgeLabel.textContent = + TorStrings.torConnect.breadcrumbTryBridge; + + this.hide(this.elements.viewLogContainer); + this.elements.viewLogLink.textContent = TorStrings.torConnect.viewLog; + this.elements.viewLogLink.addEventListener("click", event => { + RPMSendAsyncMessage("torconnect:view-tor-logs"); + }); + + 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.restartButton.textContent = + TorStrings.torConnect.restartTorBrowser; + this.elements.restartButton.addEventListener("click", () => { + RPMSendAsyncMessage("torconnect:restart"); + }); + + this.elements.configureButton.textContent = + TorStrings.torConnect.torConfigure; + this.elements.configureButton.addEventListener("click", () => { + RPMSendAsyncMessage("torconnect:open-tor-preferences"); + }); + + this.elements.cancelButton.textContent = TorStrings.torConnect.cancel; + this.elements.cancelButton.addEventListener("click", () => { + this.cancelBootstrap(); + }); + + this.elements.connectButton.textContent = + TorStrings.torConnect.torConnectButton; + this.elements.connectButton.addEventListener("click", () => { + this.beginBootstrap(); + }); + + this.populateLocations(); + this.elements.locationDropdownSelect.addEventListener("change", () => { + this.uiState.selectedLocation = this.getLocation(); + this.saveUIState(); + this.validateLocation(); + RPMSendAsyncMessage("torconnect:broadcast-user-action", { + location: this.uiState.selectedLocation, + }); + }); + + this.elements.locationDropdownLabel.textContent = + TorStrings.torConnect.yourLocation; + + this.elements.tryBridgeButton.textContent = TorStrings.torConnect.tryBridge; + this.elements.tryBridgeButton.addEventListener("click", () => { + const value = this.getLocation(); + if (value === "automatic") { + this.beginAutoBootstrap(); + } else { + this.beginAutoBootstrap(value); + } + }); + } + + initObservers() { + // TorConnectParent feeds us state blobs to we use to update our UI + RPMAddMessageListener("torconnect:state-change", ({ data }) => { + this.updateUI(data); + }); + RPMAddMessageListener("torconnect:user-action", ({ data }) => { + if (data.location) { + this.uiState.selectedLocation = data.location; + this.setLocation(); + } + if (data.uiState !== undefined) { + this.transitionUIState(data.uiState, data.connState); + } + }); + } + + 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); + InternetStatus = Object.freeze(args.InternetStatus); + this.locations = args.CountryNames; + + this.initElements(args.Direction); + this.initObservers(); + this.initKeyboardShortcuts(); + + if (Object.keys(args.State.UIState).length) { + this.uiState = args.State.UIState; + } else { + args.State.UIState = this.uiState; + this.saveUIState(); + } + this.uiStates[this.uiState.currentState](args.State); + // 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..77d2e68895708 --- /dev/null +++ b/browser/components/torconnect/content/aboutTorConnect.xhtml @@ -0,0 +1,68 @@ +<!-- 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="progressBar"> + <div id="progressBackground" /> + <div id="progressSolid" /> + </div> + <div id="connectPageContainer" class="container"> + <div id="breadcrumbs" class="hidden"> + <span id="connect-to-tor" class="breadcrumb-item"> + <span id="connect-to-tor-icon" class="breadcrumb-icon" /> + <span class="breadcrumb-label"/> + </span> + <span id="connection-assist-separator" class="breadcrumb-separator breadcrumb-icon" /> + <span id="connection-assist" class="breadcrumb-item"> + <span id="connection-assist-icon" class="breadcrumb-icon" /> + <span class="breadcrumb-label"/> + </span> + <span id="try-bridge-separator" class="breadcrumb-separator breadcrumb-icon" /> + <span id="try-bridge" class="breadcrumb-item"> + <span id="try-bridge-icon" class="breadcrumb-icon" /> + <span class="breadcrumb-label"/> + </span> + </div> + <div id="text-container"> + <div class="title"> + <h1 class="title-text"/> + </div> + <div id="connectLongContent"> + <p id="connectLongContentText" /> + </div> + <div id="connectShortDesc"> + <p id="connectShortDescText" /> + </div> + + <div id="viewLogContainer"> + <span id="viewLogLink"></span> + </div> + + <div id="quickstartContainer"> + <input id="quickstartCheckbox" type="checkbox" /> + <label id="quickstartCheckboxLabel" for="quickstartCheckbox"/> + </div> + + <div id="connectButtonContainer" class="button-container"> + <button id="restartButton" hidden="true"></button> + <button id="configureButton" hidden="true"></button> + <button id="cancelButton" hidden="true"></button> + <button id="connectButton" class="primary" hidden="true"></button> + <label id="locationDropdownLabel" for="countries"/> + <form id="locationDropdown" hidden="true"> + <select id="countries"> + </select> + </form> + <button id="tryBridgeButton" class="primary" 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/arrow-right.svg b/browser/components/torconnect/content/arrow-right.svg new file mode 100644 index 0000000000000..3f6d8ded52bed --- /dev/null +++ b/browser/components/torconnect/content/arrow-right.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M10.9991 8.352L5.53406 13.818C5.41557 13.9303 5.25792 13.9918 5.09472 13.9895C4.93152 13.9872 4.77567 13.9212 4.66039 13.8057C4.54511 13.6902 4.47951 13.5342 4.47758 13.3709C4.47565 13.2077 4.53754 13.0502 4.65006 12.932L9.58506 7.998L4.65106 3.067C4.53868 2.94864 4.47697 2.79106 4.47909 2.62786C4.48121 2.46466 4.54698 2.30874 4.66239 2.19333C4.7778 2.07792 4.93372 2.01215 5.09692 2.01003C5.26012 2.00792 5.41769 2.06962 5.53606 2.182L11.0001 7.647L10.9991 8.352Z" fill="conte [...] +</svg> diff --git a/browser/components/torconnect/content/bridge.svg b/browser/components/torconnect/content/bridge.svg new file mode 100644 index 0000000000000..5ae3f05dfd082 --- /dev/null +++ b/browser/components/torconnect/content/bridge.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M1 9.48528C1 9.48528 3.82843 9.48528 6.65685 6.65685C9.48528 3.82843 9.48528 1 9.48528 1" stroke="context-fill" stroke-width="1.25" stroke-linecap="round"/> + <path d="M6.65686 15.1421C6.65686 15.1421 6.65686 12.3137 9.48529 9.48529C12.3137 6.65686 15.1421 6.65686 15.1421 6.65686" stroke="context-fill" stroke-width="1.25" stroke-linecap="round"/> +</svg> diff --git a/browser/components/torconnect/content/connection-failure.svg b/browser/components/torconnect/content/connection-failure.svg new file mode 100644 index 0000000000000..8f2005e360556 --- /dev/null +++ b/browser/components/torconnect/content/connection-failure.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg fill="none" height="60" viewBox="0 0 60 60" width="60" xmlns="http://www.w3.org/2000/svg"> + <path fill="context-fill" d="M 30,1.875 C 14.467,1.875 1.875,14.467 1.875,30 c 0,6.725546 2.3647525,12.894963 6.3027344,17.734375 l -4.7636719,4.763672 c -0.7834743,0.783474 -0.7834743,2.044651 0,2.828125 0.7834743,0.783474 2.0446507,0.783474 2.828125,0 C 21.046044,40.52782 34.415343,27.146014 47.546875,14.023438 v -0.002 l 6.779297,-6.7792965 c 0.783474,-0.7834743 0.783474,-2.0446507 0,-2.828125 -0.783474,-0.7834743 -2.044651,-0.7834743 -2.828125,0 L 47.734375,8.1777344 C 42.894963,4. [...] + <path fill="#d70022" d="m59.5328 52.4973-10.261-18.5715c-.7112-1.2833-1.9917-1.9258-3.2722-1.9258-1.2806 0-2.5611.6425-3.2704 1.9258l-10.261 18.5715c-1.3701 2.4755.4312 5.5027 3.2704 5.5027h20.5238c2.8373 0 4.6387-3.0272 3.2704-5.5027zm-12.3666-.533-.4666.4642h-1.4l-.4667-.4642v-1.3929l.4667-.4643h1.4l.4666.4643zm0-4.992c0 .3078-.1229.603-.3417.8207s-.5155.34-.8249.34-.6062-.1223-.825-.34-.3417-.5129-.3417-.8207v-6.383c0-.3079.1229-.6031.3417-.8208s.5156-.34.825-.34.6061.1223.8249.34.3 [...] +</svg> diff --git a/browser/components/torconnect/content/connection-location.svg b/browser/components/torconnect/content/connection-location.svg new file mode 100644 index 0000000000000..1e5c41ccf99a0 --- /dev/null +++ b/browser/components/torconnect/content/connection-location.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg fill="none" height="60" viewBox="0 0 60 60" width="60" xmlns="http://www.w3.org/2000/svg"> + <path fill="context-fill" d="M 30,1.875 C 14.467,1.875 1.875,14.467 1.875,30 c 0,6.725546 2.3647429,12.894963 6.3027344,17.734375 l -4.7636719,4.763672 c -0.7834743,0.783474 -0.7834743,2.044651 0,2.828125 0.7834743,0.783474 2.0446507,0.783474 2.828125,0 C 21.049647,40.524244 34.416498,27.144859 47.546875,14.023438 v -0.002 l 6.779297,-6.7792965 c 0.783474,-0.7834743 0.783474,-2.0446507 0,-2.828125 -0.783474,-0.7834743 -2.044651,-0.7834743 -2.828125,0 L 47.734375,8.1777344 C 42.894963,4 [...] + <path fill="#ffa436" d="m45 30c-3.713 0-7.274 1.475-9.8995 4.1005s-4.1005 6.1865-4.1005 9.8995 1.475 7.274 4.1005 9.8995 6.1865 4.1005 9.8995 4.1005 7.274-1.475 9.8995-4.1005 4.1005-6.1865 4.1005-9.8995-1.475-7.274-4.1005-9.8995-6.1865-4.1005-9.8995-4.1005zm4.5677 3.2667c1.9167.8229 3.5778 2.1443 4.8108 3.8267 1.233 1.6825 1.9928 3.6644 2.2004 5.7399h-4.1608c-.2298-3.4759-1.4862-6.8054-3.6101-9.5666zm-3.8248 0c2.5257 2.5792 4.06 5.967 4.3326 9.5666h-10.151c.2726-3.5996 1.8069-6.9874 4. [...] +</svg> diff --git a/browser/components/torconnect/content/onion-slash-fillable.svg b/browser/components/torconnect/content/onion-slash-fillable.svg new file mode 100644 index 0000000000000..18f1c5a5520bd --- /dev/null +++ b/browser/components/torconnect/content/onion-slash-fillable.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="context-fill" /> + <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-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..61b15b809ef6c --- /dev/null +++ b/browser/components/torconnect/content/torBootstrapUrlbar.js @@ -0,0 +1,95 @@ +// 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"); + +/* globals browser, gURLBar, Services */ + +var TorBootstrapUrlbar = { + selectors: Object.freeze({ + torConnect: { + box: "hbox#torconnect-box", + label: "label#torconnect-label", + }, + }), + + elements: null, + + updateTorConnectBox(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(aSubject, aTopic, aData) { + if (aTopic === TorConnectTopics.StateChange) { + const obj = aSubject?.wrappedJSObject; + this.updateTorConnectBox(obj?.state); + } + }, + + init() { + 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() { + 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..96d9ff63ec713 --- /dev/null +++ b/browser/components/torconnect/content/torconnect-urlbar.css @@ -0,0 +1,37 @@ +/* + ensure our torconnect button is always visible (same rule as for the bookmark button) +*/ +label#torconnect-label { + margin: 0; + opacity: 0.6; + padding: 0 0.5em; +} + +hbox.urlbar-page-action#torconnect-box { + display: -moz-inline-box!important; +} + +/* 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..4a65177f7eae3 --- /dev/null +++ b/browser/components/torconnect/jar.mn @@ -0,0 +1,13 @@ +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/arrow-right.svg (content/arrow-right.svg) + content/browser/torconnect/bridge.svg (content/bridge.svg) + content/browser/torconnect/connection-failure.svg (content/connection-failure.svg) + content/browser/torconnect/connection-location.svg (content/connection-location.svg) + content/browser/torconnect/onion.svg (content/onion.svg) + content/browser/torconnect/onion-slash.svg (content/onion-slash.svg) + content/browser/torconnect/onion-slash-fillable.svg (content/onion-slash-fillable.svg) + skin/classic/browser/torconnect-urlbar.css (content/torconnect-urlbar.css) 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 20c7f9fa2c210..ce5bf88df5093 100644 --- a/browser/components/urlbar/UrlbarInput.jsm +++ b/browser/components/urlbar/UrlbarInput.jsm @@ -10,6 +10,38 @@ 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", @@ -2547,6 +2579,11 @@ 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/moz.build b/browser/modules/moz.build index 018ca1a9430bf..98caa91ee6f44 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -146,6 +146,8 @@ EXTRA_JS_MODULES += [ "SitePermissions.jsm", "TabsList.jsm", "TabUnloader.jsm", + "TorConnect.jsm", + "TorProcessService.jsm", "TorProtocolService.jsm", "TorSettings.jsm", "TorStrings.jsm", diff --git a/browser/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css index 1a1bb759b8b4e..c8dac0afb49a5 100644 --- a/browser/themes/shared/browser-shared.css +++ b/browser/themes/shared/browser-shared.css @@ -21,6 +21,7 @@ @import url("chrome://browser/skin/ctrlTab.css"); @import url("chrome://browser/skin/customizableui/customizeMode.css"); @import url("chrome://browser/skin/UITour.css"); +@import url("chrome://browser/skin/torconnect-urlbar.css");
@namespace html url("http://www.w3.org/1999/xhtml");
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index b84754dc32ed1..5ceaee677da65 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -17791,8 +17791,55 @@ ColorScheme Document::DefaultColorScheme() const { }
ColorScheme Document::PreferredColorScheme(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)) { + nsContentUtils::ShouldResistFingerprinting(this) && + !documentUsesPreferredColorScheme(this)) { return ColorScheme::Light; }
diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index d98276d975b73..03b90e6098d85 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -6107,6 +6107,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 1094126816f6f..1d34525baf5cf 100644 --- a/toolkit/actors/AboutHttpsOnlyErrorParent.jsm +++ b/toolkit/actors/AboutHttpsOnlyErrorParent.jsm @@ -12,6 +12,8 @@ const { PrivateBrowsingUtils } = ChromeUtils.import( ); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TorConnect } = ChromeUtils.import("resource:///modules/TorConnect.jsm"); + class AboutHttpsOnlyErrorParent extends JSWindowActorParent { get browser() { return this.browsingContext.top.embedderElement; @@ -22,7 +24,10 @@ class AboutHttpsOnlyErrorParent extends JSWindowActorParent { case "goBack": this.goBackFromErrorPage(this.browser); break; + case "ShouldShowTorConnect": + return TorConnect.shouldShowTorConnect; } + return undefined; }
goBackFromErrorPage(aBrowser) { diff --git a/toolkit/components/httpsonlyerror/content/errorpage.js b/toolkit/components/httpsonlyerror/content/errorpage.js index 50ece6e33ba16..3eec718613047 100644 --- a/toolkit/components/httpsonlyerror/content/errorpage.js +++ b/toolkit/components/httpsonlyerror/content/errorpage.js @@ -122,8 +122,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 28a16b732172e..4afa7f118a72f 100644 --- a/toolkit/components/processsingleton/MainProcessSingleton.jsm +++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm @@ -22,6 +22,7 @@ MainProcessSingleton.prototype = {
// FIXME: Is this import really necessary? ChromeUtils.import("resource:///modules/TorSettings.jsm"); + ChromeUtils.import("resource:///modules/TorConnect.jsm");
Services.ppmm.loadProcessScript( "chrome://global/content/process-content.js", diff --git a/toolkit/modules/RemotePageAccessManager.jsm b/toolkit/modules/RemotePageAccessManager.jsm index 2ae4898736804..6b1da814765f3 100644 --- a/toolkit/modules/RemotePageAccessManager.jsm +++ b/toolkit/modules/RemotePageAccessManager.jsm @@ -66,6 +66,7 @@ let RemotePageAccessManager = { RPMAddMessageListener: ["WWWReachable"], RPMTryPingSecureWWWLink: ["*"], RPMOpenSecureWWWLink: ["*"], + RPMSendQuery: ["ShouldShowTorConnect"], }, "about:certificate": { RPMSendQuery: ["getCertificates"], @@ -94,6 +95,7 @@ let RemotePageAccessManager = { RPMAddToHistogram: ["*"], RPMGetInnerMostURI: ["*"], RPMGetHttpResponseHeader: ["*"], + RPMSendQuery: ["ShouldShowTorConnect"], }, "about:plugins": { RPMSendQuery: ["RequestPlugins"], @@ -214,6 +216,30 @@ let RemotePageAccessManager = { RPMAddMessageListener: ["*"], RPMRemoveMessageListener: ["*"], }, + "about:tbupdate": { + RPMSendQuery: ["FetchUpdateData"], + }, + "about:torconnect": { + RPMAddMessageListener: [ + "torconnect:state-change", + "torconnect:user-action", + ], + RPMSendAsyncMessage: [ + "torconnect:open-tor-preferences", + "torconnect:begin-bootstrap", + "torconnect:begin-autobootstrap", + "torconnect:cancel-bootstrap", + "torconnect:set-quickstart", + "torconnect:view-tor-logs", + "torconnect:restart", + "torconnect:set-ui-state", + "torconnect:broadcast-user-action", + ], + RPMSendQuery: [ + "torconnect:get-init-args", + "torconnect:get-country-codes", + ], + }, },
/** 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 de0091ae8d4d6..962ca7a810a36 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 @@ -74,6 +74,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/"