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

gitolite role git at cupani.torproject.org
Wed Aug 3 13:05:51 UTC 2022


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

richard pushed a commit to branch tor-browser-91.12.0esr-12.0-1
in repository tor-browser.

commit ee47878a3c6aa72c7a2a456c243ba1ac2a194a04
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/torconnect/TorConnectChild.jsm  |   9 +
 browser/components/torconnect/TorConnectParent.jsm | 202 +++++
 .../torconnect/content/aboutTorConnect.css         | 325 ++++++++
 .../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       |  93 +++
 .../torconnect/content/torconnect-urlbar.css       |  57 ++
 .../torconnect/content/torconnect-urlbar.inc.xhtml |  10 +
 browser/components/torconnect/jar.mn               |  12 +
 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        |  26 +
 toolkit/mozapps/update/UpdateService.jsm           |  68 +-
 .../lib/environments/browser-window.js             |   4 +
 40 files changed, 1979 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 800dcb92d3a87..496b3fa86ec2c 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",
@@ -643,6 +644,7 @@ var gPageIcons = {
 
 var gInitialPages = [
   "about:tor",
+  "about:torconnect",
   "about:blank",
   "about:newtab",
   "about:home",
@@ -1853,6 +1855,8 @@ var gBrowserInit = {
     }
 
     this._loadHandled = true;
+
+    TorBootstrapUrlbar.init();
   },
 
   _cancelDelayedStartup() {
@@ -2401,32 +2405,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;
     })());
   },
 
@@ -2493,6 +2513,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 8fbfa05196b07..627e6ac0f8a09 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
@@ -112,6 +113,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 463428ed21b69..ce15a4727efc9 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 59d8536e443e8..fd85693809036 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 4bc7c63a827c9..a80896cc1b869 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -55,6 +55,7 @@ DIRS += [
     "syncedtabs",
     "uitour",
     "urlbar",
+    "torconnect",
     "torpreferences",
     "translation",
 ]
diff --git a/browser/components/torconnect/TorConnectChild.jsm b/browser/components/torconnect/TorConnectChild.jsm
new file mode 100644
index 0000000000000..bd6dd549f156d
--- /dev/null
+++ b/browser/components/torconnect/TorConnectChild.jsm
@@ -0,0 +1,9 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+var EXPORTED_SYMBOLS = ["TorConnectChild"];
+
+const { RemotePageChild } = ChromeUtils.import(
+  "resource://gre/actors/RemotePageChild.jsm"
+);
+
+class TorConnectChild extends RemotePageChild {}
diff --git a/browser/components/torconnect/TorConnectParent.jsm b/browser/components/torconnect/TorConnectParent.jsm
new file mode 100644
index 0000000000000..cf3c1233dd622
--- /dev/null
+++ b/browser/components/torconnect/TorConnectParent.jsm
@@ -0,0 +1,202 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+var EXPORTED_SYMBOLS = ["TorConnectParent"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+const {
+  InternetStatus,
+  TorConnect,
+  TorConnectTopics,
+  TorConnectState,
+} = ChromeUtils.import("resource:///modules/TorConnect.jsm");
+const { TorSettings, TorSettingsTopics, TorSettingsData } = ChromeUtils.import(
+  "resource:///modules/TorSettings.jsm"
+);
+
+const BroadcastTopic = "about-torconnect:broadcast";
+
+/*
+This object is basically a marshalling interface between the TorConnect module
+and a particular about:torconnect page
+*/
+
+class TorConnectParent extends JSWindowActorParent {
+  constructor(...args) {
+    super(...args);
+
+    const self = this;
+
+    this.state = {
+      State: TorConnect.state,
+      StateChanged: false,
+      PreviousState: TorConnectState.Initial,
+      ErrorMessage: TorConnect.errorMessage,
+      ErrorDetails: TorConnect.errorDetails,
+      BootstrapProgress: TorConnect.bootstrapProgress,
+      BootstrapStatus: TorConnect.bootstrapStatus,
+      InternetStatus: TorConnect.internetStatus,
+      DetectedLocation: TorConnect.detectedLocation,
+      ShowViewLog: TorConnect.logHasWarningOrError,
+      HasBootsrapEverFailed: TorConnect.hasBootstrapEverFailed,
+      QuickStartEnabled: TorSettings.quickstart.enabled,
+      UIState: TorConnect.uiState,
+    };
+
+    // JSWindowActiveParent derived objects cannot observe directly, so create a member
+    // object to do our observing for us
+    //
+    // This object converts the various lifecycle events from the TorConnect module, and
+    // maintains a state object which we pass down to our about:torconnect page, which uses
+    // the state object to update its UI
+    this.torConnectObserver = {
+      observe(aSubject, aTopic, aData) {
+        let obj = aSubject?.wrappedJSObject;
+
+        // update our state struct based on received torconnect topics and forward on
+        // to aboutTorConnect.js
+        self.state.StateChanged = false;
+        switch (aTopic) {
+          case TorConnectTopics.StateChange: {
+            self.state.PreviousState = self.state.State;
+            self.state.State = obj.state;
+            self.state.StateChanged = true;
+
+            // clear any previous error information if we are bootstrapping
+            if (self.state.State === TorConnectState.Bootstrapping) {
+              self.state.ErrorMessage = null;
+              self.state.ErrorDetails = null;
+            }
+            self.state.HasBootsrapEverFailed =
+              TorConnect.hasBootstrapEverFailed;
+            break;
+          }
+          case TorConnectTopics.BootstrapProgress: {
+            self.state.BootstrapProgress = obj.progress;
+            self.state.BootstrapStatus = obj.status;
+            self.state.ShowViewLog = obj.hasWarnings;
+            break;
+          }
+          case TorConnectTopics.BootstrapComplete: {
+            // noop
+            break;
+          }
+          case TorConnectTopics.BootstrapError: {
+            self.state.ErrorMessage = obj.message;
+            self.state.ErrorDetails = obj.details;
+            self.state.InternetStatus = TorConnect.internetStatus;
+            self.state.DetectedLocation = TorConnect.detectedLocation;
+            self.state.ShowViewLog = true;
+            break;
+          }
+          case TorConnectTopics.FatalError: {
+            // TODO: handle
+            break;
+          }
+          case TorSettingsTopics.SettingChanged: {
+            if (aData === TorSettingsData.QuickStartEnabled) {
+              self.state.QuickStartEnabled = obj.value;
+            } else {
+              // this isn't a setting torconnect cares about
+              return;
+            }
+            break;
+          }
+          default: {
+            console.log(`TorConnect: unhandled observe topic '${aTopic}'`);
+          }
+        }
+
+        self.sendAsyncMessage("torconnect:state-change", self.state);
+      },
+    };
+
+    // observe all of the torconnect:.* topics
+    for (const key in TorConnectTopics) {
+      const topic = TorConnectTopics[key];
+      Services.obs.addObserver(this.torConnectObserver, topic);
+    }
+    Services.obs.addObserver(
+      this.torConnectObserver,
+      TorSettingsTopics.SettingChanged
+    );
+
+    this.userActionObserver = {
+      observe(aSubject, aTopic, aData) {
+        let obj = aSubject?.wrappedJSObject;
+        if (obj) {
+          obj.connState = self.state;
+          self.sendAsyncMessage("torconnect:user-action", obj);
+        }
+      },
+    };
+    Services.obs.addObserver(this.userActionObserver, BroadcastTopic);
+  }
+
+  willDestroy() {
+    // stop observing all of our torconnect:.* topics
+    for (const key in TorConnectTopics) {
+      const topic = TorConnectTopics[key];
+      Services.obs.removeObserver(this.torConnectObserver, topic);
+    }
+    Services.obs.removeObserver(
+      this.torConnectObserver,
+      TorSettingsTopics.SettingChanged
+    );
+    Services.obs.removeObserver(this.userActionObserver, BroadcastTopic);
+  }
+
+  async receiveMessage(message) {
+    switch (message.name) {
+      case "torconnect:set-quickstart":
+        TorSettings.quickstart.enabled = message.data;
+        TorSettings.saveToPrefs().applySettings();
+        break;
+      case "torconnect:open-tor-preferences":
+        TorConnect.openTorPreferences();
+        break;
+      case "torconnect:cancel-bootstrap":
+        TorConnect.cancelBootstrap();
+        break;
+      case "torconnect:begin-bootstrap":
+        TorConnect.beginBootstrap();
+        break;
+      case "torconnect:begin-autobootstrap":
+        TorConnect.beginAutoBootstrap(message.data);
+        break;
+      case "torconnect:view-tor-logs":
+        TorConnect.viewTorLogs();
+        break;
+      case "torconnect:restart":
+        Services.startup.quit(
+          Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit
+        );
+        break;
+      case "torconnect:set-ui-state":
+        TorConnect.uiState = message.data;
+        this.state.UIState = TorConnect.uiState;
+        break;
+      case "torconnect:broadcast-user-action":
+        Services.obs.notifyObservers(message.data, BroadcastTopic);
+        break;
+      case "torconnect:get-init-args":
+        // called on AboutTorConnect.init(), pass down all state data it needs to init
+
+        // pretend this is a state transition on init
+        // so we always get fresh UI
+        this.state.StateChanged = true;
+        this.state.UIState = TorConnect.uiState;
+        return {
+          TorStrings,
+          TorConnectState,
+          InternetStatus,
+          Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr",
+          State: this.state,
+          CountryNames: TorConnect.countryNames,
+        };
+      case "torconnect:get-country-codes":
+        return TorConnect.getCountryCodes();
+    }
+    return undefined;
+  }
+}
diff --git a/browser/components/torconnect/content/aboutTorConnect.css b/browser/components/torconnect/content/aboutTorConnect.css
new file mode 100644
index 0000000000000..ae6a76db9fe75
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.css
@@ -0,0 +1,325 @@
+
+/* 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 (-moz-toolbar-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 (-moz-toolbar-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: fill, fill-opacity;
+  fill-opacity: var(--onion-opacity);
+  fill: var(--onion-color);
+}
+
+.title.offline, .title.assist, .title.final {
+  background-image: url("chrome://browser/content/torconnect/connection-failure.svg");
+}
+
+.title.location {
+  background-image: url("chrome://browser/content/torconnect/connection-location.svg");
+}
diff --git a/browser/components/torconnect/content/aboutTorConnect.js b/browser/components/torconnect/content/aboutTorConnect.js
new file mode 100644
index 0000000000000..6b33442c7d02b
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.js
@@ -0,0 +1,843 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+/* eslint-env mozilla/frame-script */
+
+// populated in AboutTorConnect.init()
+let TorStrings = {};
+let TorConnectState = {};
+let InternetStatus = {};
+
+const UIStates = Object.freeze({
+  ConnectToTor: "ConnectToTor",
+  Offline: "Offline",
+  ConnectionAssist: "ConnectionAssist",
+  CouldNotLocate: "CouldNotLocate",
+  LocationConfirm: "LocationConfirm",
+  FinalError: "FinalError",
+});
+
+const BreadcrumbStatus = Object.freeze({
+  Hidden: "hidden",
+  Disabled: "disabled",
+  Default: "default",
+  Active: "active",
+  Error: "error",
+});
+
+class AboutTorConnect {
+  selectors = Object.freeze({
+    textContainer: {
+      title: "div.title",
+      titleText: "h1.title-text",
+      longContentText: "#connectLongContentText",
+    },
+    progress: {
+      description: "p#connectShortDescText",
+      meter: "div#progressBar",
+    },
+    breadcrumbs: {
+      container: "#breadcrumbs",
+      connectToTor: {
+        link: "#connect-to-tor",
+        label: "#connect-to-tor .breadcrumb-label",
+      },
+      connectionAssist: {
+        separator: "#connection-assist-separator",
+        link: "#connection-assist",
+        label: "#connection-assist .breadcrumb-label",
+      },
+      tryBridge: {
+        separator: "#try-bridge-separator",
+        link: "#try-bridge",
+        label: "#try-bridge .breadcrumb-label",
+      },
+    },
+    viewLog: {
+      container: "#viewLogContainer",
+      link: "span#viewLogLink",
+    },
+    quickstart: {
+      container: "div#quickstartContainer",
+      checkbox: "input#quickstartCheckbox",
+      label: "label#quickstartCheckboxLabel",
+    },
+    buttons: {
+      restart: "button#restartButton",
+      configure: "button#configureButton",
+      cancel: "button#cancelButton",
+      connect: "button#connectButton",
+      tryBridge: "button#tryBridgeButton",
+      locationDropdownLabel: "#locationDropdownLabel",
+      locationDropdown: "form#locationDropdown",
+      locationDropdownSelect: "form#locationDropdown select",
+    },
+  });
+
+  elements = Object.freeze({
+    title: document.querySelector(this.selectors.textContainer.title),
+    titleText: document.querySelector(this.selectors.textContainer.titleText),
+    longContentText: document.querySelector(
+      this.selectors.textContainer.longContentText
+    ),
+    progressDescription: document.querySelector(
+      this.selectors.progress.description
+    ),
+    progressMeter: document.querySelector(this.selectors.progress.meter),
+    breadcrumbContainer: document.querySelector(
+      this.selectors.breadcrumbs.container
+    ),
+    connectToTorLink: document.querySelector(
+      this.selectors.breadcrumbs.connectToTor.link
+    ),
+    connectToTorLabel: document.querySelector(
+      this.selectors.breadcrumbs.connectToTor.label
+    ),
+    connectionAssistSeparator: document.querySelector(
+      this.selectors.breadcrumbs.connectionAssist.separator
+    ),
+    connectionAssistLink: document.querySelector(
+      this.selectors.breadcrumbs.connectionAssist.link
+    ),
+    connectionAssistLabel: document.querySelector(
+      this.selectors.breadcrumbs.connectionAssist.label
+    ),
+    tryBridgeSeparator: document.querySelector(
+      this.selectors.breadcrumbs.tryBridge.separator
+    ),
+    tryBridgeLink: document.querySelector(
+      this.selectors.breadcrumbs.tryBridge.link
+    ),
+    tryBridgeLabel: document.querySelector(
+      this.selectors.breadcrumbs.tryBridge.label
+    ),
+    viewLogContainer: document.querySelector(this.selectors.viewLog.container),
+    viewLogLink: document.querySelector(this.selectors.viewLog.link),
+    quickstartContainer: document.querySelector(
+      this.selectors.quickstart.container
+    ),
+    quickstartCheckbox: document.querySelector(
+      this.selectors.quickstart.checkbox
+    ),
+    quickstartLabel: document.querySelector(this.selectors.quickstart.label),
+    restartButton: document.querySelector(this.selectors.buttons.restart),
+    configureButton: document.querySelector(this.selectors.buttons.configure),
+    cancelButton: document.querySelector(this.selectors.buttons.cancel),
+    connectButton: document.querySelector(this.selectors.buttons.connect),
+    locationDropdownLabel: document.querySelector(
+      this.selectors.buttons.locationDropdownLabel
+    ),
+    locationDropdown: document.querySelector(
+      this.selectors.buttons.locationDropdown
+    ),
+    locationDropdownSelect: document.querySelector(
+      this.selectors.buttons.locationDropdownSelect
+    ),
+    tryBridgeButton: document.querySelector(this.selectors.buttons.tryBridge),
+  });
+
+  // a redirect url can be passed as a query parameter for the page to
+  // forward us to once bootstrap completes (otherwise the window will just close)
+  redirect = null;
+
+  uiState = {
+    currentState: UIStates.ConnectToTor,
+    connectIsTryAgain: false,
+    allowAutomaticLocation: true,
+    selectedLocation: "automatic",
+    bootstrapCause: UIStates.ConnectToTor,
+  };
+
+  locations = {};
+
+  constructor() {
+    this.uiStates = Object.freeze(
+      Object.fromEntries([
+        [UIStates.ConnectToTor, this.showConnectToTor.bind(this)],
+        [UIStates.Offline, this.showOffline.bind(this)],
+        [UIStates.ConnectionAssist, this.showConnectionAssistant.bind(this)],
+        [UIStates.CouldNotLocate, this.showCouldNotLocate.bind(this)],
+        [UIStates.LocationConfirm, this.showLocationConfirmation.bind(this)],
+        [UIStates.FinalError, this.showFinalError.bind(this)],
+      ])
+    );
+  }
+
+  beginBootstrap() {
+    RPMSendAsyncMessage("torconnect:begin-bootstrap");
+  }
+
+  beginAutoBootstrap(countryCode) {
+    if (countryCode === "automatic") {
+      countryCode = "";
+    }
+    RPMSendAsyncMessage("torconnect:begin-autobootstrap", countryCode);
+  }
+
+  cancelBootstrap() {
+    RPMSendAsyncMessage("torconnect:cancel-bootstrap");
+  }
+
+  transitionUIState(nextState, connState) {
+    if (nextState !== this.uiState.currentState) {
+      this.uiState.currentState = nextState;
+      this.saveUIState();
+    }
+    this.uiStates[nextState](connState);
+  }
+
+  saveUIState() {
+    RPMSendAsyncMessage("torconnect:set-ui-state", this.uiState);
+  }
+
+  /*
+  Element helper methods
+  */
+
+  show(element, primary) {
+    element.classList.toggle("primary", primary !== undefined && primary);
+    element.removeAttribute("hidden");
+  }
+
+  hide(element) {
+    element.setAttribute("hidden", "true");
+  }
+
+  hideButtons() {
+    this.hide(this.elements.quickstartContainer);
+    this.hide(this.elements.restartButton);
+    this.hide(this.elements.configureButton);
+    this.hide(this.elements.cancelButton);
+    this.hide(this.elements.connectButton);
+    this.hide(this.elements.locationDropdownLabel);
+    this.hide(this.elements.locationDropdown);
+    this.hide(this.elements.tryBridgeButton);
+  }
+
+  populateLocations() {
+    const selectCountryRegion = document.createElement("option");
+    selectCountryRegion.textContent = TorStrings.torConnect.selectCountryRegion;
+    selectCountryRegion.value = "";
+
+    // get all codes and names from TorStrings
+    const locationNodes = [];
+    for (const [code, name] of Object.entries(this.locations)) {
+      let option = document.createElement("option");
+      option.value = code;
+      option.textContent = name;
+      locationNodes.push(option);
+    }
+    // locale sort by name
+    locationNodes.sort((left, right) =>
+      left.textContent.localeCompare(right.textContent)
+    );
+    this.elements.locationDropdownSelect.append(
+      selectCountryRegion,
+      ...locationNodes
+    );
+  }
+
+  populateFrequentLocations(locations) {
+    this.removeFrequentLocations();
+    if (!locations || !locations.length) {
+      return;
+    }
+
+    const locationNodes = [];
+    for (const code of locations) {
+      const option = document.createElement("option");
+      option.value = code;
+      option.className = "frequent-location";
+      // codes (partially) come from rdsys service, so make sure we have a
+      // string defined for it
+      let name = this.locations[code];
+      if (!name) {
+        name = code;
+      }
+      option.textContent = name;
+      locationNodes.push(option);
+    }
+    // locale sort by name
+    locationNodes.sort((left, right) =>
+      left.textContent.localeCompare(right.textContent)
+    );
+
+    const frequentGroup = document.createElement("optgroup");
+    frequentGroup.setAttribute(
+      "label",
+      TorStrings.torConnect.frequentLocations
+    );
+    frequentGroup.className = "frequent-location";
+    const locationGroup = document.createElement("optgroup");
+    locationGroup.setAttribute("label", TorStrings.torConnect.otherLocations);
+    locationGroup.className = "frequent-location";
+    // options[0] is either "Select Country or Region" or "Automatic"
+    this.elements.locationDropdownSelect.options[0].after(
+      frequentGroup,
+      ...locationNodes,
+      locationGroup
+    );
+  }
+
+  removeFrequentLocations() {
+    const select = this.elements.locationDropdownSelect;
+    for (const option of select.querySelectorAll(".frequent-location")) {
+      option.remove();
+    }
+  }
+
+  validateLocation() {
+    const selectedIndex = this.elements.locationDropdownSelect.selectedIndex;
+    const selectedOption = this.elements.locationDropdownSelect.options[
+      selectedIndex
+    ];
+    if (!selectedOption.value) {
+      this.elements.tryBridgeButton.setAttribute("disabled", "disabled");
+    } else {
+      this.elements.tryBridgeButton.removeAttribute("disabled");
+    }
+  }
+
+  setTitle(title, className) {
+    this.elements.titleText.textContent = title;
+    this.elements.title.className = "title";
+    if (className) {
+      this.elements.title.classList.add(className);
+    }
+    document.title = title;
+  }
+
+  setLongText(...args) {
+    this.elements.longContentText.textContent = "";
+    this.elements.longContentText.append(...args);
+  }
+
+  setProgress(description, visible, percent) {
+    this.elements.progressDescription.textContent = description;
+    if (visible) {
+      this.show(this.elements.progressMeter);
+      this.elements.progressMeter.style.width = `${percent}%`;
+    } else {
+      this.hide(this.elements.progressMeter);
+    }
+  }
+
+  setBreadcrumbsStatus(connectToTor, connectionAssist, tryBridge) {
+    this.elements.breadcrumbContainer.classList.remove("hidden");
+    const elems = [
+      [this.elements.connectToTorLink, connectToTor, null],
+      [
+        this.elements.connectionAssistLink,
+        connectionAssist,
+        this.elements.connectionAssistSeparator,
+      ],
+      [
+        this.elements.tryBridgeLink,
+        tryBridge,
+        this.elements.tryBridgeSeparator,
+      ],
+    ];
+    elems.forEach(([elem, status, separator]) => {
+      elem.classList.remove(BreadcrumbStatus.Hidden);
+      elem.classList.remove(BreadcrumbStatus.Disabled);
+      elem.classList.remove(BreadcrumbStatus.Active);
+      elem.classList.remove(BreadcrumbStatus.Error);
+      if (status !== "") {
+        elem.classList.add(status);
+      }
+      separator?.classList.toggle("hidden", status === BreadcrumbStatus.Hidden);
+    });
+  }
+
+  hideBreadcrumbs() {
+    this.elements.breadcrumbContainer.classList.add("hidden");
+  }
+
+  /*
+  These methods update the UI based on the current TorConnect state
+  */
+
+  updateUI(state) {
+    // calls update_$state()
+    this[`update_${state.State}`](state);
+    this.elements.quickstartCheckbox.checked = state.QuickStartEnabled;
+  }
+
+  /* Per-state updates */
+
+  update_Initial(state) {
+    this.showConnectToTor(state);
+  }
+
+  update_Configuring(state) {
+    if (
+      state.StateChanged &&
+      (state.PreviousState === TorConnectState.Bootstrapping ||
+        state.PreviousState === TorConnectState.AutoBootstrapping)
+    ) {
+      // The bootstrap has been cancelled
+      this.transitionUIState(this.uiState.bootstrapCause, state);
+    }
+  }
+
+  update_AutoBootstrapping(state) {
+    this.showBootstrapping(state);
+  }
+
+  update_Bootstrapping(state) {
+    this.showBootstrapping(state);
+  }
+
+  update_Error(state) {
+    if (!this.uiState.connectIsTryAgain) {
+      // TorConnect.hasBootstrapEverFailed remains false in case of Internet
+      // offline
+      this.uiState.connectIsTryAgain = true;
+      this.saveUIState();
+    }
+    if (!state.StateChanged) {
+      return;
+    }
+    if (state.InternetStatus === InternetStatus.Offline) {
+      this.transitionUIState(UIStates.Offline, state);
+    } else if (state.PreviousState === TorConnectState.Bootstrapping) {
+      this.transitionUIState(UIStates.ConnectionAssist, state);
+    } else if (state.PreviousState === TorConnectState.AutoBootstrapping) {
+      if (this.uiState.bootstrapCause === UIStates.ConnectionAssist) {
+        if (this.getLocation() === "automatic") {
+          this.uiState.allowAutomaticLocation = false;
+          if (!state.DetectedLocation) {
+            this.transitionUIState(UIStates.CouldNotLocate, state);
+            return;
+          }
+          // Change the location only here, to avoid overriding any user change/
+          // insisting with the detected location
+          this.setLocation(state.DetectedLocation);
+        }
+        this.transitionUIState(UIStates.LocationConfirm, state);
+      } else {
+        this.transitionUIState(UIStates.FinalError, state);
+      }
+    } else {
+      console.error(
+        "We received an error starting from an unexpected state",
+        state
+      );
+    }
+  }
+
+  update_Bootstrapped(state) {
+    const showProgressbar = true;
+
+    this.setTitle(TorStrings.torConnect.torConnected, "");
+    this.setLongText(TorStrings.settings.torPreferencesDescription);
+    this.setProgress("", showProgressbar, 100);
+    this.hideButtons();
+
+    // redirects page to the requested redirect url, removes about:torconnect
+    // from the page stack, so users cannot accidentally go 'back' to the
+    // now unresponsive page
+    window.location.replace(this.redirect);
+  }
+
+  update_Disabled(state) {
+    // TODO: we should probably have some UX here if a user goes to about:torconnect when
+    // it isn't in use (eg using tor-launcher or system tor)
+  }
+
+  showConnectToTor(state) {
+    this.setTitle(TorStrings.torConnect.torConnect, "");
+    this.setLongText(TorStrings.settings.torPreferencesDescription);
+    this.setProgress("", false);
+    this.hide(this.elements.viewLogContainer);
+    this.hideButtons();
+    this.show(this.elements.quickstartContainer);
+    this.show(this.elements.configureButton);
+    this.show(this.elements.connectButton, true);
+    if (state?.StateChanged) {
+      this.elements.connectButton.focus();
+    }
+    if (this.uiState.connectIsTryAgain) {
+      this.setBreadcrumbsStatus(
+        BreadcrumbStatus.Active,
+        BreadcrumbStatus.Default,
+        BreadcrumbStatus.Disabled
+      );
+      this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
+    }
+    this.uiState.bootstrapCause = UIStates.ConnectToTor;
+    this.saveUIState();
+  }
+
+  showBootstrapping(state) {
+    const showProgressbar = true;
+    let title = "";
+    let description = "";
+    const breadcrumbs = [
+      BreadcrumbStatus.Disabled,
+      BreadcrumbStatus.Disabled,
+      BreadcrumbStatus.Disabled,
+    ];
+    switch (this.uiState.bootstrapCause) {
+      case UIStates.ConnectToTor:
+        breadcrumbs[0] = BreadcrumbStatus.Active;
+        title = this.uiState.connectIsTryAgain
+          ? TorStrings.torConnect.tryAgain
+          : TorStrings.torConnect.torConnecting;
+        description = TorStrings.settings.torPreferencesDescription;
+        break;
+      case UIStates.ConnectionAssist:
+        breadcrumbs[2] = BreadcrumbStatus.Active;
+        title = TorStrings.torConnect.tryingBridge;
+        description = TorStrings.torConnect.assistDescription;
+        break;
+      case UIStates.CouldNotLocate:
+        breadcrumbs[2] = BreadcrumbStatus.Active;
+        title = TorStrings.torConnect.tryingBridgeAgain;
+        description = TorStrings.torConnect.errorLocationDescription;
+        break;
+      case UIStates.LocationConfirm:
+        breadcrumbs[2] = BreadcrumbStatus.Active;
+        title = TorStrings.torConnect.tryingBridgeAgain;
+        description = TorStrings.torConnect.isLocationCorrectDescription;
+        break;
+    }
+    this.setTitle(title, "");
+    this.showConfigureConnectionLink(description);
+    this.setProgress("", showProgressbar, state.BootstrapProgress);
+    if (state.HasBootsrapEverFailed) {
+      this.setBreadcrumbsStatus(...breadcrumbs);
+    } else {
+      this.hideBreadcrumbs();
+    }
+    this.hideButtons();
+    if (state.ShowViewLog) {
+      this.show(this.elements.viewLogContainer);
+    } else {
+      this.hide(this.elements.viewLogContainer);
+    }
+    this.show(this.elements.cancelButton, true);
+    if (state.StateChanged) {
+      this.elements.cancelButton.focus();
+    }
+  }
+
+  showOffline(error) {
+    this.setTitle(TorStrings.torConnect.noInternet, "offline");
+    this.setLongText(TorStrings.torConnect.noInternetDescription);
+    this.setProgress(error, false);
+    this.setBreadcrumbsStatus(
+      BreadcrumbStatus.Default,
+      BreadcrumbStatus.Active,
+      BreadcrumbStatus.Hidden
+    );
+    this.show(this.elements.viewLogContainer);
+    this.hideButtons();
+    this.show(this.elements.configureButton);
+    this.show(this.elements.connectButton, true);
+    this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
+  }
+
+  showConnectionAssistant(state) {
+    this.setTitle(TorStrings.torConnect.couldNotConnect, "assist");
+    this.showConfigureConnectionLink(TorStrings.torConnect.assistDescription);
+    this.setProgress(state?.ErrorDetails, false);
+    this.setBreadcrumbsStatus(
+      BreadcrumbStatus.Default,
+      BreadcrumbStatus.Active,
+      BreadcrumbStatus.Disabled
+    );
+    this.showLocationForm(false, TorStrings.torConnect.tryBridge);
+    if (state?.StateChanged) {
+      this.elements.tryBridgeButton.focus();
+    }
+    this.uiState.bootstrapCause = UIStates.ConnectionAssist;
+    this.saveUIState();
+  }
+
+  showCouldNotLocate(state) {
+    this.uiState.allowAutomaticLocation = false;
+    this.setTitle(TorStrings.torConnect.errorLocation, "location");
+    this.showConfigureConnectionLink(
+      TorStrings.torConnect.errorLocationDescription
+    );
+    this.setProgress(state.ErrorMessage, false);
+    this.setBreadcrumbsStatus(
+      BreadcrumbStatus.Default,
+      BreadcrumbStatus.Active,
+      BreadcrumbStatus.Disabled
+    );
+    this.show(this.elements.viewLogContainer);
+    this.showLocationForm(true, TorStrings.torConnect.tryBridge);
+    if (state.StateChanged) {
+      this.elements.tryBridgeButton.focus();
+    }
+    this.uiState.bootstrapCause = UIStates.CouldNotLocate;
+    this.saveUIState();
+  }
+
+  showLocationConfirmation(state) {
+    this.setTitle(TorStrings.torConnect.isLocationCorrect, "location");
+    this.showConfigureConnectionLink(
+      TorStrings.torConnect.isLocationCorrectDescription
+    );
+    this.setProgress(state.ErrorMessage, false);
+    this.setBreadcrumbsStatus(
+      BreadcrumbStatus.Default,
+      BreadcrumbStatus.Default,
+      BreadcrumbStatus.Active
+    );
+    this.show(this.elements.viewLogContainer);
+    this.showLocationForm(true, TorStrings.torConnect.tryAgain);
+    if (state.StateChanged) {
+      this.elements.tryBridgeButton.focus();
+    }
+    this.uiState.bootstrapCause = UIStates.LocationConfirm;
+    this.saveUIState();
+  }
+
+  showFinalError(state) {
+    this.setTitle(TorStrings.torConnect.finalError, "final");
+    this.setLongText(TorStrings.torConnect.finalErrorDescription);
+    this.setProgress(state ? state.ErrorDetails : "", false);
+    this.setBreadcrumbsStatus(
+      BreadcrumbStatus.Default,
+      BreadcrumbStatus.Default,
+      BreadcrumbStatus.Active
+    );
+    this.hideButtons();
+    this.show(this.elements.restartButton);
+    this.show(this.elements.configureButton, true);
+  }
+
+  showConfigureConnectionLink(text) {
+    const pieces = text.split("#1");
+    const link = document.createElement("a");
+    link.textContent = TorStrings.torConnect.configureConnection;
+    link.setAttribute("href", "#");
+    link.addEventListener("click", e => {
+      e.preventDefault();
+      RPMSendAsyncMessage("torconnect:open-tor-preferences");
+    });
+    if (pieces.length > 1) {
+      const first = pieces.shift();
+      this.setLongText(first, link, ...pieces);
+    } else {
+      this.setLongText(text);
+    }
+  }
+
+  showLocationForm(isError, buttonLabel) {
+    this.hideButtons();
+    RPMSendQuery("torconnect:get-country-codes").then(codes => {
+      if (codes && codes.length) {
+        this.populateFrequentLocations(codes);
+        this.setLocation();
+      }
+    });
+    let firstOpt = this.elements.locationDropdownSelect.options[0];
+    if (this.uiState.allowAutomaticLocation) {
+      firstOpt.value = "automatic";
+      firstOpt.textContent = TorStrings.torConnect.automatic;
+    } else {
+      firstOpt.value = "";
+      firstOpt.textContent = TorStrings.torConnect.selectCountryRegion;
+    }
+    this.setLocation();
+    this.validateLocation();
+    this.show(this.elements.locationDropdownLabel);
+    this.show(this.elements.locationDropdown);
+    this.elements.locationDropdownLabel.classList.toggle("error", isError);
+    this.show(this.elements.tryBridgeButton, true);
+    this.elements.tryBridgeButton.classList.toggle("danger-button", isError);
+    if (buttonLabel !== undefined) {
+      this.elements.tryBridgeButton.textContent = buttonLabel;
+    }
+  }
+
+  getLocation() {
+    const selectedIndex = this.elements.locationDropdownSelect.selectedIndex;
+    return this.elements.locationDropdownSelect.options[selectedIndex].value;
+  }
+
+  setLocation(code) {
+    if (!code) {
+      code = this.uiState.selectedLocation;
+    } else {
+      this.uiState.selectedLocation = code;
+    }
+    if (this.getLocation() === code) {
+      return;
+    }
+    const options = this.elements.locationDropdownSelect.options;
+    // We need to do this way, because we have repeated values that break
+    // the .value way to select (which would however require the label,
+    // rather than the code)...
+    for (let i = 0; i < options.length; i++) {
+      if (options[i].value === code) {
+        this.elements.locationDropdownSelect.selectedIndex = i;
+        break;
+      }
+    }
+    this.validateLocation();
+  }
+
+  initElements(direction) {
+    document.documentElement.setAttribute("dir", direction);
+
+    this.elements.connectToTorLink.addEventListener("click", event => {
+      if (this.uiState.currentState === UIStates.ConnectToTor) {
+        return;
+      }
+      this.transitionUIState(UIStates.ConnectToTor, null);
+      RPMSendAsyncMessage("torconnect:broadcast-user-action", {
+        uiState: UIStates.ConnectToTor,
+      });
+    });
+    this.elements.connectToTorLabel.textContent =
+      TorStrings.torConnect.torConnect;
+    this.elements.connectionAssistLink.addEventListener("click", event => {
+      if (
+        this.elements.connectionAssistLink.classList.contains(
+          BreadcrumbStatus.Active
+        ) ||
+        this.elements.connectionAssistLink.classList.contains(
+          BreadcrumbStatus.Disabled
+        )
+      ) {
+        return;
+      }
+      this.transitionUIState(UIStates.ConnectionAssist, null);
+      RPMSendAsyncMessage("torconnect:broadcast-user-action", {
+        uiState: UIStates.ConnectionAssist,
+      });
+    });
+    this.elements.connectionAssistLabel.textContent =
+      TorStrings.torConnect.breadcrumbAssist;
+    this.elements.tryBridgeLabel.textContent =
+      TorStrings.torConnect.breadcrumbTryBridge;
+
+    this.hide(this.elements.viewLogContainer);
+    this.elements.viewLogLink.textContent = TorStrings.torConnect.viewLog;
+    this.elements.viewLogLink.addEventListener("click", event => {
+      RPMSendAsyncMessage("torconnect:view-tor-logs");
+    });
+
+    this.elements.quickstartCheckbox.addEventListener("change", () => {
+      const quickstart = this.elements.quickstartCheckbox.checked;
+      RPMSendAsyncMessage("torconnect:set-quickstart", quickstart);
+    });
+    this.elements.quickstartLabel.textContent =
+      TorStrings.settings.quickstartCheckbox;
+
+    this.elements.restartButton.textContent =
+      TorStrings.torConnect.restartTorBrowser;
+    this.elements.restartButton.addEventListener("click", () => {
+      RPMSendAsyncMessage("torconnect:restart");
+    });
+
+    this.elements.configureButton.textContent =
+      TorStrings.torConnect.torConfigure;
+    this.elements.configureButton.addEventListener("click", () => {
+      RPMSendAsyncMessage("torconnect:open-tor-preferences");
+    });
+
+    this.elements.cancelButton.textContent = TorStrings.torConnect.cancel;
+    this.elements.cancelButton.addEventListener("click", () => {
+      this.cancelBootstrap();
+    });
+
+    this.elements.connectButton.textContent =
+      TorStrings.torConnect.torConnectButton;
+    this.elements.connectButton.addEventListener("click", () => {
+      this.beginBootstrap();
+    });
+
+    this.populateLocations();
+    this.elements.locationDropdownSelect.addEventListener("change", () => {
+      this.uiState.selectedLocation = this.getLocation();
+      this.saveUIState();
+      this.validateLocation();
+      RPMSendAsyncMessage("torconnect:broadcast-user-action", {
+        location: this.uiState.selectedLocation,
+      });
+    });
+
+    this.elements.locationDropdownLabel.textContent =
+      TorStrings.torConnect.yourLocation;
+
+    this.elements.tryBridgeButton.textContent = TorStrings.torConnect.tryBridge;
+    this.elements.tryBridgeButton.addEventListener("click", () => {
+      const value = this.getLocation();
+      if (value === "automatic") {
+        this.beginAutoBootstrap();
+      } else {
+        this.beginAutoBootstrap(value);
+      }
+    });
+  }
+
+  initObservers() {
+    // TorConnectParent feeds us state blobs to we use to update our UI
+    RPMAddMessageListener("torconnect:state-change", ({ data }) => {
+      this.updateUI(data);
+    });
+    RPMAddMessageListener("torconnect:user-action", ({ data }) => {
+      if (data.location) {
+        this.uiState.selectedLocation = data.location;
+        this.setLocation();
+      }
+      if (data.uiState !== undefined) {
+        this.transitionUIState(data.uiState, data.connState);
+      }
+    });
+  }
+
+  initKeyboardShortcuts() {
+    document.onkeydown = evt => {
+      // unfortunately it looks like we still haven't standardized keycodes to
+      // integers, so we must resort to a string compare here :(
+      // see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for relevant documentation
+      if (evt.code === "Escape") {
+        this.cancelBootstrap();
+      }
+    };
+  }
+
+  async init() {
+    // see if a user has a final destination after bootstrapping
+    let params = new URLSearchParams(new URL(document.location.href).search);
+    if (params.has("redirect")) {
+      const encodedRedirect = params.get("redirect");
+      this.redirect = decodeURIComponent(encodedRedirect);
+    } else {
+      // if the user gets here manually or via the button in the urlbar
+      // then we will redirect to about:tor
+      this.redirect = "about:tor";
+    }
+
+    let args = await RPMSendQuery("torconnect:get-init-args");
+
+    // various constants
+    TorStrings = Object.freeze(args.TorStrings);
+    TorConnectState = Object.freeze(args.TorConnectState);
+    InternetStatus = Object.freeze(args.InternetStatus);
+    this.locations = args.CountryNames;
+
+    this.initElements(args.Direction);
+    this.initObservers();
+    this.initKeyboardShortcuts();
+
+    if (Object.keys(args.State.UIState).length) {
+      this.uiState = args.State.UIState;
+    } else {
+      args.State.UIState = this.uiState;
+      this.saveUIState();
+    }
+    this.uiStates[this.uiState.currentState](args.State);
+    // populate UI based on current state
+    this.updateUI(args.State);
+  }
+}
+
+const aboutTorConnect = new AboutTorConnect();
+aboutTorConnect.init();
diff --git a/browser/components/torconnect/content/aboutTorConnect.xhtml b/browser/components/torconnect/content/aboutTorConnect.xhtml
new file mode 100644
index 0000000000000..77d2e68895708
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.xhtml
@@ -0,0 +1,68 @@
+<!-- Copyright (c) 2021, The Tor Project, Inc. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+    <link rel="stylesheet" href="chrome://browser/skin/onionPattern.css" type="text/css" media="all" />
+    <link rel="stylesheet" href="chrome://browser/content/torconnect/aboutTorConnect.css" type="text/css" media="all" />
+  </head>
+  <body>
+    <div id="progressBar">
+      <div id="progressBackground" />
+      <div id="progressSolid" />
+    </div>
+    <div id="connectPageContainer" class="container">
+      <div id="breadcrumbs" class="hidden">
+        <span id="connect-to-tor" class="breadcrumb-item">
+          <span id="connect-to-tor-icon" class="breadcrumb-icon" />
+          <span class="breadcrumb-label"/>
+        </span>
+        <span id="connection-assist-separator" class="breadcrumb-separator breadcrumb-icon" />
+        <span id="connection-assist" class="breadcrumb-item">
+          <span id="connection-assist-icon" class="breadcrumb-icon" />
+          <span class="breadcrumb-label"/>
+        </span>
+        <span id="try-bridge-separator" class="breadcrumb-separator breadcrumb-icon" />
+        <span id="try-bridge" class="breadcrumb-item">
+          <span id="try-bridge-icon" class="breadcrumb-icon" />
+          <span class="breadcrumb-label"/>
+        </span>
+      </div>
+      <div id="text-container">
+        <div class="title">
+          <h1 class="title-text"/>
+        </div>
+        <div id="connectLongContent">
+          <p id="connectLongContentText" />
+        </div>
+        <div id="connectShortDesc">
+          <p id="connectShortDescText" />
+        </div>
+
+        <div id="viewLogContainer">
+          <span id="viewLogLink"></span>
+        </div>
+
+        <div id="quickstartContainer">
+          <input id="quickstartCheckbox" type="checkbox" />
+          <label id="quickstartCheckboxLabel" for="quickstartCheckbox"/>
+        </div>
+
+        <div id="connectButtonContainer" class="button-container">
+          <button id="restartButton" hidden="true"></button>
+          <button id="configureButton" hidden="true"></button>
+          <button id="cancelButton" hidden="true"></button>
+          <button id="connectButton" class="primary" hidden="true"></button>
+          <label id="locationDropdownLabel" for="countries"/>
+          <form id="locationDropdown" hidden="true">
+            <select id="countries">
+            </select>
+          </form>
+          <button id="tryBridgeButton" class="primary" hidden="true"></button>
+        </div>
+      </div>
+    </div>
+#include ../../../themes/shared/onionPattern.inc.xhtml
+  </body>
+  <script src="chrome://browser/content/torconnect/aboutTorConnect.js"/>
+</html>
diff --git a/browser/components/torconnect/content/arrow-right.svg b/browser/components/torconnect/content/arrow-right.svg
new file mode 100644
index 0000000000000..3f6d8ded52bed
--- /dev/null
+++ b/browser/components/torconnect/content/arrow-right.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M10.9991 8.352L5.53406 13.818C5.41557 13.9303 5.25792 13.9918 5.09472 13.9895C4.93152 13.9872 4.77567 13.9212 4.66039 13.8057C4.54511 13.6902 4.47951 13.5342 4.47758 13.3709C4.47565 13.2077 4.53754 13.0502 4.65006 12.932L9.58506 7.998L4.65106 3.067C4.53868 2.94864 4.47697 2.79106 4.47909 2.62786C4.48121 2.46466 4.54698 2.30874 4.66239 2.19333C4.7778 2.07792 4.93372 2.01215 5.09692 2.01003C5.26012 2.00792 5.41769 2.06962 5.53606 2.182L11.0001 7.647L10.9991 8.352Z" fill="conte [...]
+</svg>
diff --git a/browser/components/torconnect/content/bridge.svg b/browser/components/torconnect/content/bridge.svg
new file mode 100644
index 0000000000000..5ae3f05dfd082
--- /dev/null
+++ b/browser/components/torconnect/content/bridge.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M1 9.48528C1 9.48528 3.82843 9.48528 6.65685 6.65685C9.48528 3.82843 9.48528 1 9.48528 1" stroke="context-fill" stroke-width="1.25" stroke-linecap="round"/>
+    <path d="M6.65686 15.1421C6.65686 15.1421 6.65686 12.3137 9.48529 9.48529C12.3137 6.65686 15.1421 6.65686 15.1421 6.65686" stroke="context-fill" stroke-width="1.25" stroke-linecap="round"/>
+</svg>
diff --git a/browser/components/torconnect/content/connection-failure.svg b/browser/components/torconnect/content/connection-failure.svg
new file mode 100644
index 0000000000000..8f2005e360556
--- /dev/null
+++ b/browser/components/torconnect/content/connection-failure.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg fill="none" height="60" viewBox="0 0 60 60" width="60" xmlns="http://www.w3.org/2000/svg">
+  <path fill="context-fill" d="M 30,1.875 C 14.467,1.875 1.875,14.467 1.875,30 c 0,6.725546 2.3647525,12.894963 6.3027344,17.734375 l -4.7636719,4.763672 c -0.7834743,0.783474 -0.7834743,2.044651 0,2.828125 0.7834743,0.783474 2.0446507,0.783474 2.828125,0 C 21.046044,40.52782 34.415343,27.146014 47.546875,14.023438 v -0.002 l 6.779297,-6.7792965 c 0.783474,-0.7834743 0.783474,-2.0446507 0,-2.828125 -0.783474,-0.7834743 -2.044651,-0.7834743 -2.828125,0 L 47.734375,8.1777344 C 42.894963,4. [...]
+  <path fill="#d70022" d="m59.5328 52.4973-10.261-18.5715c-.7112-1.2833-1.9917-1.9258-3.2722-1.9258-1.2806 0-2.5611.6425-3.2704 1.9258l-10.261 18.5715c-1.3701 2.4755.4312 5.5027 3.2704 5.5027h20.5238c2.8373 0 4.6387-3.0272 3.2704-5.5027zm-12.3666-.533-.4666.4642h-1.4l-.4667-.4642v-1.3929l.4667-.4643h1.4l.4666.4643zm0-4.992c0 .3078-.1229.603-.3417.8207s-.5155.34-.8249.34-.6062-.1223-.825-.34-.3417-.5129-.3417-.8207v-6.383c0-.3079.1229-.6031.3417-.8208s.5156-.34.825-.34.6061.1223.8249.34.3 [...]
+</svg>
diff --git a/browser/components/torconnect/content/connection-location.svg b/browser/components/torconnect/content/connection-location.svg
new file mode 100644
index 0000000000000..1e5c41ccf99a0
--- /dev/null
+++ b/browser/components/torconnect/content/connection-location.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg fill="none" height="60" viewBox="0 0 60 60" width="60" xmlns="http://www.w3.org/2000/svg">
+  <path fill="context-fill" d="M 30,1.875 C 14.467,1.875 1.875,14.467 1.875,30 c 0,6.725546 2.3647429,12.894963 6.3027344,17.734375 l -4.7636719,4.763672 c -0.7834743,0.783474 -0.7834743,2.044651 0,2.828125 0.7834743,0.783474 2.0446507,0.783474 2.828125,0 C 21.049647,40.524244 34.416498,27.144859 47.546875,14.023438 v -0.002 l 6.779297,-6.7792965 c 0.783474,-0.7834743 0.783474,-2.0446507 0,-2.828125 -0.783474,-0.7834743 -2.044651,-0.7834743 -2.828125,0 L 47.734375,8.1777344 C 42.894963,4 [...]
+  <path fill="#ffa436" d="m45 30c-3.713 0-7.274 1.475-9.8995 4.1005s-4.1005 6.1865-4.1005 9.8995 1.475 7.274 4.1005 9.8995 6.1865 4.1005 9.8995 4.1005 7.274-1.475 9.8995-4.1005 4.1005-6.1865 4.1005-9.8995-1.475-7.274-4.1005-9.8995-6.1865-4.1005-9.8995-4.1005zm4.5677 3.2667c1.9167.8229 3.5778 2.1443 4.8108 3.8267 1.233 1.6825 1.9928 3.6644 2.2004 5.7399h-4.1608c-.2298-3.4759-1.4862-6.8054-3.6101-9.5666zm-3.8248 0c2.5257 2.5792 4.06 5.967 4.3326 9.5666h-10.151c.2726-3.5996 1.8069-6.9874 4. [...]
+</svg>
diff --git a/browser/components/torconnect/content/onion-slash-fillable.svg b/browser/components/torconnect/content/onion-slash-fillable.svg
new file mode 100644
index 0000000000000..18f1c5a5520bd
--- /dev/null
+++ b/browser/components/torconnect/content/onion-slash-fillable.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
+  <path d="m14.1161 15.6245c-.0821.0001-.1634-.016-.2393-.0474-.0758-.0314-.1447-.0775-.2027-.1356l-12.749984-12.749c-.109266-.11882-.168406-.27526-.165071-.43666.003335-.16139.068886-.31525.182967-.42946.114078-.11421.267868-.17994.429258-.18345.16139-.00352.3179.05544.43685.16457l12.74998 12.75c.1168.1176.1824.2767.1824.4425s-.0656.3249-.1824.4425c-.058.058-.1269.1039-.2028.1352-.0759.0312-.1571.0471-.2392.0468z" fill-opacity="context-fill-opacity" fill="context-fill" />
+  <path d="m 8,0.5000002 c -1.61963,0 -3.1197431,0.5137987 -4.3457031,1.3867188 l 0.84375,0.8417968 0.7792969,0.78125 0.8613281,0.8613282 0.8164062,0.8164062 0.9863281,0.984375 h 0.058594 c 1.00965,0 1.828125,0.818485 1.828125,1.828125 0,0.01968 6.2e-4,0.039074 0,0.058594 L 10.8125,9.0449221 C 10.9334,8.7195921 11,8.3674002 11,8.0000002 c 0,-1.65685 -1.34314,-3 -3,-3 v -1.078125 c 2.25231,0 4.078125,1.825845 4.078125,4.078125 0,0.67051 -0.162519,1.3033281 -0.449219,1.8613281 l 0.861328,0 [...]
+</svg>
diff --git a/browser/components/torconnect/content/onion-slash.svg b/browser/components/torconnect/content/onion-slash.svg
new file mode 100644
index 0000000000000..93eb24b039055
--- /dev/null
+++ b/browser/components/torconnect/content/onion-slash.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
+  <path d="m14.1161 15.6245c-.0821.0001-.1634-.016-.2393-.0474-.0758-.0314-.1447-.0775-.2027-.1356l-12.749984-12.749c-.109266-.11882-.168406-.27526-.165071-.43666.003335-.16139.068886-.31525.182967-.42946.114078-.11421.267868-.17994.429258-.18345.16139-.00352.3179.05544.43685.16457l12.74998 12.75c.1168.1176.1824.2767.1824.4425s-.0656.3249-.1824.4425c-.058.058-.1269.1039-.2028.1352-.0759.0312-.1571.0471-.2392.0468z" fill-opacity="context-fill-opacity" fill="#ff0039" />
+  <path d="m 8,0.5000002 c -1.61963,0 -3.1197431,0.5137987 -4.3457031,1.3867188 l 0.84375,0.8417968 0.7792969,0.78125 0.8613281,0.8613282 0.8164062,0.8164062 0.9863281,0.984375 h 0.058594 c 1.00965,0 1.828125,0.818485 1.828125,1.828125 0,0.01968 6.2e-4,0.039074 0,0.058594 L 10.8125,9.0449221 C 10.9334,8.7195921 11,8.3674002 11,8.0000002 c 0,-1.65685 -1.34314,-3 -3,-3 v -1.078125 c 2.25231,0 4.078125,1.825845 4.078125,4.078125 0,0.67051 -0.162519,1.3033281 -0.449219,1.8613281 l 0.861328,0 [...]
+</svg>
diff --git a/browser/components/torconnect/content/onion.svg b/browser/components/torconnect/content/onion.svg
new file mode 100644
index 0000000000000..7655a800d9eec
--- /dev/null
+++ b/browser/components/torconnect/content/onion.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
+  <path d="M 8 0.5 C 3.85786 0.5 0.5 3.85786 0.5 8 C 0.5 12.1421 3.85786 15.5 8 15.5 C 12.1421 15.5 15.5 12.1421 15.5 8 C 15.5 3.85786 12.1421 0.5 8 0.5 z M 8 1.671875 C 11.4949 1.671875 14.328125 4.50507 14.328125 8 C 14.328125 11.4949 11.4949 14.328125 8 14.328125 L 8 13.25 C 10.89951 13.25 13.25 10.89951 13.25 8 C 13.25 5.10051 10.89951 2.75 8 2.75 L 8 1.671875 z M 8 3.921875 C 10.25231 3.921875 12.078125 5.74772 12.078125 8 C 12.078125 10.25231 10.25231 12.078125 8 12.078125 L 8 11 C [...]
+</svg>
diff --git a/browser/components/torconnect/content/torBootstrapUrlbar.js b/browser/components/torconnect/content/torBootstrapUrlbar.js
new file mode 100644
index 0000000000000..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..043bd1435cda5
--- /dev/null
+++ b/browser/components/torconnect/jar.mn
@@ -0,0 +1,12 @@
+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)
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 d269e915bc59e..5b6bc94ab88ff 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..73c48281276b9 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,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/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