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

gitolite role git at cupani.torproject.org
Tue May 3 17:40:11 UTC 2022


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

richard pushed a commit to branch tor-browser-91.9.0esr-11.5-1
in repository tor-browser.

commit 417ba1436bfb6f62409fd0ef11c37a7e34806f8e
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                  |   8 +
 browser/base/content/browser-siteIdentity.js       |   2 +-
 browser/base/content/browser.js                    |  66 +-
 browser/base/content/browser.xhtml                 |   2 +
 browser/base/content/certerror/aboutNetError.js    |  12 +-
 browser/base/content/navigator-toolbox.inc.xhtml   |   1 +
 browser/base/content/utilityOverlay.js             |  14 +
 browser/components/BrowserGlue.jsm                 |  14 +
 browser/components/about/AboutRedirector.cpp       |   4 +
 browser/components/about/components.conf           |   1 +
 browser/components/moz.build                       |   1 +
 browser/components/sessionstore/SessionStore.jsm   |   4 +
 browser/components/torconnect/TorConnectChild.jsm  |   9 +
 browser/components/torconnect/TorConnectParent.jsm | 176 ++++++
 .../torconnect/content/aboutTorConnect.css         | 247 ++++++++
 .../torconnect/content/aboutTorConnect.js          | 680 +++++++++++++++++++++
 .../torconnect/content/aboutTorConnect.xhtml       |  66 ++
 .../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 +
 browser/components/torconnect/content/globe.svg    |   4 +
 .../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       |  93 +++
 .../torconnect/content/torconnect-urlbar.css       |  57 ++
 .../torconnect/content/torconnect-urlbar.inc.xhtml |  10 +
 browser/components/torconnect/jar.mn               |  13 +
 browser/components/torconnect/moz.build            |   6 +
 browser/components/urlbar/UrlbarInput.jsm          |  32 +
 browser/modules/TorProcessService.jsm              |  12 +
 browser/modules/moz.build                          |   2 +
 browser/themes/shared/urlbar-searchbar.inc.css     |   3 +
 dom/base/Document.cpp                              |  51 +-
 dom/base/nsGlobalWindowOuter.cpp                   |   2 +
 toolkit/actors/AboutHttpsOnlyErrorParent.jsm       |   5 +
 .../components/httpsonlyerror/content/errorpage.js |  19 +-
 .../processsingleton/MainProcessSingleton.jsm      |   5 +
 toolkit/modules/RemotePageAccessManager.jsm        |  21 +
 toolkit/mozapps/update/UpdateService.jsm           |  68 ++-
 .../lib/environments/browser-window.js             |   4 +
 42 files changed, 1714 insertions(+), 33 deletions(-)

diff --git a/browser/actors/NetErrorParent.jsm b/browser/actors/NetErrorParent.jsm
index 3472c68f664a4..13afbbbfd4a8b 100644
--- a/browser/actors/NetErrorParent.jsm
+++ b/browser/actors/NetErrorParent.jsm
@@ -21,6 +21,10 @@ const { TelemetryController } = ChromeUtils.import(
   "resource://gre/modules/TelemetryController.jsm"
 );
 
+const { TorConnect } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+
 const PREF_SSL_IMPACT_ROOTS = [
   "security.tls.version.",
   "security.ssl3.",
@@ -350,6 +354,10 @@ class NetErrorParent extends JSWindowActorParent {
             break;
           }
         }
+        break;
+      case "ShouldShowTorConnect":
+        return TorConnect.shouldShowTorConnect;
     }
+    return undefined;
   }
 }
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
index 2846a1cb2fcfd..6901ce71814a3 100644
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -57,7 +57,7 @@ var gIdentityHandler = {
    * RegExp used to decide if an about url should be shown as being part of
    * the browser UI.
    */
-  _secureInternalPages: /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback)(?:[?#]|$)/i,
+  _secureInternalPages: /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|tor|torconnect)(?:[?#]|$)/i,
 
   /**
    * Whether the established HTTPS connection is considered "broken".
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index 566976b6d7aa0..ef8a191987676 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -79,6 +79,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   TabModalPrompt: "chrome://global/content/tabprompts.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
+  TorConnect: "resource:///modules/TorConnect.jsm",
   Translation: "resource:///modules/translation/TranslationParent.jsm",
   UITour: "resource:///modules/UITour.jsm",
   UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
@@ -633,6 +634,7 @@ var gPageIcons = {
 
 var gInitialPages = [
   "about:tor",
+  "about:torconnect",
   "about:blank",
   "about:newtab",
   "about:home",
@@ -1837,6 +1839,8 @@ var gBrowserInit = {
     }
 
     this._loadHandled = true;
+
+    TorBootstrapUrlbar.init();
   },
 
   _cancelDelayedStartup() {
@@ -2385,32 +2389,48 @@ var gBrowserInit = {
 
       let defaultArgs = BrowserHandler.defaultArgs;
 
-      // If the given URI is different from the homepage, we want to load it.
-      if (uri != defaultArgs) {
-        AboutNewTab.noteNonDefaultStartup();
+      // figure out which URI to actually load (or a Promise to get the uri)
+      uri = ((uri) => {
+        // If the given URI is different from the homepage, we want to load it.
+        if (uri != defaultArgs) {
+          AboutNewTab.noteNonDefaultStartup();
+
+          if (uri instanceof Ci.nsIArray) {
+            // Transform the nsIArray of nsISupportsString's into a JS Array of
+            // JS strings.
+            return Array.from(
+              uri.enumerate(Ci.nsISupportsString),
+              supportStr => supportStr.data
+            );
+          } else if (uri instanceof Ci.nsISupportsString) {
+            return uri.data;
+          }
+          return uri;
+        }
 
-        if (uri instanceof Ci.nsIArray) {
-          // Transform the nsIArray of nsISupportsString's into a JS Array of
-          // JS strings.
-          return Array.from(
-            uri.enumerate(Ci.nsISupportsString),
-            supportStr => supportStr.data
-          );
-        } else if (uri instanceof Ci.nsISupportsString) {
-          return uri.data;
+        // The URI appears to be the the homepage. We want to load it only if
+        // session restore isn't about to override the homepage.
+        let willOverride = SessionStartup.willOverrideHomepage;
+        if (typeof willOverride == "boolean") {
+          return willOverride ? null : uri;
         }
-        return uri;
-      }
+        return willOverride.then(willOverrideHomepage =>
+          willOverrideHomepage ? null : uri
+        );
+      })(uri);
+
+      // if using TorConnect, convert these uris to redirects
+      if (TorConnect.shouldShowTorConnect) {
+        return Promise.resolve(uri).then((uri) => {
+          if (uri == null) {
+            uri  = [];
+          }
 
-      // The URI appears to be the the homepage. We want to load it only if
-      // session restore isn't about to override the homepage.
-      let willOverride = SessionStartup.willOverrideHomepage;
-      if (typeof willOverride == "boolean") {
-        return willOverride ? null : uri;
+          uri = TorConnect.getURIsToLoad(uri);
+          return uri;
+        });
       }
-      return willOverride.then(willOverrideHomepage =>
-        willOverrideHomepage ? null : uri
-      );
+      return uri;
     })());
   },
 
@@ -2473,6 +2493,8 @@ var gBrowserInit = {
 
     DownloadsButton.uninit();
 
+    TorBootstrapUrlbar.uninit();
+
     gAccessibilityServiceIndicator.uninit();
 
     if (gToolbarKeyNavEnabled) {
diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml
index 8efb544918b8e..f163073657286 100644
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -10,6 +10,7 @@
      override rules using selectors with the same specificity. This applies to
      both "content" and "skin" packages, which bug 1385444 will unify later. -->
 <?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://branding/content/tor-styles.css" type="text/css"?>
 
 <!-- While these stylesheets are defined in Toolkit, they are only used in the
      main browser window, so we can load them here. Bug 1474241 is on file to
@@ -110,6 +111,7 @@
   Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this);
   Services.scriptloader.loadSubScript("chrome://torbutton/content/tor-circuit-display.js", this);
   Services.scriptloader.loadSubScript("chrome://torbutton/content/torbutton.js", this);
+  Services.scriptloader.loadSubScript("chrome://browser/content/torconnect/torBootstrapUrlbar.js", this);
 
   window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
   window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
diff --git a/browser/base/content/certerror/aboutNetError.js b/browser/base/content/certerror/aboutNetError.js
index 31c4838a053da..edf97c2a5daf3 100644
--- a/browser/base/content/certerror/aboutNetError.js
+++ b/browser/base/content/certerror/aboutNetError.js
@@ -239,7 +239,7 @@ function setErrorPageStrings(err) {
   document.l10n.setAttributes(titleElement, title);
 }
 
-function initPage() {
+async function initPage() {
   // We show an offline support page in case of a system-wide error,
   // when a user cannot connect to the internet and access the SUMO website.
   // For example, clock error, which causes certerrors across the web or
@@ -258,6 +258,16 @@ function initPage() {
   }
 
   var err = getErrorCode();
+
+  // proxyConnectFailure because no-tor running daemon would return this error
+  if (
+     (err === "proxyConnectFailure") &&
+     (await RPMSendQuery("ShouldShowTorConnect"))
+  ) {
+     // pass orginal destination as redirect param
+     const encodedRedirect = encodeURIComponent(document.location.href);
+     document.location.replace(`about:torconnect?redirect=${encodedRedirect}`);
+  }
   // List of error pages with an illustration.
   let illustratedErrors = [
     "malformedURI",
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index 02636a6b46b5c..e7f63116ff39e 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -330,6 +330,7 @@
                    data-l10n-id="urlbar-go-button"/>
             <hbox id="page-action-buttons" context="pageActionContextMenu">
               <toolbartabstop/>
+#include ../../components/torconnect/content/torconnect-urlbar.inc.xhtml
               <hbox id="contextual-feature-recommendation" role="button" hidden="true">
                 <hbox id="cfr-label-container">
                   <label id="cfr-label"/>
diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js
index 3b14beaa5b1e3..a95717544b80f 100644
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -21,6 +21,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   ExtensionSettingsStore: "resource://gre/modules/ExtensionSettingsStore.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   ShellService: "resource:///modules/ShellService.jsm",
+  TorConnect: "resource:///modules/TorConnect.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(this, "ReferrerInfo", () =>
@@ -258,6 +259,19 @@ function openUILinkIn(
   aPostData,
   aReferrerInfo
 ) {
+  // make sure users are not faced with the scary red 'tor isn't working' screen
+  // if they navigate to about:tor before bootstrapped
+  //
+  // fixes tor-browser#40752
+  // new tabs also redirect to about:tor if browser.newtabpage.enabled is true
+  // otherwise they go to about:blank
+  if (TorConnect.shouldShowTorConnect) {
+    if (url === "about:tor" ||
+        (url === "about:newtab" && Services.prefs.getBoolPref("browser.newtabpage.enabled", false))) {
+      url = TorConnect.getRedirectURL(url);
+    }
+  }
+
   var params;
 
   if (arguments.length == 3 && typeof arguments[2] == "object") {
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index 9dfad0358ed75..dc956bc796166 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -703,6 +703,20 @@ let JSWINDOWACTORS = {
     allFrames: true,
   },
 
+  TorConnect: {
+    parent: {
+      moduleURI: "resource:///modules/TorConnectParent.jsm",
+    },
+    child: {
+      moduleURI: "resource:///modules/TorConnectChild.jsm",
+      events: {
+        DOMWindowCreated: {},
+      },
+    },
+
+    matches: ["about:torconnect","about:torconnect?*"],
+  },
+
   Translation: {
     parent: {
       moduleURI: "resource:///modules/translation/TranslationParent.jsm",
diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp
index 6d283fe67b206..21f673f601d26 100644
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -122,6 +122,10 @@ static const RedirEntry kRedirMap[] = {
          nsIAboutModule::HIDE_FROM_ABOUTABOUT},
     {"restartrequired", "chrome://browser/content/aboutRestartRequired.xhtml",
      nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::HIDE_FROM_ABOUTABOUT},
+    {"torconnect", "chrome://browser/content/torconnect/aboutTorConnect.xhtml",
+     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+         nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT |
+         nsIAboutModule::HIDE_FROM_ABOUTABOUT},
 };
 
 static nsAutoCString GetAboutModuleName(nsIURI* aURI) {
diff --git a/browser/components/about/components.conf b/browser/components/about/components.conf
index 8ce22e9cff519..733abef1a80f6 100644
--- a/browser/components/about/components.conf
+++ b/browser/components/about/components.conf
@@ -26,6 +26,7 @@ pages = [
     'robots',
     'sessionrestore',
     'tabcrashed',
+    'torconnect',
     'welcome',
     'welcomeback',
 ]
diff --git a/browser/components/moz.build b/browser/components/moz.build
index 66de87290bd83..d15ff30515936 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -53,6 +53,7 @@ DIRS += [
     "syncedtabs",
     "uitour",
     "urlbar",
+    "torconnect",
     "torpreferences",
     "translation",
 ]
diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm
index 2150c424d8b83..ddeb923784329 100644
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -186,6 +186,10 @@ ChromeUtils.defineModuleGetter(
   "resource://gre/modules/sessionstore/SessionHistory.jsm"
 );
 
+const { TorProtocolService } = ChromeUtils.import(
+    "resource:///modules/TorProtocolService.jsm"
+);
+
 XPCOMUtils.defineLazyServiceGetters(this, {
   gScreenManager: ["@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"],
 });
diff --git a/browser/components/torconnect/TorConnectChild.jsm b/browser/components/torconnect/TorConnectChild.jsm
new file mode 100644
index 0000000000000..bd6dd549f156d
--- /dev/null
+++ b/browser/components/torconnect/TorConnectChild.jsm
@@ -0,0 +1,9 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+var EXPORTED_SYMBOLS = ["TorConnectChild"];
+
+const { RemotePageChild } = ChromeUtils.import(
+  "resource://gre/actors/RemotePageChild.jsm"
+);
+
+class TorConnectChild extends RemotePageChild {}
diff --git a/browser/components/torconnect/TorConnectParent.jsm b/browser/components/torconnect/TorConnectParent.jsm
new file mode 100644
index 0000000000000..dd3d1b2410f95
--- /dev/null
+++ b/browser/components/torconnect/TorConnectParent.jsm
@@ -0,0 +1,176 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+var EXPORTED_SYMBOLS = ["TorConnectParent"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+const { TorConnect, TorConnectTopics, TorConnectState, TorCensorshipLevel } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+const { TorSettings, TorSettingsTopics, TorSettingsData } = ChromeUtils.import(
+  "resource:///modules/TorSettings.jsm"
+);
+
+/*
+This object is basically a marshalling interface between the TorConnect module
+and a particular about:torconnect page
+*/
+
+class TorConnectParent extends JSWindowActorParent {
+  constructor(...args) {
+    super(...args);
+
+    const self = this;
+
+    this.state = {
+      State: TorConnect.state,
+      DetectedCensorshiplevel: TorConnect.detectedCensorshiplevel,
+      StateChanged: false,
+      ErrorMessage: TorConnect.errorMessage,
+      ErrorDetails: TorConnect.errorDetails,
+      BootstrapProgress: TorConnect.bootstrapProgress,
+      BootstrapStatus: TorConnect.bootstrapStatus,
+      ShowViewLog: TorConnect.logHasWarningOrError,
+      QuickStartEnabled: TorSettings.quickstart.enabled,
+      CountryCodes: TorConnect.countryCodes,
+    };
+
+    // JSWindowActiveParent derived objects cannot observe directly, so create a member
+    // object to do our observing for us
+    //
+    // This object converts the various lifecycle events from the TorConnect module, and
+    // maintains a state object which we pass down to our about:torconnect page, which uses
+    // the state object to update its UI
+    this.torConnectObserver = {
+      observe(aSubject, aTopic, aData) {
+        let obj = aSubject?.wrappedJSObject;
+
+        // update our state struct based on received torconnect topics and forward on
+        // to aboutTorConnect.js
+        self.state.StateChanged = false;
+        switch (aTopic) {
+          case TorConnectTopics.StateChange: {
+            self.state.State = obj.state;
+            self.state.StateChanged = true;
+
+            // clear any previous error information if we are bootstrapping
+            if (self.state.State === TorConnectState.Bootstrapping) {
+              self.state.ErrorMessage = null;
+              self.state.ErrorDetails = null;
+            }
+            break;
+          }
+          case TorConnectTopics.BootstrapProgress: {
+            self.state.BootstrapProgress = obj.progress;
+            self.state.BootstrapStatus = obj.status;
+            self.state.ShowViewLog = obj.hasWarnings;
+            break;
+          }
+          case TorConnectTopics.BootstrapComplete: {
+            // noop
+            break;
+          }
+          case TorConnectTopics.BootstrapError: {
+            self.state.ErrorMessage = obj.message;
+            self.state.ErrorDetails = obj.details;
+            self.state.DetectedCensorshiplevel = obj.censorshipLevel;
+
+            // With severe censorshp, we offer user list of countries to try
+            if (self.state.DetectedCensorshiplevel == TorCensorshipLevel.Severe) {
+              self.state.CountryCodes = TorConnect.countryCodes;
+            }
+
+            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
+    );
+  }
+
+  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
+    );
+  }
+
+  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:view-tor-logs":
+        TorConnect.viewTorLogs();
+        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:restart":
+        Services.startup.quit(
+          Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit
+        );
+        break;
+      case "torconnect:get-init-args":
+        // called on AboutTorConnect.init(), pass down all state data it needs to init
+
+        // pretend this is a state transition on init
+        // so we always get fresh UI
+        this.state.StateChanged = true;
+        return {
+          TorStrings,
+          TorConnectState,
+          TorCensorshipLevel,
+          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..0a3dc9fbd75fd
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.css
@@ -0,0 +1,247 @@
+
+/* 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;
+}
+
+/* override firefox's default blue focus coloring */
+:focus {
+  outline:  none!important;
+  box-shadow: 0 0 0 3px var(--purple-30) !important;
+  border:  1px var(--purple-80) solid !important;
+}
+
+ at media (prefers-color-scheme: dark)
+{
+  :focus {
+    box-shadow: 0 0 0 3px var(--purple-50)!important;
+  }
+}
+
+#breadcrumbs {
+  display: flex;
+  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;
+}
+
+.breadcrumb-item {
+  cursor: pointer;
+  color: var(--in-content-text-color);
+}
+
+.breadcrumb-item:hover {
+  color: var(--blue-60);
+}
+
+.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;
+  list-style-position: inside;
+  fill: currentColor;
+  -moz-context-properties: fill;
+}
+
+.breadcrumb-label {
+  margin-top: -1px;
+}
+
+#breadcrumbs .active {
+  color: var(--blue-60);
+}
+
+#breadcrumbs .disabled, #breadcrumbs .disabled:hover {
+  color: var(--green-90-a40);
+  cursor: default;
+}
+
+#breadcrumbs .error {
+  color: var(--red-60);
+}
+
+#connection-assist {
+  margin-left: 0;
+}
+
+#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.primary {
+  background-color: var(--purple-60)!important;
+  color: white!important;
+  fill: white!important;
+}
+
+button.primary:hover {
+  background-color: var(--purple-70)!important;
+  color: white!important;
+  fill: white!important;
+}
+
+button.primary:active {
+  background-color: var(--purple-80)!important;
+  color: white!important;
+  fill: white!important;
+}
+
+div#locationDropdownLabel {
+  margin-block: auto;
+  margin-inline: 4px;
+}
+
+/* these two follow 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) {
+  form#locationDropdown,
+  div#locationDropdownLabel {
+    margin: 0.66em 0 0;
+  }
+
+  form#locationDropdown {
+    width: 100%;
+  }
+}
+
+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;
+}
+
+#progressBackground {
+  position:fixed;
+  padding:0;
+  margin:0;
+  top:0;
+  left:0;
+  width: 0%;
+  height: 7px;
+  background-image: linear-gradient(90deg, rgb(20, 218, 221) 0%, rgb(128, 109, 236) 100%);
+  border-radius: 0;
+}
+
+#connectPageContainer {
+  margin-top: 10vh;
+  width: 50%;
+}
+
+#quickstartCheckbox, #quickstartCheckboxLabel {
+  vertical-align: middle;
+}
+
+/* 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.error {
+  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..8710eef95a49e
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.js
@@ -0,0 +1,680 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+/* eslint-env mozilla/frame-script */
+
+// populated in AboutTorConnect.init()
+let TorStrings = {};
+let TorConnectState = {};
+let TorCensorshipLevel = {};
+
+const BreadcrumbStatus = Object.freeze({
+  Disabled: -1,
+  Default: 0,
+  Active: 1,
+  Error: 2,
+});
+
+class AboutTorConnect {
+  selectors = Object.freeze({
+    textContainer: {
+      title: "div.title",
+      titleText: "h1.title-text",
+      longContentText: "#connectLongContentText",
+    },
+    progress: {
+      description: "p#connectShortDescText",
+      meter: "div#progressBackground",
+    },
+    breadcrumbs: {
+      container: "#breadcrumbs",
+      connectionAssist: {
+        link: "#connection-assist",
+        label: "#connection-assist .breadcrumb-label",
+      },
+      locationSettings: {
+        link: "#location-settings",
+        label: "#location-settings .breadcrumb-label",
+      },
+      tryBridge: {
+        link: "#try-bridge",
+        label: "#try-bridge .breadcrumb-label",
+      },
+    },
+    viewLog: {
+      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: "div#locationDropdownLabel",
+      locationDropdown: "form#locationDropdown",
+      locationDropdownSelect: "form#locationDropdown select",
+      tryAgain: "button#tryAgainButton",
+    },
+  });
+
+  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
+    ),
+    connectionAssistLink: document.querySelector(
+      this.selectors.breadcrumbs.connectionAssist.link
+    ),
+    connectionAssistLabel: document.querySelector(
+      this.selectors.breadcrumbs.connectionAssist.label
+    ),
+    locationSettingsLink: document.querySelector(
+      this.selectors.breadcrumbs.locationSettings.link
+    ),
+    locationSettingsLabel: document.querySelector(
+      this.selectors.breadcrumbs.locationSettings.label
+    ),
+    tryBridgeLink: document.querySelector(
+      this.selectors.breadcrumbs.tryBridge.link
+    ),
+    tryBridgeLabel: document.querySelector(
+      this.selectors.breadcrumbs.tryBridge.label
+    ),
+    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),
+    tryBridgeButton: document.querySelector(this.selectors.buttons.tryBridge),
+    locationDropdownLabel: document.querySelector(
+      this.selectors.buttons.locationDropdownLabel
+    ),
+    locationDropdown: document.querySelector(
+      this.selectors.buttons.locationDropdown
+    ),
+    locationDropdownSelect: document.querySelector(
+      this.selectors.buttons.locationDropdownSelect
+    ),
+    tryAgainButton: document.querySelector(this.selectors.buttons.tryAgain),
+  });
+
+  // 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;
+
+  locations = {};
+
+  beginBootstrap() {
+    this.hide(this.elements.connectButton);
+    this.hide(this.elements.quickstartContainer);
+    this.show(this.elements.cancelButton);
+    this.elements.cancelButton.focus();
+    RPMSendAsyncMessage("torconnect:begin-bootstrap");
+  }
+
+  beginAutoBootstrap(countryCode) {
+    this.hide(this.elements.tryBridgeButton);
+    this.show(this.elements.cancelButton);
+    this.elements.cancelButton.focus();
+    RPMSendAsyncMessage("torconnect:begin-autobootstrap", countryCode);
+  }
+
+  cancelBootstrap() {
+    RPMSendAsyncMessage("torconnect:cancel-bootstrap");
+  }
+
+  /*
+  Element helper methods
+  */
+
+  show(element, primary) {
+    if (primary) {
+      element.classList.add("primary");
+    } else {
+      element.classList.remove("primary");
+    }
+    element.removeAttribute("hidden");
+  }
+
+  hide(element) {
+    element.setAttribute("hidden", "true");
+  }
+
+  hideButtons() {
+    this.hide(this.elements.restartButton);
+    this.hide(this.elements.configureButton);
+    this.hide(this.elements.cancelButton);
+    this.hide(this.elements.connectButton);
+    this.hide(this.elements.tryBridgeButton);
+    this.hide(this.elements.locationDropdownLabel);
+    this.hide(this.elements.locationDropdown);
+    this.hide(this.elements.tryAgainButton);
+  }
+
+  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
+    );
+  }
+
+  populateSpecialLocations(specialLocations) {
+    this.removeSpecialLocations();
+    if (!specialLocations || !specialLocations.length) {
+      return;
+    }
+
+    const locationNodes = [];
+    for (const code of specialLocations) {
+      const option = document.createElement("option");
+      option.value = code;
+
+      // 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 disabledDividerNode = document.createElement("option");
+    disabledDividerNode.setAttribute("disabled", true);
+    disabledDividerNode.className = "divider";
+    this.elements.locationDropdownSelect.options[0].after(
+      ...locationNodes,
+      disabledDividerNode
+    );
+  }
+
+  removeSpecialLocations() {
+    const select = this.elements.locationDropdownSelect;
+    if (select.querySelector(".divider") === null) {
+      return;
+    }
+
+    while (select.options.length > 1) {
+      // Skip the "select country/region" option
+      const opt = select.options[1];
+      opt.remove();
+      if (opt.className === "divider") {
+        break;
+      }
+    }
+  }
+
+  validateLocation() {
+    const selectedIndex = this.elements.locationDropdownSelect.selectedIndex;
+    const selectedOption = this.elements.locationDropdownSelect.options[
+      selectedIndex
+    ];
+    if (!selectedOption.value) {
+      this.elements.tryAgainButton.setAttribute("disabled", "disabled");
+    } else {
+      this.elements.tryAgainButton.removeAttribute("disabled");
+    }
+  }
+
+  setTitle(title, className) {
+    this.elements.titleText.textContent = title;
+    if (className !== "error") {
+      this.elements.title.classList.remove("error");
+    }
+    if (className !== "location") {
+      this.elements.title.classList.remove("location");
+    }
+    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(connectionAssist, locationSettings, tryBridge) {
+    this.elements.breadcrumbContainer.classList.remove("hidden");
+    let elems = [
+      [this.elements.connectionAssistLink, connectionAssist],
+      [this.elements.locationSettingsLink, locationSettings],
+      [this.elements.tryBridgeLink, tryBridge],
+    ];
+    elems.forEach(([elem, status]) => {
+      elem.classList.remove("disabled");
+      elem.classList.remove("active");
+      elem.classList.remove("error");
+      switch (status) {
+        case BreadcrumbStatus.Disabled:
+          elem.classList.add("disabled");
+          break;
+        case BreadcrumbStatus.Active:
+          elem.classList.add("active");
+          break;
+        case BreadcrumbStatus.Error:
+          elem.classList.add("error");
+          break;
+      }
+    });
+  }
+
+  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) {
+    const hasError = false;
+    const showProgressbar = false;
+
+    this.setTitle(TorStrings.torConnect.torConnect, hasError ? "error" : "");
+    this.setProgress(
+      TorStrings.settings.torPreferencesDescription,
+      showProgressbar
+    );
+    this.hide(this.elements.quickstartContainer);
+    this.hide(this.elements.viewLogLink);
+    this.hideButtons();
+  }
+
+  update_Configuring(state) {
+    const hasError = state.ErrorMessage != null;
+    const showProgressbar = false;
+
+    this.hide(this.elements.quickstartContainer);
+    this.hide(this.elements.viewLogLink);
+    this.hideButtons();
+
+    if (hasError) {
+      switch (state.DetectedCensorshiplevel) {
+        case TorCensorshipLevel.None:
+          // we shouldn't be able to get here
+          break;
+        case TorCensorshipLevel.Moderate:
+          // bootstrap failed once, offer auto bootstrap
+          this.showConnectionAssistant(state.ErrorDetails);
+          if (state.StateChanged) {
+            this.elements.tryBridgeButton.focus();
+          }
+          break;
+        case TorCensorshipLevel.Severe:
+          // autobootstrap failed, verify correct location
+          this.showLocationSettings(state.CountryCodes, state.ErrorMessage);
+          if (state.StateChanged) {
+            this.elements.tryAgainButton.focus();
+          }
+          break;
+        case TorCensorshipLevel.Extreme:
+          // finally offer to restart tor-browser or go to configure options
+          this.showFinalError(state);
+          break;
+      }
+    } else {
+      this.setTitle(TorStrings.torConnect.torConnect, "");
+      this.setLongText(TorStrings.settings.torPreferencesDescription);
+      this.setProgress("", showProgressbar);
+      this.show(this.elements.quickstartContainer);
+      this.show(this.elements.configureButton);
+      this.show(this.elements.connectButton, true);
+      if (state.StateChanged) {
+        this.elements.connectButton.focus();
+      }
+      this.elements.connectButton.textContent =
+        TorStrings.torConnect.torConnectButton;
+    }
+  }
+
+  update_AutoBootstrapping(state) {
+    const showProgressbar = true;
+
+    if (state.DetectedCensorshiplevel >= TorCensorshipLevel.Severe) {
+      this.setTitle(TorStrings.torConnect.tryingBridgeAgain, "");
+    } else {
+      this.setTitle(TorStrings.torConnect.tryingBridge, "");
+    }
+    this.showConfigureConnectionLink(TorStrings.torConnect.assistDescription);
+    this.setProgress(
+      state.BootstrapStatus,
+      showProgressbar,
+      state.BootstrapProgress
+    );
+    this.setBreadcrumbsStatus(
+      BreadcrumbStatus.Disabled,
+      BreadcrumbStatus.Disabled,
+      BreadcrumbStatus.Active
+    );
+    if (state.ShowViewLog) {
+      this.show(this.elements.viewLogLink);
+    } else {
+      this.hide(this.elements.viewLogLink);
+    }
+    this.hideButtons();
+    this.show(this.elements.cancelButton, true);
+    if (state.StateChanged) {
+      this.elements.cancelButton.focus();
+    }
+  }
+
+  update_Bootstrapping(state) {
+    const showProgressbar = true;
+
+    this.setTitle(TorStrings.torConnect.torConnecting, "");
+    this.setLongText(TorStrings.settings.torPreferencesDescription);
+    this.setProgress("", showProgressbar, state.BootstrapProgress);
+    this.hideBreadcrumbs();
+    if (state.ShowViewLog) {
+      this.show(this.elements.viewLogLink);
+    } else {
+      this.hide(this.elements.viewLogLink);
+    }
+    this.hideButtons();
+    this.show(this.elements.cancelButton, true);
+    if (state.StateChanged) {
+      this.elements.cancelButton.focus();
+    }
+  }
+
+  update_Error(state) {
+    const showProgressbar = false;
+
+    this.setTitle(state.ErrorMessage, "error");
+    this.setLongText("");
+    this.setProgress(state.ErrorDetails, showProgressbar);
+    this.hideButtons();
+    this.show(this.elements.viewLogLink);
+  }
+
+  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)
+  }
+
+  showConnectionAssistant(error) {
+    const hasError = !!error;
+    this.setTitle(
+      TorStrings.torConnect.couldNotConnect,
+      hasError ? "error" : ""
+    );
+    this.showConfigureConnectionLink(TorStrings.torConnect.assistDescription);
+    this.setProgress(error, false);
+    this.setBreadcrumbsStatus(
+      BreadcrumbStatus.Active,
+      BreadcrumbStatus.Default,
+      BreadcrumbStatus.Disabled
+    );
+    this.hideButtons();
+    this.show(this.elements.configureButton);
+    this.show(this.elements.connectButton);
+    this.show(this.elements.tryBridgeButton, 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");
+    });
+    this.setLongText(pieces[0], link, pieces[1]);
+  }
+
+  showLocationSettings(locations, error) {
+    const hasError = !!error;
+    if (hasError) {
+      this.setTitle(TorStrings.torConnect.errorLocation, "location");
+      this.setLongText(TorStrings.torConnect.errorLocationDescription);
+      this.setBreadcrumbsStatus(
+        BreadcrumbStatus.Disabled,
+        BreadcrumbStatus.Error,
+        BreadcrumbStatus.Disabled
+      );
+      this.elements.tryAgainButton.textContent = TorStrings.torConnect.tryAgain;
+    } else {
+      this.setTitle(TorStrings.torConnect.addLocation, "location");
+      this.showConfigureConnectionLink(
+        TorStrings.torConnect.addLocationDescription
+      );
+      this.setBreadcrumbsStatus(
+        BreadcrumbStatus.Default,
+        BreadcrumbStatus.Active,
+        BreadcrumbStatus.Disabled
+      );
+      this.elements.tryAgainButton.textContent =
+        TorStrings.torConnect.tryBridge;
+    }
+    this.setProgress(error, false);
+    this.hideButtons();
+    if (!locations || !locations.length) {
+      RPMSendQuery("torconnect:get-country-codes").then(codes => {
+        if (codes && codes.length) {
+          this.populateSpecialLocations(codes);
+        }
+      });
+    } else {
+      this.populateSpecialLocations(locations);
+    }
+    this.validateLocation();
+    this.show(this.elements.locationDropdownLabel);
+    this.show(this.elements.locationDropdown);
+    this.show(this.elements.tryAgainButton, true);
+  }
+
+  showFinalError(state) {
+    this.setTitle(TorStrings.torConnect.finalError, "error");
+    this.setLongText(TorStrings.torConnect.finalErrorDescription);
+    this.setProgress(state ? state.ErrorDetails : "", false);
+    this.hideButtons();
+    this.show(this.elements.restartButton);
+    this.show(this.elements.configureButton);
+    this.show(this.elements.connectButton, true);
+  }
+
+  initElements(direction) {
+    document.documentElement.setAttribute("dir", direction);
+
+    this.elements.connectionAssistLink.addEventListener("click", event => {
+      if (!this.elements.connectionAssistLink.classList.contains("disabled")) {
+        this.showConnectionAssistant();
+      }
+    });
+    this.elements.connectionAssistLabel.textContent =
+      TorStrings.torConnect.breadcrumbAssist;
+    this.elements.locationSettingsLink.addEventListener("click", event => {
+      if (!this.elements.connectionAssistLink.classList.contains("disabled")) {
+        this.showLocationSettings();
+      }
+    });
+    this.elements.locationSettingsLabel.textContent =
+      TorStrings.torConnect.breadcrumbLocation;
+    this.elements.tryBridgeLabel.textContent =
+      TorStrings.torConnect.breadcrumbTryBridge;
+
+    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.validateLocation();
+    });
+
+    this.elements.tryBridgeButton.textContent = TorStrings.torConnect.tryBridge;
+    this.elements.tryBridgeButton.addEventListener("click", () => {
+      this.beginAutoBootstrap();
+    });
+
+    this.elements.locationDropdownLabel.textContent =
+      TorStrings.torConnect.yourLocation;
+
+    this.elements.tryAgainButton.textContent = TorStrings.torConnect.tryAgain;
+    this.elements.tryAgainButton.setAttribute("disabled", "disabled");
+    this.elements.tryAgainButton.addEventListener("click", () => {
+      let selectedIndex = this.elements.locationDropdownSelect.selectedIndex;
+      let selectedOption = this.elements.locationDropdownSelect.options[
+        selectedIndex
+      ];
+
+      this.beginAutoBootstrap(selectedOption.value);
+    });
+  }
+
+  initObservers() {
+    // TorConnectParent feeds us state blobs to we use to update our UI
+    RPMAddMessageListener("torconnect:state-change", ({ data }) => {
+      this.updateUI(data);
+    });
+  }
+
+  initKeyboardShortcuts() {
+    document.onkeydown = evt => {
+      // unfortunately it looks like we still haven't standardized keycodes to
+      // integers, so we must resort to a string compare here :(
+      // see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for relevant documentation
+      if (evt.code === "Escape") {
+        this.cancelBootstrap();
+      }
+    };
+  }
+
+  async init() {
+    // see if a user has a final destination after bootstrapping
+    let params = new URLSearchParams(new URL(document.location.href).search);
+    if (params.has("redirect")) {
+      const encodedRedirect = params.get("redirect");
+      this.redirect = decodeURIComponent(encodedRedirect);
+    } else {
+      // if the user gets here manually or via the button in the urlbar
+      // then we will redirect to about:tor
+      this.redirect = "about:tor";
+    }
+
+    let args = await RPMSendQuery("torconnect:get-init-args");
+
+    // various constants
+    TorStrings = Object.freeze(args.TorStrings);
+    TorConnectState = Object.freeze(args.TorConnectState);
+    TorCensorshipLevel = Object.freeze(args.TorCensorshipLevel);
+    this.locations = args.CountryNames;
+
+    this.initElements(args.Direction);
+    this.initObservers();
+    this.initKeyboardShortcuts();
+
+    // populate UI based on current state
+    this.updateUI(args.State);
+  }
+}
+
+const aboutTorConnect = new AboutTorConnect();
+aboutTorConnect.init();
diff --git a/browser/components/torconnect/content/aboutTorConnect.xhtml b/browser/components/torconnect/content/aboutTorConnect.xhtml
new file mode 100644
index 0000000000000..a98af43e2d53f
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.xhtml
@@ -0,0 +1,66 @@
+<!-- Copyright (c) 2021, The Tor Project, Inc. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+    <link rel="stylesheet" href="chrome://browser/skin/onionPattern.css" type="text/css" media="all" />
+    <link rel="stylesheet" href="chrome://browser/content/torconnect/aboutTorConnect.css" type="text/css" media="all" />
+  </head>
+  <body>
+    <div id="progressBackground"></div>
+    <div id="connectPageContainer" class="container">
+      <div id="breadcrumbs" class="hidden">
+        <span id="connection-assist" class="breadcrumb-item">
+          <span id="connection-assist-icon" class="breadcrumb-icon" />
+          <span class="breadcrumb-label"/>
+        </span>
+        <span class="breadcrumb-separator breadcrumb-icon" />
+        <span id="location-settings" class="breadcrumb-item">
+          <span id="location-settings-icon" class="breadcrumb-icon" />
+          <span class="breadcrumb-label"/>
+        </span>
+        <span 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" hidden="true"></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>
+          <button id="tryBridgeButton" class="primary" hidden="true"></button>
+          <div id="locationDropdownLabel"/>
+          <form id="locationDropdown" hidden="true">
+            <select id="countries">
+            </select>
+          </form>
+          <button id="tryAgainButton" 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/globe.svg b/browser/components/torconnect/content/globe.svg
new file mode 100644
index 0000000000000..f4d1f19b43ce8
--- /dev/null
+++ b/browser/components/torconnect/content/globe.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="M8 0.5C6.01088 0.5 4.10322 1.29018 2.6967 2.6967C1.29018 4.10322 0.5 6.01088 0.5 8C0.5 9.98912 1.29018 11.8968 2.6967 13.3033C4.10322 14.7098 6.01088 15.5 8 15.5C9.98912 15.5 11.8968 14.7098 13.3033 13.3033C14.7098 11.8968 15.5 9.98912 15.5 8C15.5 6.01088 14.7098 4.10322 13.3033 2.6967C11.8968 1.29018 9.98912 0.5 8 0.5ZM10.447 2.25C11.4738 2.69088 12.3637 3.39877 13.0242 4.30006C13.6848 5.20135 14.0918 6.26313 14.203 7.375H11.974C11.8509 5.51288 11.1778 3.72922 10.04 2.25H10 [...]
+</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..e6a88490f33d4
--- /dev/null
+++ b/browser/components/torconnect/content/torBootstrapUrlbar.js
@@ -0,0 +1,93 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+"use strict";
+
+const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+const { TorStrings } = ChromeUtils.import(
+  "resource:///modules/TorStrings.jsm"
+);
+
+var TorBootstrapUrlbar = {
+  selectors: Object.freeze({
+    torConnect: {
+      box: "hbox#torconnect-box",
+      label: "label#torconnect-label",
+    },
+  }),
+
+  elements: null,
+
+  updateTorConnectBox: function(state) {
+    switch(state)
+    {
+      case TorConnectState.Initial:
+      case TorConnectState.Configuring:
+      case TorConnectState.AutoConfiguring:
+      case TorConnectState.Error:
+      case TorConnectState.FatalError: {
+        this.elements.torConnectBox.removeAttribute("hidden");
+        this.elements.torConnectLabel.textContent =
+          TorStrings.torConnect.torNotConnectedConcise;
+        this.elements.inputContainer.setAttribute("torconnect", "offline");
+        break;
+      }
+      case TorConnectState.Bootstrapping: {
+        this.elements.torConnectBox.removeAttribute("hidden");
+        this.elements.torConnectLabel.textContent =
+          TorStrings.torConnect.torConnectingConcise;
+        this.elements.inputContainer.setAttribute("torconnect", "connecting");
+        break;
+      }
+      case TorConnectState.Bootstrapped: {
+        this.elements.torConnectBox.removeAttribute("hidden");
+        this.elements.torConnectLabel.textContent =
+          TorStrings.torConnect.torConnectedConcise;
+        this.elements.inputContainer.setAttribute("torconnect", "connected");
+        // hide torconnect box after 5 seconds
+        setTimeout(() => {
+          this.elements.torConnectBox.setAttribute("hidden", "true");
+        }, 5000);
+        break;
+      }
+      case TorConnectState.Disabled: {
+        this.elements.torConnectBox.setAttribute("hidden", "true");
+        break;
+      }
+      default:
+        break;
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic === TorConnectTopics.StateChange) {
+      const obj = aSubject?.wrappedJSObject;
+      this.updateTorConnectBox(obj?.state);
+    }
+  },
+
+  init: function() {
+    if (TorConnect.shouldShowTorConnect) {
+      // browser isn't populated until init
+      this.elements = Object.freeze({
+        torConnectBox: browser.ownerGlobal.document.querySelector(this.selectors.torConnect.box),
+        torConnectLabel: browser.ownerGlobal.document.querySelector(this.selectors.torConnect.label),
+        inputContainer: gURLBar._inputContainer,
+      })
+      this.elements.torConnectBox.addEventListener("click", () => {
+        TorConnect.openTorConnect();
+      });
+      Services.obs.addObserver(this, TorConnectTopics.StateChange);
+      this.observing = true;
+      this.updateTorConnectBox(TorConnect.state);
+    }
+  },
+
+  uninit: function() {
+    if (this.observing) {
+      Services.obs.removeObserver(this, TorConnectTopics.StateChange);
+    }
+  },
+};
+
diff --git a/browser/components/torconnect/content/torconnect-urlbar.css b/browser/components/torconnect/content/torconnect-urlbar.css
new file mode 100644
index 0000000000000..5aabcffedbd02
--- /dev/null
+++ b/browser/components/torconnect/content/torconnect-urlbar.css
@@ -0,0 +1,57 @@
+/*
+    ensure our torconnect button is always visible (same rule as for the bookmark button)
+*/
+hbox.urlbar-page-action#torconnect-box {
+    display: -moz-inline-box!important;
+    height: 28px;
+}
+
+label#torconnect-label {
+    line-height: 28px;
+    margin: 0;
+    opacity: 0.6;
+    padding: 0 0.5em;
+}
+
+/* set appropriate sizes for the non-standard ui densities */
+:root[uidensity=compact] hbox.urlbar-page-action#torconnect-box {
+    height: 24px;
+}
+:root[uidensity=compact] label#torconnect-label {
+    line-height: 24px;
+}
+
+
+:root[uidensity=touch] hbox.urlbar-page-action#torconnect-box {
+    height: 30px;
+}
+:root[uidensity=touch] label#torconnect-label {
+    line-height: 30px;
+}
+
+
+/* hide when hidden attribute is set */
+hbox.urlbar-page-action#torconnect-box[hidden="true"],
+/* hide when user is typing in URL bar */
+#urlbar[usertyping] > #urlbar-input-container > #page-action-buttons > #torconnect-box {
+    display: none!important;
+}
+
+/* hide urlbar's placeholder text when not connectd to tor */
+hbox#urlbar-input-container[torconnect="offline"] input#urlbar-input::placeholder,
+hbox#urlbar-input-container[torconnect="connecting"] input#urlbar-input::placeholder {
+    opacity: 0;
+}
+
+/* hide search suggestions when not connected to tor */
+hbox#urlbar-input-container[torconnect="offline"] + vbox.urlbarView,
+hbox#urlbar-input-container[torconnect="connecting"] + vbox.urlbarView {
+    display: none!important;
+}
+
+/* hide search icon when we are not connected to tor */
+hbox#urlbar-input-container[torconnect="offline"] > #identity-box[pageproxystate="invalid"] > #identity-icon,
+hbox#urlbar-input-container[torconnect="connecting"] > #identity-box[pageproxystate="invalid"] > #identity-icon
+{
+    display: none!important;
+}
diff --git a/browser/components/torconnect/content/torconnect-urlbar.inc.xhtml b/browser/components/torconnect/content/torconnect-urlbar.inc.xhtml
new file mode 100644
index 0000000000000..60e985a726910
--- /dev/null
+++ b/browser/components/torconnect/content/torconnect-urlbar.inc.xhtml
@@ -0,0 +1,10 @@
+# Copyright (c) 2021, The Tor Project, Inc.
+
+<hbox id="torconnect-box"
+      class="urlbar-icon-wrapper urlbar-page-action"
+      role="status"
+      hidden="true">
+    <hbox id="torconnect-container">
+        <label id="torconnect-label"/>
+    </hbox>
+</hbox>
\ No newline at end of file
diff --git a/browser/components/torconnect/jar.mn b/browser/components/torconnect/jar.mn
new file mode 100644
index 0000000000000..8ca0b0651523e
--- /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/globe.svg                           (content/globe.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)
diff --git a/browser/components/torconnect/moz.build b/browser/components/torconnect/moz.build
new file mode 100644
index 0000000000000..eb29c31a42439
--- /dev/null
+++ b/browser/components/torconnect/moz.build
@@ -0,0 +1,6 @@
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+    'TorConnectChild.jsm',
+    'TorConnectParent.jsm',
+]
diff --git a/browser/components/urlbar/UrlbarInput.jsm b/browser/components/urlbar/UrlbarInput.jsm
index b2f76bdee2f4a..db83e09109bf2 100644
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -10,6 +10,34 @@ const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
+const { TorConnect } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+
+// in certain scenarios we want user input uris to open in a new tab if they do so from the
+// about:torconnect tab 
+function maybeUpdateOpenLocationForTorConnect(openUILinkWhere, currentURI, destinationURI) {
+  try {
+    // only open in new tab if:
+    if (// user is navigating away from about:torconnect
+        currentURI === "about:torconnect" &&
+        // we are trying to open in same tab
+        openUILinkWhere === "current" &&
+        // only if user still has not bootstrapped
+        TorConnect.shouldShowTorConnect &&
+        // and user is not just navigating to about:torconnect
+        destinationURI !== "about:torconnect") {
+      return "tab";
+    }
+  } catch (e) {
+    // swallow exception and fall through returning original so we don't accidentally break
+    // anything if an exception is thrown
+    console.log(e?.message ? e.message : e);
+  }
+
+  return openUILinkWhere;
+};
+
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm",
@@ -2418,6 +2446,10 @@ class UrlbarInput {
       this.selectionStart = this.selectionEnd = 0;
     }
 
+    openUILinkWhere = maybeUpdateOpenLocationForTorConnect(
+                        openUILinkWhere,
+                        this.window.gBrowser.currentURI.asciiSpec,
+                        url);
     if (openUILinkWhere != "current") {
       this.handleRevert();
     }
diff --git a/browser/modules/TorProcessService.jsm b/browser/modules/TorProcessService.jsm
new file mode 100644
index 0000000000000..201e331b28066
--- /dev/null
+++ b/browser/modules/TorProcessService.jsm
@@ -0,0 +1,12 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorProcessService"];
+
+var TorProcessService = {
+  get isBootstrapDone() {
+    const svc = Cc["@torproject.org/torlauncher-process-service;1"].getService(
+      Ci.nsISupports
+    ).wrappedJSObject;
+    return svc.mIsBootstrapDone;
+  },
+};
diff --git a/browser/modules/moz.build b/browser/modules/moz.build
index bc543283d887b..a06914ccf8d9e 100644
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -154,6 +154,8 @@ EXTRA_JS_MODULES += [
     "TabsList.jsm",
     "TabUnloader.jsm",
     "ThemeVariableMap.jsm",
+    'TorConnect.jsm',
+    'TorProcessService.jsm',
     "TorProtocolService.jsm",
     "TorSettings.jsm",
     "TorStrings.jsm",
diff --git a/browser/themes/shared/urlbar-searchbar.inc.css b/browser/themes/shared/urlbar-searchbar.inc.css
index 82675dae2041b..f91278ce5ed3b 100644
--- a/browser/themes/shared/urlbar-searchbar.inc.css
+++ b/browser/themes/shared/urlbar-searchbar.inc.css
@@ -745,3 +745,6 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left::after {
 .searchbar-textbox::placeholder {
   opacity: 0.69;
 }
+
+%include ../../components/torconnect/content/torconnect-urlbar.css
+
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
index d4489a35009cd..5df0e24bec398 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -17124,9 +17124,56 @@ void Document::RemoveToplevelLoadingDocument(Document* aDoc) {
 
 StylePrefersColorScheme Document::PrefersColorScheme(
     IgnoreRFP aIgnoreRFP) const {
+
+  // tor-browser#27476
+  // should this document ignore resist finger-printing settings with regards to
+  // setting the color scheme
+  // currently only enabled for about:torconnect but we could expand to other non-
+  // SystemPrincipal pages if we wish
+  const auto documentUsesPreferredColorScheme = [](auto const* constDocument) -> bool {
+    if (auto* document = const_cast<Document*>(constDocument); document != nullptr) {
+      auto uri = document->GetDocBaseURI();
+
+      // try and extract out our prepath and filepath portions of the uri to C-strings
+      nsAutoCString prePathStr, filePathStr;
+      if(NS_FAILED(uri->GetPrePath(prePathStr)) ||
+         NS_FAILED(uri->GetFilePath(filePathStr))) {
+        return false;
+      }
+
+      // stick them in string view for easy comparisons
+      std::string_view prePath(prePathStr.get(), prePathStr.Length()),
+                       filePath(filePathStr.get(), filePathStr.Length());
+
+      // these about URIs will have the user's preferred color scheme exposed to them
+      // we can place other URIs here in the future if we wish
+      // see nsIURI.idl for URI part definitions
+      constexpr struct {
+        std::string_view prePath;
+        std::string_view filePath;
+      } allowedURIs[] = {
+        { "about:", "torconnect" },
+      };
+
+      // check each uri in the allow list against this document's uri
+      // verify the prepath and the file path match
+      for(auto const& uri : allowedURIs) {
+        if (prePath == uri.prePath &&
+            filePath == uri.filePath) {
+          // positive match means we can apply dark-mode to the page
+          return true;
+        }
+      }
+    }
+
+    // do not allow if no match or other error
+    return false;
+  };
+
   if (aIgnoreRFP == IgnoreRFP::No &&
-      nsContentUtils::ShouldResistFingerprinting(this)) {
-    return StylePrefersColorScheme::Light;
+      nsContentUtils::ShouldResistFingerprinting(this) &&
+      !documentUsesPreferredColorScheme(this)) {
+      return StylePrefersColorScheme::Light;
   }
 
   if (auto* bc = GetBrowsingContext()) {
diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp
index 41c93c51cf3b1..aab4a37e78a8f 100644
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -6212,6 +6212,8 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) {
     NS_ENSURE_SUCCESS_VOID(rv);
 
     if (!StringBeginsWith(url, u"about:neterror"_ns) &&
+        // we want about:torconnect pages to be able to close themselves after bootstrap
+        !StringBeginsWith(url, u"about:torconnect"_ns) &&
         !mBrowsingContext->HadOriginalOpener() && !aTrustedCaller &&
         !IsOnlyTopLevelDocumentInSHistory()) {
       bool allowClose =
diff --git a/toolkit/actors/AboutHttpsOnlyErrorParent.jsm b/toolkit/actors/AboutHttpsOnlyErrorParent.jsm
index b949cb065a583..407e014ff6574 100644
--- a/toolkit/actors/AboutHttpsOnlyErrorParent.jsm
+++ b/toolkit/actors/AboutHttpsOnlyErrorParent.jsm
@@ -15,6 +15,8 @@ const { SessionStore } = ChromeUtils.import(
   "resource:///modules/sessionstore/SessionStore.jsm"
 );
 
+const { TorConnect } = ChromeUtils.import("resource:///modules/TorConnect.jsm");
+
 class AboutHttpsOnlyErrorParent extends JSWindowActorParent {
   get browser() {
     return this.browsingContext.top.embedderElement;
@@ -28,7 +30,10 @@ class AboutHttpsOnlyErrorParent extends JSWindowActorParent {
       case "openInsecure":
         this.openWebsiteInsecure(this.browser, aMessage.data.inFrame);
         break;
+      case "ShouldShowTorConnect":
+        return TorConnect.shouldShowTorConnect;
     }
+    return undefined;
   }
 
   goBackFromErrorPage(aWindow) {
diff --git a/toolkit/components/httpsonlyerror/content/errorpage.js b/toolkit/components/httpsonlyerror/content/errorpage.js
index 1dc8c827789b4..cdb269346eeb9 100644
--- a/toolkit/components/httpsonlyerror/content/errorpage.js
+++ b/toolkit/components/httpsonlyerror/content/errorpage.js
@@ -124,8 +124,17 @@ function addAutofocus(selector, position = "afterbegin") {
 
 /* Initialize Page */
 
-initPage();
-// Dispatch this event so tests can detect that we finished loading the error page.
-// We're using the same event name as neterror because BrowserTestUtils.jsm relies on that.
-let event = new CustomEvent("AboutNetErrorLoad", { bubbles: true });
-document.dispatchEvent(event);
+RPMSendQuery("ShouldShowTorConnect").then(shouldShow => {
+  if (shouldShow) {
+    // pass orginal destination as redirect param
+    const encodedRedirect = encodeURIComponent(document.location.href);
+    document.location.replace(`about:torconnect?redirect=${encodedRedirect}`);
+    return;
+  }
+
+  initPage();
+  // Dispatch this event so tests can detect that we finished loading the error page.
+  // We're using the same event name as neterror because BrowserTestUtils.jsm relies on that.
+  let event = new CustomEvent("AboutNetErrorLoad", { bubbles: true });
+  document.dispatchEvent(event);
+});
diff --git a/toolkit/components/processsingleton/MainProcessSingleton.jsm b/toolkit/components/processsingleton/MainProcessSingleton.jsm
index 7bde782e54ce8..ba8cd0f3f97d3 100644
--- a/toolkit/components/processsingleton/MainProcessSingleton.jsm
+++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm
@@ -29,6 +29,11 @@ MainProcessSingleton.prototype = {
           null
         );
 
+        ChromeUtils.import(
+          "resource:///modules/TorConnect.jsm",
+          null
+        );
+
         Services.ppmm.loadProcessScript(
           "chrome://global/content/process-content.js",
           true
diff --git a/toolkit/modules/RemotePageAccessManager.jsm b/toolkit/modules/RemotePageAccessManager.jsm
index 50fb4ea8d4179..225429d95b667 100644
--- a/toolkit/modules/RemotePageAccessManager.jsm
+++ b/toolkit/modules/RemotePageAccessManager.jsm
@@ -67,6 +67,7 @@ let RemotePageAccessManager = {
       RPMAddMessageListener: ["WWWReachable"],
       RPMTryPingSecureWWWLink: ["*"],
       RPMOpenSecureWWWLink: ["*"],
+      RPMSendQuery: ["ShouldShowTorConnect"],
     },
     "about:certificate": {
       RPMSendQuery: ["getCertificates"],
@@ -102,6 +103,7 @@ let RemotePageAccessManager = {
       RPMAddToHistogram: ["*"],
       RPMGetInnerMostURI: ["*"],
       RPMGetHttpResponseHeader: ["*"],
+      RPMSendQuery: ["ShouldShowTorConnect"],
     },
     "about:plugins": {
       RPMSendQuery: ["RequestPlugins"],
@@ -213,6 +215,25 @@ let RemotePageAccessManager = {
       RPMAddMessageListener: ["*"],
       RPMRemoveMessageListener: ["*"],
     },
+    "about:tbupdate": {
+      RPMSendQuery: ["FetchUpdateData"],
+    },
+    "about:torconnect": {
+      RPMAddMessageListener: ["torconnect:state-change"],
+      RPMSendAsyncMessage: [
+        "torconnect:open-tor-preferences",
+        "torconnect:begin-bootstrap",
+        "torconnect:begin-autobootstrap",
+        "torconnect:cancel-bootstrap",
+        "torconnect:set-quickstart",
+        "torconnect:view-tor-logs",
+        "torconnect:restart",
+      ],
+      RPMSendQuery: [
+        "torconnect:get-init-args",
+        "torconnect:get-country-codes",
+      ],
+    },
   },
 
   /**
diff --git a/toolkit/mozapps/update/UpdateService.jsm b/toolkit/mozapps/update/UpdateService.jsm
index 4d1b1c59eff5c..cd87b21b0ff9c 100644
--- a/toolkit/mozapps/update/UpdateService.jsm
+++ b/toolkit/mozapps/update/UpdateService.jsm
@@ -12,6 +12,17 @@ const { AppConstants } = ChromeUtils.import(
 const { AUSTLMY } = ChromeUtils.import(
   "resource://gre/modules/UpdateTelemetry.jsm"
 );
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+
+function _shouldRegisterBootstrapObserver(errorCode) {
+  return errorCode == PROXY_SERVER_CONNECTION_REFUSED &&
+         !TorProtocolService.isBootstrapDone() &&
+         TorProtocolService.ownsTorDaemon;
+};
+
 const {
   Bits,
   BitsRequest,
@@ -228,6 +239,7 @@ const SERVICE_ERRORS = [
 // Custom update error codes
 const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
 const NETWORK_ERROR_OFFLINE = 111;
+const PROXY_SERVER_CONNECTION_REFUSED = 2152398920;
 
 // Error codes should be < 1000. Errors above 1000 represent http status codes
 const HTTP_ERROR_OFFSET = 1000;
@@ -2613,6 +2625,9 @@ UpdateService.prototype = {
       case "network:offline-status-changed":
         this._offlineStatusChanged(data);
         break;
+      case "torconnect:bootstrap-complete":
+        this._bootstrapComplete();
+        break;
       case "nsPref:changed":
         if (data == PREF_APP_UPDATE_LOG || data == PREF_APP_UPDATE_LOG_FILE) {
           gLogEnabled; // Assigning this before it is lazy-loaded is an error.
@@ -3063,6 +3078,35 @@ UpdateService.prototype = {
     this._attemptResume();
   },
 
+  _registerBootstrapObserver: function AUS__registerBootstrapObserver() {
+    if (this._registeredBootstrapObserver) {
+      LOG(
+        "UpdateService:_registerBootstrapObserver - observer already registered"
+      );
+      return;
+    }
+
+    LOG(
+      "UpdateService:_registerBootstrapObserver - waiting for tor bootstrap to " +
+        "be complete, then forcing another check"
+    );
+
+    Services.obs.addObserver(this, "torconnect:bootstrap-complete");
+    this._registeredBootstrapObserver = true;
+  },
+
+  _bootstrapComplete: function AUS__bootstrapComplete() {
+    Services.obs.removeObserver(this, "torconnect:bootstrap-complete");
+    this._registeredBootstrapObserver = false;
+
+    LOG(
+      "UpdateService:_bootstrapComplete - bootstrapping complete, forcing " +
+        "another background check"
+    );
+
+    this._attemptResume();
+  },
+
   onCheckComplete: function AUS_onCheckComplete(request, updates) {
     this._selectAndInstallUpdate(updates);
   },
@@ -3082,6 +3126,11 @@ UpdateService.prototype = {
         AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE);
       }
       return;
+    } else if (_shouldRegisterBootstrapObserver(update.errorCode)) {
+      // Register boostrap observer to try again, but only when we own the
+      // tor process.
+      this._registerBootstrapObserver();
+      return;
     }
 
     // Send the error code to telemetry
@@ -5843,6 +5892,7 @@ Downloader.prototype = {
     var state = this._patch.state;
     var shouldShowPrompt = false;
     var shouldRegisterOnlineObserver = false;
+    var shouldRegisterBootstrapObserver = false;
     var shouldRetrySoon = false;
     var deleteActiveUpdate = false;
     let migratedToReadyUpdate = false;
@@ -5961,7 +6011,18 @@ Downloader.prototype = {
       );
       shouldRegisterOnlineObserver = true;
       deleteActiveUpdate = false;
-
+    } else if(_shouldRegisterBootstrapObserver(status)) {
+      // Register a bootstrap observer to try again.
+      // The bootstrap observer will continue the incremental download by
+      // calling downloadUpdate on the active update which continues
+      // downloading the file from where it was.
+      LOG("Downloader:onStopRequest - not bootstrapped, register bootstrap observer: true");
+      AUSTLMY.pingDownloadCode(
+        this.isCompleteUpdate,
+        AUSTLMY.DWNLD_RETRY_OFFLINE
+      );
+      shouldRegisterBootstrapObserver = true;
+      deleteActiveUpdate = false;
       // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED,
       // NS_ERROR_NET_RESET and NS_ERROR_DOCUMENT_NOT_CACHED can be returned
       // when disconnecting the internet while a download of a MAR is in
@@ -6083,7 +6144,7 @@ Downloader.prototype = {
 
     // Only notify listeners about the stopped state if we
     // aren't handling an internal retry.
-    if (!shouldRetrySoon && !shouldRegisterOnlineObserver) {
+    if (!shouldRetrySoon && !shouldRegisterOnlineObserver && !shouldRegisterBootstrapObserver) {
       this.updateService.forEachDownloadListener(listener => {
         listener.onStopRequest(request, status);
       });
@@ -6269,6 +6330,9 @@ Downloader.prototype = {
     if (shouldRegisterOnlineObserver) {
       LOG("Downloader:onStopRequest - Registering online observer");
       this.updateService._registerOnlineObserver();
+    } else if (shouldRegisterBootstrapObserver) {
+      LOG("Downloader:onStopRequest - Registering bootstrap observer");
+      this.updateService._registerBootstrapObserver();
     } else if (shouldRetrySoon) {
       LOG("Downloader:onStopRequest - Retrying soon");
       this.updateService._consecutiveSocketErrors++;
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
index 2ff107b553b2a..f8fa83574df70 100644
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
@@ -70,6 +70,10 @@ function getGlobalScriptIncludes(scriptPath) {
     let match = line.match(globalScriptsRegExp);
     if (match) {
       let sourceFile = match[1]
+        .replace(
+          "chrome://browser/content/torconnect/",
+          "browser/components/torconnect/content/"
+        )
         .replace(
           "chrome://browser/content/search/",
           "browser/components/search/content/"

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


More information about the tbb-commits mailing list