[tbb-commits] [tor-browser] 62/74: Bug 27476: Implement about:torconnect captive portal within Tor Browser

gitolite role git at cupani.torproject.org
Thu Oct 27 21:22:07 UTC 2022


This is an automated email from the git hooks/post-receive script.

richard pushed a commit to branch tor-browser-102.4.0esr-12.0-2
in repository tor-browser.

commit 71babf7714a7842af14f331ac528309c622c05cf
Author: Richard Pospesel <richard at 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         | 326 ++++++++
 .../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/themes/shared/browser-shared.css           |   1 +
 dom/base/Document.cpp                              |  50 +-
 dom/base/nsGlobalWindowOuter.cpp                   |   2 +
 toolkit/actors/AboutHttpsOnlyErrorParent.jsm       |   5 +
 .../components/httpsonlyerror/content/errorpage.js |  19 +-
 toolkit/modules/RemotePageAccessManager.jsm        |  26 +
 .../lib/environments/browser-window.js             |   4 +
 35 files changed, 1882 insertions(+), 29 deletions(-)

diff --git a/browser/actors/NetErrorParent.jsm b/browser/actors/NetErrorParent.jsm
index 315957ff112e..250b95b95059 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 597bff687349..0a5ec63c4a63 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 cb29fa7bd280..32d971c93d6d 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 43a1777a8b41..1714b1b8a4be 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 6123e1336aed..81479615dc72 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -323,6 +323,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 977f40e5e331..90e795888a15 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 b1a0fc5ffe44..0092d25948e7 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 7c915f2b45b0..0a495a223e3e 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 56226324bc32..6095653409cd 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 0fa76a0e7038..ab1dac40dfab 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 000000000000..bd6dd549f156
--- /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 000000000000..cf3c1233dd62
--- /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 000000000000..688a11c4415d
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.css
@@ -0,0 +1,326 @@
+
+/* Copyright (c) 2021, The Tor Project, Inc. */
+
+ at import url("chrome://browser/skin/error-pages.css");
+ at 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;
+}
+
+ at 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 */
+ at 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);
+  }
+}
+
+ at 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%);
+}
+
+ at 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%;
+}
+
+ at keyframes progressAnimation {
+  0% {
+    background-position: 200%;
+  }
+  50% {
+    background-position: 100%;
+  }
+  100% {
+    background-position: 0%;
+  }
+}
+
+ at 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: stroke, 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");
+  stroke: var(--in-content-danger-button-background);
+}
+
+.title.location {
+  background-image: url("chrome://browser/content/torconnect/connection-location.svg");
+  stroke: var(--torbrowser-warning);
+}
diff --git a/browser/components/torconnect/content/aboutTorConnect.js b/browser/components/torconnect/content/aboutTorConnect.js
new file mode 100644
index 000000000000..6b33442c7d02
--- /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 000000000000..77d2e6889570
--- /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 000000000000..3f6d8ded52be
--- /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 000000000000..5ae3f05dfd08
--- /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 000000000000..76af8c8b94e4
--- /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="context-stroke" 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.82 [...]
+</svg>
diff --git a/browser/components/torconnect/content/connection-location.svg b/browser/components/torconnect/content/connection-location.svg
new file mode 100644
index 000000000000..1da280993b07
--- /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="context-stroke" 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. [...]
+</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 000000000000..18f1c5a5520b
--- /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 000000000000..93eb24b03905
--- /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 000000000000..7655a800d9ee
--- /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 000000000000..61b15b809ef6
--- /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 000000000000..96d9ff63ec71
--- /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 000000000000..60e985a72691
--- /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 000000000000..4a65177f7eae
--- /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 000000000000..eb29c31a4243
--- /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 20c7f9fa2c21..ce5bf88df509 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/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css
index 1a1bb759b8b4..c8dac0afb49a 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");
+ at 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 ad3abe7bffec..b1be884fe828 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -17795,8 +17795,56 @@ 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 8f7f880de51a..1d4ffff18a52 100644
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -6110,6 +6110,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 1094126816f6..1d34525baf5c 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 50ece6e33ba1..3eec71861304 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/modules/RemotePageAccessManager.jsm b/toolkit/modules/RemotePageAccessManager.jsm
index 2ae489873680..6b1da814765f 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 de0091ae8d4d..962ca7a810a3 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/"

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the tbb-commits mailing list