tor-commits
  Threads by month 
                
            - ----- 2025 -----
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
April 2021
- 16 participants
- 1357 discussions
 
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 30541: Disable WebGL readPixel() for web content
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit d40b03d3c3b234e927b69915b730a63b641309be
Author: Georg Koppen <gk(a)torproject.org>
Date:   Wed May 29 12:29:19 2019 +0000
    Bug 30541: Disable WebGL readPixel() for web content
---
 dom/canvas/ClientWebGLContext.cpp | 8 ++++++++
 1 file changed, 8 insertions(+)
diff --git a/dom/canvas/ClientWebGLContext.cpp b/dom/canvas/ClientWebGLContext.cpp
index b940b69fd7bc..d3fe79beec38 100644
--- a/dom/canvas/ClientWebGLContext.cpp
+++ b/dom/canvas/ClientWebGLContext.cpp
@@ -4619,6 +4619,14 @@ bool ClientWebGLContext::ReadPixels_SharedPrecheck(
     return false;
   }
 
+  // Security check passed, but don't let content readPixel calls through for
+  // now, if Resist Fingerprinting Mode is enabled.
+  if (nsContentUtils::ResistFingerprinting(aCallerType)) {
+    JsWarning("readPixels: Not allowed in Resist Fingerprinting Mode");
+    out_error.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return false;
+  }
+
   return true;
 }
 
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 23104: Add a default line height compensation
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit d49bafb748de41431f6e8d26ae3117202e2fa9dd
Author: Igor Oliveira <igor.oliveira(a)posteo.net>
Date:   Sun Dec 10 18:16:59 2017 -0200
    Bug 23104: Add a default line height compensation
    
    Many fonts have issues with their vertical metrics. they
    are used to influence the height of ascenders and depth
    of descenders. Gecko uses it to calculate the line height
    (font height + ascender + descender), however because of
    that idiosyncratic behavior across multiple operating
    systems, it can be used to identify the user's OS.
    
    The solution proposed in the patch uses a default factor
    to be multiplied with the font size, simulating the concept
    of ascender and descender. This way all operating
    systems will have the same line height only and only if the
    frame is outside the chrome.
---
 layout/generic/ReflowInput.cpp             | 19 +++++++++---
 layout/generic/test/mochitest.ini          |  1 +
 layout/generic/test/test_tor_bug23104.html | 50 ++++++++++++++++++++++++++++++
 3 files changed, 65 insertions(+), 5 deletions(-)
diff --git a/layout/generic/ReflowInput.cpp b/layout/generic/ReflowInput.cpp
index 4ab0a5d0ecb7..61d303e95438 100644
--- a/layout/generic/ReflowInput.cpp
+++ b/layout/generic/ReflowInput.cpp
@@ -31,6 +31,7 @@
 #include "mozilla/SVGUtils.h"
 #include "mozilla/dom/HTMLInputElement.h"
 #include "nsGridContainerFrame.h"
+#include "nsContentUtils.h"
 
 using namespace mozilla;
 using namespace mozilla::css;
@@ -2651,7 +2652,8 @@ void ReflowInput::CalculateBlockSideMargins() {
 
 // For risk management, we use preference to control the behavior, and
 // eNoExternalLeading is the old behavior.
-static nscoord GetNormalLineHeight(nsFontMetrics* aFontMetrics) {
+static nscoord GetNormalLineHeight(nsIContent* aContent,
+                                   nsFontMetrics* aFontMetrics) {
   MOZ_ASSERT(nullptr != aFontMetrics, "no font metrics");
 
   nscoord normalLineHeight;
@@ -2659,6 +2661,12 @@ static nscoord GetNormalLineHeight(nsFontMetrics* aFontMetrics) {
   nscoord externalLeading = aFontMetrics->ExternalLeading();
   nscoord internalLeading = aFontMetrics->InternalLeading();
   nscoord emHeight = aFontMetrics->EmHeight();
+
+  if (nsContentUtils::ShouldResistFingerprinting() &&
+      !aContent->IsInChromeDocument()) {
+    return NSToCoordRound(emHeight * NORMAL_LINE_HEIGHT_FACTOR);
+  }
+
   switch (GetNormalLineHeightCalcControl()) {
     case eIncludeExternalLeading:
       normalLineHeight = emHeight + internalLeading + externalLeading;
@@ -2676,7 +2684,8 @@ static nscoord GetNormalLineHeight(nsFontMetrics* aFontMetrics) {
   return normalLineHeight;
 }
 
-static inline nscoord ComputeLineHeight(ComputedStyle* aComputedStyle,
+static inline nscoord ComputeLineHeight(nsIContent* aContent,
+                                        ComputedStyle* aComputedStyle,
                                         nsPresContext* aPresContext,
                                         nscoord aBlockBSize,
                                         float aFontSizeInflation) {
@@ -2705,7 +2714,7 @@ static inline nscoord ComputeLineHeight(ComputedStyle* aComputedStyle,
 
   RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsForComputedStyle(
       aComputedStyle, aPresContext, aFontSizeInflation);
-  return GetNormalLineHeight(fm);
+  return GetNormalLineHeight(aContent, fm);
 }
 
 nscoord ReflowInput::CalcLineHeight() const {
@@ -2727,7 +2736,7 @@ nscoord ReflowInput::CalcLineHeight(nsIContent* aContent,
                                     float aFontSizeInflation) {
   MOZ_ASSERT(aComputedStyle, "Must have a ComputedStyle");
 
-  nscoord lineHeight = ComputeLineHeight(aComputedStyle, aPresContext,
+  nscoord lineHeight = ComputeLineHeight(aContent, aComputedStyle, aPresContext,
                                          aBlockBSize, aFontSizeInflation);
 
   NS_ASSERTION(lineHeight >= 0, "ComputeLineHeight screwed up");
@@ -2740,7 +2749,7 @@ nscoord ReflowInput::CalcLineHeight(nsIContent* aContent,
     if (!lh.IsNormal()) {
       RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsForComputedStyle(
           aComputedStyle, aPresContext, aFontSizeInflation);
-      nscoord normal = GetNormalLineHeight(fm);
+      nscoord normal = GetNormalLineHeight(aContent, fm);
       if (lineHeight < normal) {
         lineHeight = normal;
       }
diff --git a/layout/generic/test/mochitest.ini b/layout/generic/test/mochitest.ini
index 072b11da7eb6..3c377e0781ed 100644
--- a/layout/generic/test/mochitest.ini
+++ b/layout/generic/test/mochitest.ini
@@ -145,3 +145,4 @@ skip-if = debug == true || tsan # the test is slow. tsan: bug 1612707
 support-files =
     file_reframe_for_lazy_load_image.html
 [test_bug1655135.html]
+[test_tor_bug23104.html]
diff --git a/layout/generic/test/test_tor_bug23104.html b/layout/generic/test/test_tor_bug23104.html
new file mode 100644
index 000000000000..8ff1d2190c45
--- /dev/null
+++ b/layout/generic/test/test_tor_bug23104.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<meta charset="UTF-8">
+<html>
+<head>
+  <title>Test for Tor Bug #23104: CSS line-height reveals the platform Tor browser is running</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <style type="text/css">
+    span {
+      background-color: #000;
+      color: #fff;
+      font-size: 16.5px;
+    }
+  </style>
+</head>
+<body>
+<span id="test1">Test1</span>
+<span id="test2">كلمة</span>
+<span id="test3">ação</span>
+<script>
+
+let setPref = async function (key, value) {
+  await SpecialPowers.pushPrefEnv({"set": [[key, value]]});
+}
+
+function getStyle(el, styleprop) {
+  el = document.getElementById(el);
+  return document.defaultView.getComputedStyle(el, null).getPropertyValue(styleprop);
+}
+
+function validateElement(elementName, isFingerprintResistent) {
+  var fontSize = getStyle(elementName, 'font-size');
+  var lineHeight = getStyle(elementName, 'line-height');
+  var validationCb = isFingerprintResistent ? is : isnot;
+  validationCb(parseFloat(lineHeight), Math.round(parseFloat(fontSize)) * 1.2, 'Line Height validation');
+}
+
+add_task(async function() {
+  await setPref("layout.css.line-height.normal-as-resolved-value.enabled", false);
+  for (let resistFingerprintingValue of [true, false]) {
+    await setPref("privacy.resistFingerprinting", resistFingerprintingValue);
+    for (let elementId of ['test1', 'test2', 'test3']) {
+      validateElement(elementId, resistFingerprintingValue);
+    }
+  }
+});
+
+</script>
+</body>
+</html>
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 28369: Stop shipping pingsender executable
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit 881f2f68658764411c49019956d05e6430376e35
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Wed Apr 10 17:52:51 2019 +0200
    Bug 28369: Stop shipping pingsender executable
---
 browser/app/macbuild/Contents/MacOS-files.in       |  1 -
 browser/installer/package-manifest.in              |  4 ----
 browser/installer/windows/nsis/shared.nsh          |  1 -
 python/mozbuild/mozbuild/artifacts.py              |  2 --
 toolkit/components/telemetry/app/TelemetrySend.jsm | 19 +------------------
 toolkit/components/telemetry/moz.build             |  4 ----
 6 files changed, 1 insertion(+), 30 deletions(-)
diff --git a/browser/app/macbuild/Contents/MacOS-files.in b/browser/app/macbuild/Contents/MacOS-files.in
index 6f0b4481473b..6e8a1689ea19 100644
--- a/browser/app/macbuild/Contents/MacOS-files.in
+++ b/browser/app/macbuild/Contents/MacOS-files.in
@@ -17,7 +17,6 @@
 #if defined(MOZ_CRASHREPORTER)
 /minidump-analyzer
 #endif
-/pingsender
 /pk12util
 /ssltunnel
 /xpcshell
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
index 27424c5d7fcd..0754508f8693 100644
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -452,10 +452,6 @@ bin/libfreebl_64int_3.so
 @BINPATH@/minidump-analyzer@BIN_SUFFIX@
 #endif
 
-; [ Ping Sender ]
-;
-@BINPATH@/pingsender@BIN_SUFFIX@
-
 ; Shutdown Terminator
 @RESPATH@/components/terminator.manifest
 
diff --git a/browser/installer/windows/nsis/shared.nsh b/browser/installer/windows/nsis/shared.nsh
index 74e1c0c7141e..c5181b8d7ae6 100755
--- a/browser/installer/windows/nsis/shared.nsh
+++ b/browser/installer/windows/nsis/shared.nsh
@@ -1475,7 +1475,6 @@ ${RemoveDefaultBrowserAgentShortcut}
   Push "crashreporter.exe"
   Push "default-browser-agent.exe"
   Push "minidump-analyzer.exe"
-  Push "pingsender.exe"
   Push "updater.exe"
   Push "${FileMainEXE}"
 !macroend
diff --git a/python/mozbuild/mozbuild/artifacts.py b/python/mozbuild/mozbuild/artifacts.py
index 50ca81a4aaf5..3f3454b4b7b5 100644
--- a/python/mozbuild/mozbuild/artifacts.py
+++ b/python/mozbuild/mozbuild/artifacts.py
@@ -495,7 +495,6 @@ class LinuxArtifactJob(ArtifactJob):
         "{product}/{product}",
         "{product}/{product}-bin",
         "{product}/minidump-analyzer",
-        "{product}/pingsender",
         "{product}/plugin-container",
         "{product}/updater",
         "{product}/**/*.so",
@@ -550,7 +549,6 @@ class MacArtifactJob(ArtifactJob):
             "{product}-bin",
             "*.dylib",
             "minidump-analyzer",
-            "pingsender",
             "plugin-container.app/Contents/MacOS/plugin-container",
             "updater.app/Contents/MacOS/org.mozilla.updater",
             # 'xpcshell',
diff --git a/toolkit/components/telemetry/app/TelemetrySend.jsm b/toolkit/components/telemetry/app/TelemetrySend.jsm
index d64da6858124..ba125ea6a459 100644
--- a/toolkit/components/telemetry/app/TelemetrySend.jsm
+++ b/toolkit/components/telemetry/app/TelemetrySend.jsm
@@ -1588,23 +1588,6 @@ var TelemetrySendImpl = {
   },
 
   runPingSender(pings, observer) {
-    if (AppConstants.platform === "android") {
-      throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
-    }
-
-    const exeName =
-      AppConstants.platform === "win" ? "pingsender.exe" : "pingsender";
-
-    let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
-    exe.append(exeName);
-
-    let params = pings.flatMap(ping => [ping.url, ping.path]);
-    let process = Cc["@mozilla.org/process/util;1"].createInstance(
-      Ci.nsIProcess
-    );
-    process.init(exe);
-    process.startHidden = true;
-    process.noShell = true;
-    process.runAsync(params, params.length, observer);
+    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
   },
 };
diff --git a/toolkit/components/telemetry/moz.build b/toolkit/components/telemetry/moz.build
index 32e670b76b18..21fe4e8c71eb 100644
--- a/toolkit/components/telemetry/moz.build
+++ b/toolkit/components/telemetry/moz.build
@@ -8,10 +8,6 @@ include("/ipc/chromium/chromium-config.mozbuild")
 
 FINAL_LIBRARY = "xul"
 
-DIRS = [
-    "pingsender",
-]
-
 DEFINES["MOZ_APP_VERSION"] = '"%s"' % CONFIG["MOZ_APP_VERSION"]
 
 LOCAL_INCLUDES += [
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 26353: Prevent speculative connect that violated FPI.
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit 261421defae697df00d44114b4e6a14f19bf31e5
Author: Arthur Edelstein <arthuredelstein(a)gmail.com>
Date:   Sat Jul 14 08:50:55 2018 -0700
    Bug 26353: Prevent speculative connect that violated FPI.
    
    Connections were observed in the catch-all circuit when
    the user entered an https or http URL in the URL bar, or
    typed a search term.
---
 toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm | 4 ++++
 1 file changed, 4 insertions(+)
diff --git a/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm b/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm
index 568e70688dc4..e1adbc72bdad 100644
--- a/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm
+++ b/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm
@@ -93,6 +93,9 @@ class RemoteWebNavigation {
       }
 
       uri = Services.uriFixup.getFixupURIInfo(aURI, fixupFlags).preferredURI;
+/*******************************************************************************
+   TOR BROWSER: Disable the following speculative connect until
+   we can make it properly obey first-party isolation.
 
       // We know the url is going to be loaded, let's start requesting network
       // connection before the content process asks.
@@ -116,6 +119,7 @@ class RemoteWebNavigation {
         }
         Services.io.speculativeConnect(uri, principal, null);
       }
+*******************************************************************************/
     } catch (ex) {
       // Can't setup speculative connection for this uri string for some
       // reason (such as failing to parse the URI), just ignore it.
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 26345: Hide tracking protection UI
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit b84225de8725f06123b2a1d7ce643ba408598ebb
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Tue Sep 10 16:29:31 2019 +0200
    Bug 26345: Hide tracking protection UI
---
 browser/base/content/browser-siteIdentity.js  | 4 ++--
 browser/base/content/browser.xhtml            | 4 ++--
 browser/components/about/AboutRedirector.cpp  | 4 ----
 browser/components/about/components.conf      | 1 -
 browser/components/moz.build                  | 1 -
 browser/themes/shared/preferences/privacy.css | 4 ++++
 6 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
index 370e2af9477b..4c11ea8d18f0 100644
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -911,10 +911,10 @@ var gIdentityHandler = {
       gPermissionPanel.refreshPermissionIcons();
     }
 
-    // Hide the shield icon if it is a chrome page.
+    // Bug 26345: Hide tracking protection UI.
     gProtectionsHandler._trackingProtectionIconContainer.classList.toggle(
       "chromeUI",
-      this._isSecureInternalUI
+      true
     );
   },
 
diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml
index f8381a5dad9e..c859d65af413 100644
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -782,7 +782,7 @@
                         oncommand="gSync.toggleAccountPanel(this, event)"/>
         </toolbaritem>
         <toolbarseparator class="sync-ui-item"/>
-        <toolbaritem>
+        <toolbaritem hidden="true">
           <toolbarbutton id="appMenu-protection-report-button"
                class="subviewbutton subviewbutton-iconic"
                oncommand="gProtectionsHandler.openProtections(); gProtectionsHandler.recordClick('open_full_report', null, 'app_menu');">
@@ -793,7 +793,7 @@
             </label>
           </toolbarbutton>
         </toolbaritem>
-        <toolbarseparator id="appMenu-tp-separator"/>
+        <toolbarseparator hidden="true" id="appMenu-tp-separator"/>
         <toolbarbutton id="appMenu-new-window-button"
                        class="subviewbutton subviewbutton-iconic"
                        label="&newNavigatorCmd.label;"
diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp
index 85bd42d67263..179b46804a0b 100644
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -122,10 +122,6 @@ static const RedirEntry kRedirMap[] = {
      nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
          nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
          nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::HIDE_FROM_ABOUTABOUT},
-    {"protections", "chrome://browser/content/protections.html",
-     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
-         nsIAboutModule::URI_MUST_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT |
-         nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS},
     {"ion", "chrome://browser/content/ion.html",
      nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::HIDE_FROM_ABOUTABOUT},
 #ifdef TOR_BROWSER_UPDATE
diff --git a/browser/components/about/components.conf b/browser/components/about/components.conf
index b6d013df3fbc..3729b7e4bf82 100644
--- a/browser/components/about/components.conf
+++ b/browser/components/about/components.conf
@@ -20,7 +20,6 @@ pages = [
     'policies',
     'preferences',
     'privatebrowsing',
-    'protections',
     'profiling',
     'reader',
     'restartrequired',
diff --git a/browser/components/moz.build b/browser/components/moz.build
index 1c421b761888..ef09055b990a 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -46,7 +46,6 @@ DIRS += [
     "preferences",
     "privatebrowsing",
     "prompts",
-    "protections",
     "protocolhandler",
     "resistfingerprinting",
     "search",
diff --git a/browser/themes/shared/preferences/privacy.css b/browser/themes/shared/preferences/privacy.css
index a0cedd7943c6..305692fe0f8a 100644
--- a/browser/themes/shared/preferences/privacy.css
+++ b/browser/themes/shared/preferences/privacy.css
@@ -114,6 +114,10 @@
 
 /* Content Blocking */
 
+#trackingGroup {
+  display: none;
+}
+
 /* Override styling that sets descriptions as grey */
 #trackingGroup description.indent,
 #trackingGroup .indent > description {
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 23247: Communicating security expectations for .onion
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit ae8803aa4e0b9cba6a56c88d0021befe19517c14
Author: Richard Pospesel <richard(a)torproject.org>
Date:   Fri Jun 8 13:38:40 2018 -0700
    Bug 23247: Communicating security expectations for .onion
    
    Encrypting pages hosted on Onion Services with SSL/TLS is redundant
    (in terms of hiding content) as all traffic within the Tor network is
    already fully encrypted.  Therefore, serving HTTP pages from an Onion
    Service is more or less fine.
    
    Prior to this patch, Tor Browser would mostly treat pages delivered
    via Onion Services as well as pages delivered in the ordinary fashion
    over the internet in the same way.  This created some inconsistencies
    in behaviour and misinformation presented to the user relating to the
    security of pages delivered via Onion Services:
    
     - HTTP Onion Service pages did not have any 'lock' icon indicating
       the site was secure
     - HTTP Onion Service pages would be marked as unencrypted in the Page
       Info screen
     - Mixed-mode content restrictions did not apply to HTTP Onion Service
       pages embedding Non-Onion HTTP content
    
    This patch fixes the above issues, and also adds several new 'Onion'
    icons to the mix to indicate all of the various permutations of Onion
    Services hosted HTTP or HTTPS pages with HTTP or HTTPS content.
    
    Strings for Onion Service Page Info page are pulled from Torbutton's
    localization strings.
---
 browser/base/content/browser-siteIdentity.js       | 39 ++++++++-----
 browser/base/content/pageinfo/security.js          | 64 ++++++++++++++++++----
 .../shared/identity-block/identity-block.inc.css   | 19 +++++++
 .../themes/shared/identity-block/onion-slash.svg   |  5 ++
 .../themes/shared/identity-block/onion-warning.svg |  6 ++
 browser/themes/shared/identity-block/onion.svg     |  3 +
 browser/themes/shared/jar.inc.mn                   |  3 +
 dom/base/nsContentUtils.cpp                        | 19 +++++++
 dom/base/nsContentUtils.h                          |  5 ++
 dom/base/nsGlobalWindowOuter.cpp                   |  3 +-
 dom/ipc/WindowGlobalActor.cpp                      |  5 +-
 dom/ipc/WindowGlobalChild.cpp                      |  6 +-
 dom/security/nsMixedContentBlocker.cpp             | 16 +++++-
 .../modules/geckoview/GeckoViewProgress.jsm        |  4 ++
 security/manager/ssl/nsSecureBrowserUI.cpp         | 12 ++++
 15 files changed, 178 insertions(+), 31 deletions(-)
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
index c17d6c76394b..370e2af9477b 100644
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -140,6 +140,10 @@ var gIdentityHandler = {
     );
   },
 
+  get _uriIsOnionHost() {
+    return this._uriHasHost ? this._uri.host.toLowerCase().endsWith(".onion") : false;
+  },
+
   get _isAboutNetErrorPage() {
     return (
       gBrowser.selectedBrowser.documentURI &&
@@ -735,9 +739,9 @@ var gIdentityHandler = {
   get pointerlockFsWarningClassName() {
     // Note that the fullscreen warning does not handle _isSecureInternalUI.
     if (this._uriHasHost && this._isSecureConnection) {
-      return "verifiedDomain";
+      return this._uriIsOnionHost ? "onionVerifiedDomain" : "verifiedDomain";
     }
-    return "unknownIdentity";
+    return this._uriIsOnionHost ? "onionUnknownIdentity" : "unknownIdentity";
   },
 
   /**
@@ -745,6 +749,10 @@ var gIdentityHandler = {
    * built-in (returns false) or imported (returns true).
    */
   _hasCustomRoot() {
+    if (!this._secInfo) {
+      return false;
+    }
+
     let issuerCert = null;
     issuerCert = this._secInfo.succeededCertChain[
       this._secInfo.succeededCertChain.length - 1
@@ -787,11 +795,13 @@ var gIdentityHandler = {
         "identity.extension.label",
         [extensionName]
       );
-    } else if (this._uriHasHost && this._isSecureConnection) {
+    } else if (this._uriHasHost && this._isSecureConnection && this._secInfo) {
       // This is a secure connection.
-      this._identityBox.className = "verifiedDomain";
+      // _isSecureConnection implicitly includes onion services, which may not have an SSL certificate
+      const uriIsOnionHost = this._uriIsOnionHost;
+      this._identityBox.className = uriIsOnionHost ? "onionVerifiedDomain" : "verifiedDomain";
       if (this._isMixedActiveContentBlocked) {
-        this._identityBox.classList.add("mixedActiveBlocked");
+        this._identityBox.classList.add(uriIsOnionHost ? "onionMixedActiveBlocked" : "mixedActiveBlocked");
       }
       if (!this._isCertUserOverridden) {
         // It's a normal cert, verifier is the CA Org.
@@ -802,17 +812,17 @@ var gIdentityHandler = {
       }
     } else if (this._isBrokenConnection) {
       // This is a secure connection, but something is wrong.
-      this._identityBox.className = "unknownIdentity";
+      const uriIsOnionHost = this._uriIsOnionHost;
+      this._identityBox.className = uriIsOnionHost ? "onionUnknownIdentity" : "unknownIdentity";
 
       if (this._isMixedActiveContentLoaded) {
-        this._identityBox.classList.add("mixedActiveContent");
+        this._identityBox.classList.add(uriIsOnionHost ? "onionMixedActiveContent" : "mixedActiveContent");
       } else if (this._isMixedActiveContentBlocked) {
-        this._identityBox.classList.add(
-          "mixedDisplayContentLoadedActiveBlocked"
-        );
+        this._identityBox.classList.add(uriIsOnionHost ? "onionMixedDisplayContentLoadedActiveBlocked" : "mixedDisplayContentLoadedActiveBlocked");
       } else if (this._isMixedPassiveContentLoaded) {
-        this._identityBox.classList.add("mixedDisplayContent");
+        this._identityBox.classList.add(uriIsOnionHost ? "onionMixedDisplayContent" : "mixedDisplayContent");
       } else {
+        // TODO: ignore weak https cipher for onionsites?
         this._identityBox.classList.add("weakCipher");
       }
     } else if (this._isAboutCertErrorPage) {
@@ -825,8 +835,8 @@ var gIdentityHandler = {
       // Network errors and blocked pages get a more neutral icon
       this._identityBox.className = "unknownIdentity";
     } else if (this._isPotentiallyTrustworthy) {
-      // This is a local resource (and shouldn't be marked insecure).
-      this._identityBox.className = "localResource";
+      // This is a local resource or an onion site (and shouldn't be marked insecure).
+      this._identityBox.className = this._uriIsOnionHost ? "onionUnknownIdentity" : "localResource";
     } else {
       // This is an insecure connection.
       let warnOnInsecure =
@@ -850,7 +860,8 @@ var gIdentityHandler = {
     }
 
     if (this._isCertUserOverridden) {
-      this._identityBox.classList.add("certUserOverridden");
+      const uriIsOnionHost = this._uriIsOnionHost;
+      this._identityBox.classList.add(uriIsOnionHost ? "onionCertUserOverridden" : "certUserOverridden");
       // Cert is trusted because of a security exception, verifier is a special string.
       tooltip = gNavigatorBundle.getString(
         "identity.identified.verified_by_you"
diff --git a/browser/base/content/pageinfo/security.js b/browser/base/content/pageinfo/security.js
index 6a2d09ec8442..192e9f763700 100644
--- a/browser/base/content/pageinfo/security.js
+++ b/browser/base/content/pageinfo/security.js
@@ -22,6 +22,13 @@ ChromeUtils.defineModuleGetter(
   "PluralForm",
   "resource://gre/modules/PluralForm.jsm"
 );
+XPCOMUtils.defineLazyGetter(
+  this,
+  "gTorButtonBundle",
+  function() {
+    return Services.strings.createBundle("chrome://torbutton/locale/torbutton.properties");
+  }
+);
 
 var security = {
   async init(uri, windowInfo) {
@@ -60,6 +67,11 @@ var security = {
       (Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT |
         Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
     var isEV = ui.state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
+    var isOnion = false;
+    const hostName = this.windowInfo.hostName;
+    if (hostName && hostName.endsWith(".onion")) {
+      isOnion = true;
+    }
 
     let retval = {
       cAName: "",
@@ -69,6 +81,7 @@ var security = {
       isBroken,
       isMixed,
       isEV,
+      isOnion,
       cert: null,
       certificateTransparency: null,
     };
@@ -107,6 +120,7 @@ var security = {
       isBroken,
       isMixed,
       isEV,
+      isOnion,
       cert,
       certChain: certChainArray,
       certificateTransparency: undefined,
@@ -349,22 +363,50 @@ async function securityOnLoad(uri, windowInfo) {
     }
     msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
   } else if (info.encryptionStrength > 0) {
-    hdr = pkiBundle.getFormattedString(
-      "pageInfo_EncryptionWithBitsAndProtocol",
-      [info.encryptionAlgorithm, info.encryptionStrength + "", info.version]
-    );
+    if (!info.isOnion) {
+      hdr = pkiBundle.getFormattedString(
+        "pageInfo_EncryptionWithBitsAndProtocol",
+        [info.encryptionAlgorithm, info.encryptionStrength + "", info.version]
+      );
+    } else {
+      try {
+        hdr = gTorButtonBundle.formatStringFromName(
+          "pageInfo_OnionEncryptionWithBitsAndProtocol",
+          [info.encryptionAlgorithm, info.encryptionStrength + "", info.version]
+        );
+      } catch(err) {
+        hdr = "Connection Encrypted (Onion Service, "
+               + info.encryptionAlgorithm
+               + ", "
+               + info.encryptionStrength
+               + " bit keys, "
+               + info.version
+               + ")";
+      }
+    }
     msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1");
     msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2");
   } else {
-    hdr = pkiBundle.getString("pageInfo_NoEncryption");
-    if (windowInfo.hostName != null) {
-      msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [
-        windowInfo.hostName,
-      ]);
+    if (!info.isOnion) {
+      hdr = pkiBundle.getString("pageInfo_NoEncryption");
+      if (windowInfo.hostName != null) {
+        msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [
+          windowInfo.hostName,
+        ]);
+      } else {
+        msg1 = pkiBundle.getString("pageInfo_Privacy_None4");
+      }
+      msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
     } else {
-      msg1 = pkiBundle.getString("pageInfo_Privacy_None4");
+      try {
+        hdr = gTorButtonBundle.GetStringFromName("pageInfo_OnionEncryption");
+      } catch (err) {
+        hdr = "Connection Encrypted (Onion Service)";
+      }
+
+      msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1");
+      msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2");
     }
-    msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
   }
   setText("security-technical-shortform", hdr);
   setText("security-technical-longform1", msg1);
diff --git a/browser/themes/shared/identity-block/identity-block.inc.css b/browser/themes/shared/identity-block/identity-block.inc.css
index 23b66d8a7747..8b5a9fa8ae82 100644
--- a/browser/themes/shared/identity-block/identity-block.inc.css
+++ b/browser/themes/shared/identity-block/identity-block.inc.css
@@ -284,6 +284,25 @@ toolbar[brighttext] #identity-box[pageproxystate="valid"].chromeUI #identity-ico
   list-style-image: url(chrome://global/skin/icons/security-broken.svg);
 }
 
+#identity-box[pageproxystate="valid"].onionUnknownIdentity > #identity-icon,
+#identity-box[pageproxystate="valid"].onionVerifiedDomain > #identity-icon,
+#identity-box[pageproxystate="valid"].onionMixedActiveBlocked > #identity-icon {
+  list-style-image: url(chrome://browser/skin/onion.svg);
+  visibility: visible;
+}
+
+#identity-box[pageproxystate="valid"].onionMixedDisplayContent > #identity-icon,
+#identity-box[pageproxystate="valid"].onionMixedDisplayContentLoadedActiveBlocked > #identity-icon,
+#identity-box[pageproxystate="valid"].onionCertUserOverridden > #identity-icon {
+  list-style-image: url(chrome://browser/skin/onion-warning.svg);
+  visibility: visible;
+}
+
+#identity-box[pageproxystate="valid"].onionMixedActiveContent > #identity-icon {
+  list-style-image: url(chrome://browser/skin/onion-slash.svg);
+  visibility: visible;
+}
+
 #permissions-granted-icon {
   list-style-image: url(chrome://browser/skin/permissions.svg);
 }
diff --git a/browser/themes/shared/identity-block/onion-slash.svg b/browser/themes/shared/identity-block/onion-slash.svg
new file mode 100644
index 000000000000..e7c98b769482
--- /dev/null
+++ b/browser/themes/shared/identity-block/onion-slash.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path d="M3.409559 13.112147C3.409559 13.112147 8.200807 8.103115 8.200807 8.103115C8.200807 8.103115 8.200807 6.516403 8.200807 6.516403C8.620819 6.516403 9.009719 6.703075 9.274171 6.998639C9.274171 6.998639 10.160863 6.080835 10.160863 6.080835C9.663071 5.567487 8.978607 5.256367 8.200807 5.256367C8.200807 5.256367 8.200807 4.400787 8.200807 4.400787C9.196391 4.400787 10.098639 4.805243 10.736435 5.458595C10.736435 5.458595 11.623127 4.540791 11.623127 4.540791C10.751991 3.669655 9.538623 3.125195 8.200807 3.125195C8.200807 3.125195 8.200807 2.269615 8.200807 2.269615C9.756407 2.269615 11.172003 2.907411 12.214255 3.918551C12.214255 3.918551 13.100947 3.000747 13.100947 3.000747C11.825355 1.756267 10.098639 0.994023 8.185251 0.994023C4.311807 0.994023 1.185051 4.120779 1.185051 7.994223C1.185051 10.016503 2.040631 11.836555 3.409559 13.112147C3.409559 13.112147 3.409559 13.112147 3.409559 13.112147" fill-opacity="context-fill-opacity" fill="context-fill" />
+  <path d="M14.205423 4.416343C14.205423 4.416343 13.287619 5.380815 13.287619 5.380815C13.692075 6.158615 13.909859 7.045307 13.909859 7.994223C13.909859 11.152091 11.358675 13.718831 8.200807 13.718831C8.200807 13.718831 8.200807 12.863251 8.200807 12.863251C10.891995 12.863251 13.069835 10.669855 13.069835 7.978667C13.069835 7.278647 12.929831 6.625295 12.665379 6.018611C12.665379 6.018611 11.685351 7.045307 11.685351 7.045307C11.763131 7.340871 11.809799 7.651991 11.809799 7.963111C11.809799 9.954279 10.207531 11.556547 8.216363 11.572103C8.216363 11.572103 8.216363 10.716523 8.216363 10.716523C9.725295 10.700967 10.954219 9.472043 10.954219 7.963111C10.954219 7.916443 10.954219 7.854219 10.954219 7.807551C10.954219 7.807551 4.887379 14.169955 4.887379 14.169955C5.867407 14.698859 6.987439 14.994423 8.185251 14.994423C12.058695 14.994423 15.185451 11.867667 15.185451 7.994223C15.185451 6.687519 14.827663 5.474151 14.205423 4.416343C14.205423 4.416343 14.205423 4.416343 14.205423
  4.416343" fill-opacity="context-fill-opacity" fill="context-fill" />
+  <path d="M1.791735 15.461103C1.402835 15.461103 1.045047 15.212207 0.889487 14.838863C0.733927 14.465519 0.827267 14.014395 1.107271 13.734387C1.107271 13.734387 13.458735 0.822907 13.458735 0.822907C13.847635 0.434007 14.454319 0.449563 14.827663 0.838467C15.201007 1.227367 15.216563 1.865163 14.843223 2.269619C14.843223 2.269619 2.491759 15.181099 2.491759 15.181099C2.289531 15.352215 2.040635 15.461107 1.791739 15.461107C1.791739 15.461107 1.791735 15.461103 1.791735 15.461103" fill="#ff0039" />
+</svg>
diff --git a/browser/themes/shared/identity-block/onion-warning.svg b/browser/themes/shared/identity-block/onion-warning.svg
new file mode 100644
index 000000000000..d42a7dab7246
--- /dev/null
+++ b/browser/themes/shared/identity-block/onion-warning.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path d="M15.8630401732 14.127C15.8630401732 14.127 12.6649598146 7.716 12.6649598146 7.716C12.4469357756 7.279935 12.0003277145 7.0043454 11.5116853046 7.0043454C11.0230428947 7.0043454 10.5764348336 7.279935 10.3584107946 7.716C10.3584107946 7.716 7.1573218938 14.127 7.1573218938 14.127C6.95646770542 14.527294 6.97733695982 15.002669 7.21250176686 15.38393C7.4476665739 15.765191 7.86372750208 15.998191 8.3126020986 16.0C8.3126020986 16.0 14.7077599684 16.0 14.7077599684 16.0C15.1566344646 15.9982 15.572695794 15.765191 15.8078605007 15.38393C16.0430252075 15.002669 16.0638944619 14.527294 15.8630371647 14.127C15.8630371647 14.127 15.8630401732 14.127 15.8630401732 14.127" fill="#ffbf00" />
+  <path d="M11.5106824572 8.0C11.6210488221 7.99691 11.7223975832 8.060469 11.7674113916 8.161C11.7674113916 8.161 14.9644889028 14.573 14.9644889028 14.573C15.0126456349 14.66534 15.0076715118 14.776305 14.9514518866 14.864C14.9011992034 14.95041 14.8079143382 15.002854 14.7077599684 15.001048C14.7077599684 15.001048 8.3126020986 15.001048 8.3126020986 15.001048C8.2124480296 15.002854 8.1191607576 14.950409 8.0689101804 14.864C8.0124814615 14.77637 8.0075053327 14.665298 8.0558731642 14.573C8.0558731642 14.573 11.2529506754 8.161 11.2529506754 8.161C11.2981038796 8.0601247 11.3999560701 7.9964997 11.5106824572 8.0M11.5106824572 6.9999751C11.0194557096 6.9969427 10.5701148893 7.2754275 10.3554022524 7.716C10.3554022524 7.716 7.1573218938 14.127 7.1573218938 14.127C6.95646770542 14.527294 6.97733695982 15.002669 7.21250176686 15.38393C7.4476665739 15.765191 7.86372750208 15.998191 8.3126020986 16.0C8.3126020986 16.0 14.7077599684 16.0 14.7077599684 16.0C15.1566344646 15.9982 15.57269
 5794 15.765191 15.8078605007 15.38393C16.0430252075 15.002669 16.0638944619 14.527294 15.8630371647 14.127C15.8630371647 14.127 12.6649598146 7.716 12.6649598146 7.716C12.4504036219 7.2757546 12.0015481798 6.9973287 11.5106824572 6.9999751C11.5106824572 6.9999751 11.5106824572 6.9999751 11.5106824572 6.9999751" opacity="0.35" fill="#d76e00" />
+  <path d="M11.5327451 12.0C11.8096733867 12.0 12.0341688 11.776142 12.0341688 11.5C12.0341688 11.5 12.0341688 9.5 12.0341688 9.5C12.0341688 9.2238576 11.8096733867 9.0 11.5327451 9.0C11.2558168133 9.0 11.0313214 9.2238576 11.0313214 9.5C11.0313214 9.5 11.0313214 11.5 11.0313214 11.5C11.0313214 11.776142 11.2558168133 12.0 11.5327451 12.0C11.5327451 12.0 11.5327451 12.0 11.5327451 12.0M11.5327451 12.809C11.1500294496 12.809 10.8397775466 13.118371 10.8397775466 13.5C10.8397775466 13.881629 11.1500294496 14.191 11.5327451 14.191C11.9154607504 14.191 12.2257126534 13.881629 12.2257126534 13.5C12.2257126534 13.118371 11.9154607504 12.809 11.5327451 12.809C11.5327451 12.809 11.5327451 12.809 11.5327451 12.809" fill="#ffffff" />
+  <path d="M7.08030321348 6.552C7.90163523408 6.56 8.5645173655 7.225 8.5645173655 8.046C8.5645173655 8.866 7.90163523408 9.532 7.08030321348 9.54C7.08030321348 9.54 7.08030321348 6.552 7.08030321348 6.552M6.30610502068 13.756C6.30610502068 13.756 9.4991711423 7.353 9.4991711423 7.353C9.5453021227 7.259 9.6144985933 7.184 9.6716608951 7.098C9.2845617987 6.039 8.2756973143 5.277 7.08030321348 5.271C7.08030321348 5.271 7.08030321348 4.417 7.08030321348 4.417C8.5043465215 4.423 9.7238089599 5.251 10.3164917733 6.443C10.6795225321 6.21 11.1067355245 6.074 11.5519997701 6.074C11.5519997701 6.074 11.5620282441 6.074 11.5620282441 6.074C11.5620282441 6.074 11.5640339389 6.074 11.5640339389 6.074C11.5660396337 6.074 11.5690481759 6.075 11.5710538707 6.075C10.8108955415 4.35 9.0900094031 3.141 7.08030321348 3.135C7.08030321348 3.135 7.08030321348 2.281 7.08030321348 2.281C9.6716608951 2.288 11.8618796167 3.993 12.5889439817 6.34C13.0231769059 6.561 13.3922247491 6.9 13.6088397875 7.344C13.60
 88397875 7.344 14.1162805719 8.361 14.1162805719 8.361C14.1202919615 8.256 14.1313232829 8.152 14.1313232829 8.046C14.1313232829 4.155 10.9683425833 1.0 7.06626334988 1.0C3.16318126908 1.0 0.00020056948 4.155 0.00020056948 8.046C0.00020056948 11.603 2.64571201068 14.536 6.08046435568 15.015C6.03633907008 14.595 6.10252699848 14.16 6.30610502068 13.756C6.30610502068 13.756 6.30610502068 13.756 6.30610502068 13.756" fill-opacity="context-fill-opacity" fill="context-fill" />
+</svg>
diff --git a/browser/themes/shared/identity-block/onion.svg b/browser/themes/shared/identity-block/onion.svg
new file mode 100644
index 000000000000..b123a9786acc
--- /dev/null
+++ b/browser/themes/shared/identity-block/onion.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path d="M8.01435945 13.726867125C8.01435945 13.726867125 8.01435945 12.87830525 8.01435945 12.87830525C10.70227825 12.87051775 12.87869375 10.689666 12.87869375 7.9998060125C12.87869375 5.310140275 10.70227825 3.1292621 8.01435945 3.121500325C8.01435945 3.121500325 8.01435945 2.272938975 8.01435945 2.272938975C11.170899375 2.280892725 13.727061375 4.8415202875 13.727061375 7.9998060125C13.727061375 11.158285375 11.170899375 13.719105 8.01435945 13.726867125C8.01435945 13.726867125 8.01435945 13.726867125 8.01435945 13.726867125M8.01435945 10.756805625C9.5304373 10.74884925 10.75758175 9.5180185125 10.75758175 7.9998060125C10.75758175 6.4817875 9.5304373 5.2509564125 8.01435945 5.2430005625C8.01435945 5.2430005625 8.01435945 4.3946332875 8.01435945 4.3946332875C9.999251625 4.4023945375 11.60614275 6.013167425 11.60614275 7.9998060125C11.60614275 9.986639375 9.999251625 11.597411125 8.01435945 11.605172375C8.01435945 11.605172375 8.01435945 10.756805625 8.01435945 10.756805625M8.01
 435945 6.5157454625C8.8276046625 6.5235067125 9.484837025 7.184620575 9.484837025 7.9998060125C9.484837025 8.815185875 8.8276046625 9.4762985125 8.01435945 9.4840608125C8.01435945 9.4840608125 8.01435945 6.5157454625 8.01435945 6.5157454625M1.0 7.9998060125C1.0 11.8659705 4.1338360375 15.0 8.0000000875 15.0C11.8659705 15.0 15.0 11.8659705 15.0 7.9998060125C15.0 4.1338360375 11.8659705 1.0 8.0000000875 1.0C4.1338360375 1.0 1.0 4.1338360375 1.0 7.9998060125C1.0 7.9998060125 1.0 7.9998060125 1.0 7.9998060125" fill-rule="even-odd" fill-opacity="context-fill-opacity" fill="context-fill" />
+</svg>
diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn
index 9a62864b3f1f..cbf18156a455 100644
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -52,6 +52,9 @@
   skin/classic/browser/downloads/notification-start-animation.svg  (../shared/downloads/notification-start-animation.svg)
   skin/classic/browser/drm-icon.svg                            (../shared/drm-icon.svg)
   skin/classic/browser/permissions.svg                         (../shared/identity-block/permissions.svg)
+  skin/classic/browser/onion.svg                               (../shared/identity-block/onion.svg)
+  skin/classic/browser/onion-slash.svg                         (../shared/identity-block/onion-slash.svg)
+  skin/classic/browser/onion-warning.svg                       (../shared/identity-block/onion-warning.svg)
   skin/classic/browser/newInstall.css                          (../shared/newInstall.css)
   skin/classic/browser/newInstallPage.css                      (../shared/newInstallPage.css)
   skin/classic/browser/illustrations/blue-berror.svg                        (../shared/illustrations/blue-berror.svg)
diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp
index a5d51c1f6b63..5c328e3753d2 100644
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -9204,6 +9204,25 @@ bool nsContentUtils::ComputeIsSecureContext(nsIChannel* aChannel) {
   return principal->GetIsOriginPotentiallyTrustworthy();
 }
 
+/* static */ bool nsContentUtils::DocumentHasOnionURI(Document* aDocument) {
+  if (!aDocument) {
+    return false;
+  }
+
+  nsIURI* uri = aDocument->GetDocumentURI();
+  if (!uri) {
+    return false;
+  }
+
+  nsAutoCString host;
+  if (NS_SUCCEEDED(uri->GetHost(host))) {
+    bool hasOnionURI = StringEndsWith(host, ".onion"_ns);
+    return hasOnionURI;
+  }
+
+  return false;
+}
+
 /* static */
 void nsContentUtils::TryToUpgradeElement(Element* aElement) {
   NodeInfo* nodeInfo = aElement->NodeInfo();
diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h
index 4fe89763c102..9fc33d9780a2 100644
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -2979,6 +2979,11 @@ class nsContentUtils {
    */
   static bool HttpsStateIsModern(Document* aDocument);
 
+  /**
+   * Returns true of the document's URI is a .onion
+   */
+  static bool DocumentHasOnionURI(Document* aDocument);
+
   /**
    * Returns true if the channel is for top-level window and is over secure
    * context.
diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp
index 653c8c03fbdc..e5cb4d31d94c 100644
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -1881,7 +1881,8 @@ bool nsGlobalWindowOuter::ComputeIsSecureContext(Document* aDocument,
     return false;
   }
 
-  if (nsContentUtils::HttpsStateIsModern(aDocument)) {
+  if (nsContentUtils::HttpsStateIsModern(aDocument) ||
+      nsContentUtils::DocumentHasOnionURI(aDocument)) {
     return true;
   }
 
diff --git a/dom/ipc/WindowGlobalActor.cpp b/dom/ipc/WindowGlobalActor.cpp
index 64085dbe5d4c..b859ba966ac8 100644
--- a/dom/ipc/WindowGlobalActor.cpp
+++ b/dom/ipc/WindowGlobalActor.cpp
@@ -20,6 +20,7 @@
 #include "mozilla/net/CookieJarSettings.h"
 #include "mozilla/dom/WindowGlobalChild.h"
 #include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/dom/nsMixedContentBlocker.h"
 
 #include "nsGlobalWindowInner.h"
 #include "nsNetUtil.h"
@@ -116,7 +117,9 @@ WindowGlobalInit WindowGlobalActor::WindowInitializer(
   // Init Mixed Content Fields
   nsCOMPtr<nsIURI> innerDocURI = NS_GetInnermostURI(doc->GetDocumentURI());
   if (innerDocURI) {
-    fields.mIsSecure = innerDocURI->SchemeIs("https");
+    fields.mIsSecure =
+        innerDocURI->SchemeIs("https") ||
+        nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(innerDocURI);
   }
   nsCOMPtr<nsIChannel> mixedChannel;
   aWindow->GetDocShell()->GetMixedContentChannel(getter_AddRefs(mixedChannel));
diff --git a/dom/ipc/WindowGlobalChild.cpp b/dom/ipc/WindowGlobalChild.cpp
index 92a6fd1e3b7c..6635b83bd339 100644
--- a/dom/ipc/WindowGlobalChild.cpp
+++ b/dom/ipc/WindowGlobalChild.cpp
@@ -45,6 +45,8 @@
 #  include "GeckoProfiler.h"
 #endif
 
+#include "mozilla/dom/nsMixedContentBlocker.h"
+
 using namespace mozilla::ipc;
 using namespace mozilla::dom::ipc;
 
@@ -230,7 +232,9 @@ void WindowGlobalChild::OnNewDocument(Document* aDocument) {
   nsCOMPtr<nsIURI> innerDocURI =
       NS_GetInnermostURI(aDocument->GetDocumentURI());
   if (innerDocURI) {
-    txn.SetIsSecure(innerDocURI->SchemeIs("https"));
+    txn.SetIsSecure(
+        innerDocURI->SchemeIs("https") ||
+        nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(innerDocURI));
   }
   nsCOMPtr<nsIChannel> mixedChannel;
   mWindowGlobal->GetDocShell()->GetMixedContentChannel(
diff --git a/dom/security/nsMixedContentBlocker.cpp b/dom/security/nsMixedContentBlocker.cpp
index 64a9f3178874..ba88625eda69 100644
--- a/dom/security/nsMixedContentBlocker.cpp
+++ b/dom/security/nsMixedContentBlocker.cpp
@@ -638,8 +638,8 @@ nsresult nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect,
     return NS_OK;
   }
 
-  // Check the parent scheme. If it is not an HTTPS page then mixed content
-  // restrictions do not apply.
+  // Check the parent scheme. If it is not an HTTPS or .onion page then mixed
+  // content restrictions do not apply.
   nsCOMPtr<nsIURI> innerRequestingLocation =
       NS_GetInnermostURI(requestingLocation);
   if (!innerRequestingLocation) {
@@ -654,6 +654,17 @@ nsresult nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect,
 
   bool parentIsHttps = innerRequestingLocation->SchemeIs("https");
   if (!parentIsHttps) {
+    bool parentIsOnion = IsPotentiallyTrustworthyOnion(innerRequestingLocation);
+    if (!parentIsOnion) {
+      *aDecision = ACCEPT;
+      return NS_OK;
+    }
+  }
+
+  bool isHttpScheme = innerContentLocation->SchemeIs("http");
+  // .onion URLs are encrypted and authenticated. Don't treat them as mixed
+  // content if potentially trustworthy (i.e. whitelisted).
+  if (isHttpScheme && IsPotentiallyTrustworthyOnion(innerContentLocation)) {
     *aDecision = ACCEPT;
     MOZ_LOG(sMCBLog, LogLevel::Verbose,
             ("  -> decision: Request will be allowed because the requesting "
@@ -680,7 +691,6 @@ nsresult nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect,
     return NS_OK;
   }
 
-  bool isHttpScheme = innerContentLocation->SchemeIs("http");
   if (isHttpScheme && IsPotentiallyTrustworthyOrigin(innerContentLocation)) {
     *aDecision = ACCEPT;
     return NS_OK;
diff --git a/mobile/android/modules/geckoview/GeckoViewProgress.jsm b/mobile/android/modules/geckoview/GeckoViewProgress.jsm
index 17069dbe657f..c1346b1858cf 100644
--- a/mobile/android/modules/geckoview/GeckoViewProgress.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewProgress.jsm
@@ -145,6 +145,10 @@ var IdentityHandler = {
       result.host = uri.host;
     }
 
+    if (!aBrowser.securityUI.secInfo) {
+      return result;
+    }
+
     const cert = aBrowser.securityUI.secInfo.serverCert;
 
     result.certificate = aBrowser.securityUI.secInfo.serverCert.getBase64DERString();
diff --git a/security/manager/ssl/nsSecureBrowserUI.cpp b/security/manager/ssl/nsSecureBrowserUI.cpp
index b4de1a331ffc..f1ce39582854 100644
--- a/security/manager/ssl/nsSecureBrowserUI.cpp
+++ b/security/manager/ssl/nsSecureBrowserUI.cpp
@@ -9,6 +9,7 @@
 #include "mozilla/Logging.h"
 #include "mozilla/Unused.h"
 #include "mozilla/dom/Document.h"
+#include "mozilla/dom/nsMixedContentBlocker.h"
 #include "nsContentUtils.h"
 #include "nsIChannel.h"
 #include "nsDocShell.h"
@@ -85,6 +86,17 @@ void nsSecureBrowserUI::RecomputeSecurityFlags() {
         }
       }
     }
+
+    // any protocol routed over tor is secure
+    if (!(mState & nsIWebProgressListener::STATE_IS_SECURE)) {
+      nsCOMPtr<nsIURI> innerDocURI = NS_GetInnermostURI(win->GetDocumentURI());
+      if (innerDocURI &&
+          nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(innerDocURI)) {
+        MOZ_LOG(gSecureBrowserUILog, LogLevel::Debug, ("  is onion"));
+        mState = (mState & ~nsIWebProgressListener::STATE_IS_INSECURE) |
+                 nsIWebProgressListener::STATE_IS_SECURE;
+      }
+    }
   }
 
   // Add upgraded-state flags when request has been
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 31575: Replace Firefox Home (newtab) with about:tor
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit d1f8c8945f182d723fb682bb35b5cf3e5e07ea32
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Mon Sep 9 13:04:34 2019 +0200
    Bug 31575: Replace Firefox Home (newtab) with about:tor
    
    Avoid loading AboutNewTab in BrowserGlue.jsm in order
    to avoid several network requests that we do not need. Besides,
    about:newtab will now point to about:blank or about:tor (depending
    on browser.newtabpage.enabled) and about:home will point to
    about:tor.
---
 browser/components/BrowserGlue.jsm               | 33 ++----------------------
 browser/components/newtab/AboutNewTabService.jsm | 15 +----------
 browser/components/preferences/home.inc.xhtml    |  4 +--
 browser/components/preferences/preferences.xhtml |  5 +++-
 browser/modules/HomePage.jsm                     |  2 +-
 5 files changed, 10 insertions(+), 49 deletions(-)
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index 231bb540921b..10c789d822f7 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -18,7 +18,6 @@ const { AppConstants } = ChromeUtils.import(
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
-  AboutNewTab: "resource:///modules/AboutNewTab.jsm",
   ActorManagerParent: "resource://gre/modules/ActorManagerParent.jsm",
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm",
@@ -225,28 +224,6 @@ let JSWINDOWACTORS = {
     matches: ["about:newinstall"],
   },
 
-  AboutNewTab: {
-    parent: {
-      moduleURI: "resource:///actors/AboutNewTabParent.jsm",
-    },
-    child: {
-      moduleURI: "resource:///actors/AboutNewTabChild.jsm",
-      events: {
-        DOMContentLoaded: {},
-        pageshow: {},
-        visibilitychange: {},
-      },
-    },
-    // The wildcard on about:newtab is for the ?endpoint query parameter
-    // that is used for snippets debugging. The wildcard for about:home
-    // is similar, and also allows for falling back to loading the
-    // about:home document dynamically if an attempt is made to load
-    // about:home?jscache from the AboutHomeStartupCache as a top-level
-    // load.
-    matches: ["about:home*", "about:welcome", "about:newtab*"],
-    remoteTypes: ["privilegedabout"],
-  },
-
   AboutPlugins: {
     parent: {
       moduleURI: "resource:///actors/AboutPluginsParent.jsm",
@@ -1650,8 +1627,6 @@ BrowserGlue.prototype = {
 
   // the first browser window has finished initializing
   _onFirstWindowLoaded: function BG__onFirstWindowLoaded(aWindow) {
-    AboutNewTab.init();
-
     TabCrashHandler.init();
 
     ProcessHangMonitor.init();
@@ -5142,12 +5117,8 @@ var AboutHomeStartupCache = {
       return { pageInputStream: null, scriptInputStream: null };
     }
 
-    let state = AboutNewTab.activityStream.store.getState();
-    return new Promise(resolve => {
-      this._cacheDeferred = resolve;
-      this.log.trace("Parent is requesting cache streams.");
-      this._procManager.sendAsyncMessage(this.CACHE_REQUEST_MESSAGE, { state });
-    });
+    this.log.error("Activity Stream is disabled in Tor Browser.");
+    return { pageInputStream: null, scriptInputStream: null };
   },
 
   /**
diff --git a/browser/components/newtab/AboutNewTabService.jsm b/browser/components/newtab/AboutNewTabService.jsm
index 605d34c0de1c..3f4f04a386db 100644
--- a/browser/components/newtab/AboutNewTabService.jsm
+++ b/browser/components/newtab/AboutNewTabService.jsm
@@ -425,20 +425,7 @@ class BaseAboutNewTabService {
    * the newtab page has no effect on the result of this function.
    */
   get defaultURL() {
-    // Generate the desired activity stream resource depending on state, e.g.,
-    // "resource://activity-stream/prerendered/activity-stream.html"
-    // "resource://activity-stream/prerendered/activity-stream-debug.html"
-    // "resource://activity-stream/prerendered/activity-stream-noscripts.html"
-    return [
-      "resource://activity-stream/prerendered/",
-      "activity-stream",
-      // Debug version loads dev scripts but noscripts separately loads scripts
-      this.activityStreamDebug && !this.privilegedAboutProcessEnabled
-        ? "-debug"
-        : "",
-      this.privilegedAboutProcessEnabled ? "-noscripts" : "",
-      ".html",
-    ].join("");
+    return "about:tor";
   }
 
   get welcomeURL() {
diff --git a/browser/components/preferences/home.inc.xhtml b/browser/components/preferences/home.inc.xhtml
index c348e1cf754b..c37dc5e731f6 100644
--- a/browser/components/preferences/home.inc.xhtml
+++ b/browser/components/preferences/home.inc.xhtml
@@ -33,7 +33,7 @@
                 class="check-home-page-controlled"
                 data-preference-related="browser.startup.homepage">
         <menupopup>
-          <menuitem value="0" data-l10n-id="home-mode-choice-default" />
+          <menuitem value="0" label="&aboutTor.title;" />
           <menuitem value="2" data-l10n-id="home-mode-choice-custom" />
           <menuitem value="1" data-l10n-id="home-mode-choice-blank" />
         </menupopup>
@@ -85,7 +85,7 @@
         Preferences so we need to handle setting the pref manually.-->
       <menulist id="newTabMode" flex="1" data-preference-related="browser.newtabpage.enabled">
         <menupopup>
-          <menuitem value="0" data-l10n-id="home-mode-choice-default" />
+          <menuitem value="0" label="&aboutTor.title;" />
           <menuitem value="1" data-l10n-id="home-mode-choice-blank" />
         </menupopup>
       </menulist>
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
index 5f9900564288..c1322ea10357 100644
--- a/browser/components/preferences/preferences.xhtml
+++ b/browser/components/preferences/preferences.xhtml
@@ -14,7 +14,10 @@
 <?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?>
 <?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?>
 
-<!DOCTYPE html>
+<!DOCTYPE html [
+<!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd">
+  %aboutTorDTD;
+]>
 
 <html xmlns="http://www.w3.org/1999/xhtml"
         xmlns:html="http://www.w3.org/1999/xhtml"
diff --git a/browser/modules/HomePage.jsm b/browser/modules/HomePage.jsm
index 751e6ebb39b3..01317b9e9754 100644
--- a/browser/modules/HomePage.jsm
+++ b/browser/modules/HomePage.jsm
@@ -21,7 +21,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
 });
 
 const kPrefName = "browser.startup.homepage";
-const kDefaultHomePage = "about:home";
+const kDefaultHomePage = "about:tor";
 const kExtensionControllerPref =
   "browser.startup.homepage_override.extensionControlled";
 const kHomePageIgnoreListId = "homepage-urls";
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 27511: Add new identity button to toolbar
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit ebe87b1e46a157dc9e61ba2e62e13fd7ded2cd76
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Fri Oct 4 19:08:33 2019 +0200
    Bug 27511: Add new identity button to toolbar
    
    Also added 'New circuit for this site' button to CustomizableUI, but
    not visible by default.
---
 browser/base/content/browser.xhtml                  | 10 ++++++++++
 .../components/customizableui/CustomizableUI.jsm    | 21 +++++++++++++++++++++
 browser/themes/shared/icons/new_circuit.svg         |  8 ++++++++
 browser/themes/shared/icons/new_identity.svg        |  9 +++++++++
 browser/themes/shared/jar.inc.mn                    |  3 +++
 browser/themes/shared/menupanel.inc.css             |  8 ++++++++
 browser/themes/shared/toolbarbutton-icons.inc.css   |  8 ++++++++
 7 files changed, 67 insertions(+)
diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml
index c859d65af413..4c3a7e7cae55 100644
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -2281,6 +2281,16 @@
                      ondragenter="newWindowButtonObserver.onDragOver(event)"
                      ondragexit="newWindowButtonObserver.onDragExit(event)"/>
 
+      <toolbarbutton id="new-identity-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+                     label="&torbutton.context_menu.new_identity;"
+                     oncommand="torbutton_new_identity();"
+                     tooltiptext="&torbutton.context_menu.new_identity;"/>
+
+      <toolbarbutton id="new-circuit-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+                     label="&torbutton.context_menu.new_circuit;"
+                     oncommand="torbutton_new_circuit();"
+                     tooltiptext="&torbutton.context_menu.new_circuit;"/>
+
       <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
                      observes="View:FullScreen"
                      type="checkbox"
diff --git a/browser/components/customizableui/CustomizableUI.jsm b/browser/components/customizableui/CustomizableUI.jsm
index 5188ac328fbd..e3942dec97fd 100644
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -87,6 +87,8 @@ const kSubviewEvents = ["ViewShowing", "ViewHiding"];
  */
 var kVersion = 16;
 
+var kTorVersion = 1;
+
 /**
  * Buttons removed from built-ins by version they were removed. kVersion must be
  * bumped any time a new id is added to this. Use the button id as key, and
@@ -603,6 +605,20 @@ var CustomizableUIInternal = {
         navbarPlacements.push("fxa-toolbar-menu-button");
       }
     }
+
+    let currentTorVersion = gSavedState.currentTorVersion;
+    if (currentTorVersion < 1 && gSavedState.placements) {
+      let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
+      if (navbarPlacements) {
+        let secLevelIndex = navbarPlacements.indexOf("security-level-button");
+        if (secLevelIndex === -1) {
+          let urlbarIndex = navbarPlacements.indexOf("urlbar-container");
+          secLevelIndex = urlbarIndex + 1;
+          navbarPlacements.splice(secLevelIndex, 0, "security-level-button");
+        }
+        navbarPlacements.splice(secLevelIndex + 1, 0, "new-identity-button");
+      }
+    }
   },
 
   _updateForNewProtonVersion() {
@@ -2505,6 +2521,10 @@ var CustomizableUIInternal = {
       gSavedState.currentVersion = 0;
     }
 
+    if (!("currentTorVersion" in gSavedState)) {
+      gSavedState.currentTorVersion = 0;
+    }
+
     gSeenWidgets = new Set(gSavedState.seen || []);
     gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
     gNewElementCount = gSavedState.newElementCount || 0;
@@ -2583,6 +2603,7 @@ var CustomizableUIInternal = {
       seen: gSeenWidgets,
       dirtyAreaCache: gDirtyAreaCache,
       currentVersion: kVersion,
+      currentTorVersion: kTorVersion,
       newElementCount: gNewElementCount,
     };
 
diff --git a/browser/themes/shared/icons/new_circuit.svg b/browser/themes/shared/icons/new_circuit.svg
new file mode 100644
index 000000000000..e0a93cc83502
--- /dev/null
+++ b/browser/themes/shared/icons/new_circuit.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>Icon / New Circuit(a)1.5x</title>
+    <g id="Icon-/-New-Circuit" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <path d="M13.4411138,10.1446317 L9.5375349,10.1446317 C8.99786512,10.1446317 8.56164018,10.5818326 8.56164018,11.1205264 C8.56164018,11.6592203 8.99786512,12.0964212 9.5375349,12.0964212 L11.4571198,12.0964212 C10.7554515,13.0479185 9.73466563,13.692009 8.60067597,13.9359827 C8.41818366,13.9720908 8.23276366,14.0033194 8.04734366,14.0218614 C7.97219977,14.0277168 7.89803177,14.0306445 7.82288788,14.0335722 C6.07506044,14.137017 4.290149,13.4499871 3.38647049,11.857327 C2.52280367,10.3349312 2.77263271,8.15966189 3.93687511,6.87343267 C5.12453898,5.56183017 7.44814431,5.04363008 8.21226987,3.38558497 C9.01738301,4.92847451 9.60682342,5.02801577 10.853041,6.15029468 C11.2892659,6.54455615 11.9704404,7.55558307 12.1861132,8.10501179 C12.3051723,8.40949094 12.5013272,9.17947187 12.5013272,9.17947187 L14.2862386,9.17947187 C14.2091429,7.59754654 13.439162,5.96877827 12.2261248,4.93628166 C11.279507,4.13116853 10.5065984,3.84718317 9.77662911,2.8088312 C9.63219669,2.60194152 9.599
 99216,2.4565332 9.56290816,2.21646311 C9.53851079,2.00762164 9.54143848,1.78511764 9.62048595,1.53919218 C9.65952174,1.41720534 9.59804037,1.28545955 9.47702943,1.23764071 L6.40296106,0.0167964277 C6.32391359,-0.0134563083 6.23413128,-0.00272146652 6.16679454,0.0480250584 L5.95502539,0.206120002 C5.85743592,0.280288 5.82815908,0.416913259 5.89159223,0.523285783 C6.70060895,1.92564648 6.36978064,2.82542141 5.8984235,3.20211676 C5.4914754,3.4900057 4.99084141,3.72226864 4.63366394,3.95453159 C3.82367132,4.47956294 3.03222071,5.02508808 2.40374451,5.76774396 C0.434388969,8.09427695 0.519291809,12.0046871 2.77165682,14.1077402 C3.65288975,14.9284676 4.70295247,15.4749686 5.81742423,15.7570022 C5.81742423,15.7570022 6.13556591,15.833122 6.21754107,15.8497122 C7.36616915,16.0829511 8.53529102,16.0146384 9.62243774,15.6672199 C9.67416016,15.6525815 9.77174963,15.620377 9.76784605,15.6154975 C10.7730176,15.2700308 11.7049971,14.7010841 12.4652191,13.90573 L12.4652191,15.0241053 C12.4652191,
 15.5627992 12.901444,16 13.4411138,16 C13.9798077,16 14.4170085,15.5627992 14.4170085,15.0241053 L14.4170085,11.1205264 C14.4170085,10.5818326 13.9798077,10.1446317 13.4411138,10.1446317" id="Fill-3" fill="context-fill" fill-opacity="context-fill-opacity"></path>
+        <path d="M5.107,7.462 C4.405,8.078 4,8.946 4,9.839 C4,10.712 4.422,11.57 5.13,12.132 C5.724,12.607 6.627,12.898 7.642,12.949 L7.642,5.8 C7.39,6.029 7.103,6.227 6.791,6.387 C5.993,6.812 5.489,7.133 5.107,7.462" id="Fill-1" fill="context-fill" fill-opacity="context-fill-opacity"></path>
+    </g>
+</svg>
diff --git a/browser/themes/shared/icons/new_identity.svg b/browser/themes/shared/icons/new_identity.svg
new file mode 100644
index 000000000000..91d5b35f7e80
--- /dev/null
+++ b/browser/themes/shared/icons/new_identity.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>New Identity Icon</title>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="New-Identity-Icon" fill="#000000" fill-rule="nonzero">
+            <path d="M4.65687153,14.5532899 L5.79494313,12.0855326 C5.8689125,11.9251399 5.6620883,11.7793527 5.53742729,11.9040137 L3.77194352,13.6694975 L2.32342782,12.2228406 L4.089841,10.4564274 C4.21450201,10.3317664 4.06871482,10.1249422 3.90832206,10.1989116 L1.43773764,11.338287 L0.206601383,10.1087306 C0.0509544211,9.9532834 -0.0167994233,9.75447206 0.00351451705,9.53432844 C0.0238284574,9.31418483 0.154794797,9.13897939 0.330406365,9.0302193 L4.61213917,6.53066101 C4.98542292,6.31331572 5.42541251,6.16259067 5.8659261,6.07796117 C6.63682488,5.92985954 7.40999434,6.06817199 8.09666802,6.42610336 L12.618483,1.910278 C13.0562019,1.47313888 13.7399062,1.45652879 14.1403159,1.87828207 C14.5407256,2.30003536 14.523905,2.96081599 14.0861861,3.39795511 L9.56437119,7.91378047 C9.92258101,8.57753432 10.0391721,9.37155544 9.91292178,10.1416209 C9.85023328,10.5817332 9.67706706,10.9989392 9.45960494,11.3937636 L6.95651989,15.6478297 C6.84761416,15.82321 6.6720026,15.9319701 6.47398108
 ,15.9964916 C6.25354962,16.0167745 6.0544801,15.9491049 5.89883314,15.7936577 L4.65687153,14.5532899 L4.65687153,14.5532899 Z M6.35600863,9.57888316 C6.35684236,9.57982492 6.35770616,9.58074275 6.35860024,9.58163642 L7.56801202,10.7899206 C7.78820303,11.010009 8.15567242,10.9533982 8.29166823,10.678253 C8.42766403,10.4031079 8.55818512,10.1511975 8.61427424,9.83946755 C8.73630873,9.14856819 8.51477165,8.45005355 8.01189873,7.92920397 C8.01085853,7.92816425 8.00979562,7.92715687 8.00871022,7.92618158 C8.00773493,7.92509618 8.00672754,7.92403327 8.00568783,7.92299307 C7.48483824,7.42012014 6.7863236,7.19858307 6.09542425,7.32061756 C5.78369428,7.37670668 5.53178393,7.50722777 5.25663877,7.64322357 C4.98149362,7.77921937 4.92488284,8.14668876 5.14497116,8.36687978 L6.35325537,9.57629155 C6.35414904,9.57718564 6.35506687,9.57804944 6.35600863,9.57888316 L6.35600863,9.57888316 Z M3.56503003,4.86094581 C3.44279837,4.85716019 3.33693302,4.76594656 3.31450832,4.6450962 C3.29259157,4.5009814
 3 3.24425431,4.36089837 3.1719467,4.23194774 C3.04272848,4.15978087 2.90235166,4.11153221 2.75793184,4.08964745 C2.63678145,4.06729735 2.5453314,3.9616241 2.54155161,3.83961366 C2.53777182,3.71760322 2.62276629,3.61489221 2.74265726,3.59658884 C2.88757581,3.57942626 3.02687427,3.53584537 3.15371096,3.46798665 C3.21938702,3.3436261 3.26061987,3.20700605 3.27529255,3.0651408 C3.29205048,2.94466859 3.39451537,2.85825378 3.5172925,2.86104768 C3.6386065,2.86399065 3.74452528,2.95324633 3.76872081,3.07292141 C3.79288781,3.21715288 3.84342323,3.35694342 3.91777207,3.4852254 C4.04615548,3.55876237 4.18583906,3.60883869 4.32991405,3.63297757 C4.45015386,3.6576218 4.53936117,3.76418021 4.54139495,3.88559216 C4.54342874,4.00700411 4.45770065,4.10814717 4.33816215,4.12536877 C4.1960481,4.14067978 4.05931708,4.18249381 3.9349938,4.24866259 C3.86697751,4.37522253 3.82328954,4.51422019 3.80607564,4.65882867 C3.78847982,4.77811508 3.68677836,4.86339193 3.56503003,4.86094581 Z M14.4103464,14.3126948
  C14.2513672,14.307719 14.1137716,14.188804 14.0849193,14.0314492 C14.045996,13.7585014 13.9510862,13.4938971 13.8061961,13.2543814 C13.5663773,13.109665 13.301434,13.0148623 13.0281329,12.9759728 C12.8707684,12.946921 12.75198,12.8095493 12.7470672,12.6509372 C12.7421545,12.492325 12.8525523,12.3587997 13.0082799,12.3350024 C13.2816632,12.3044807 13.5433622,12.2185794 13.7775725,12.0824861 C13.9099238,11.8524988 13.992337,11.5955854 14.0197279,11.3275956 C14.0417134,11.1717293 14.1740126,11.0598594 14.3327736,11.0628895 C14.4905572,11.0667732 14.6282205,11.1831391 14.6593783,11.3389665 C14.703143,11.6110771 14.8017156,11.8740418 14.9490566,12.1117486 C15.1872615,12.2578242 15.450159,12.3559923 15.7221615,12.4004323 C15.8783433,12.4324665 15.9942186,12.5709889 15.9968634,12.7288231 C15.9995083,12.8866572 15.8881575,13.0181443 15.7328877,13.0405352 C15.4641157,13.0669716 15.2064728,13.14931 14.9763475,13.2823129 C14.8406047,13.5164173 14.7548186,13.7777086 14.724105,14.0506041 C14.70
 09285,14.2056508 14.5685348,14.3162427 14.4103464,14.3126948 Z M8.37194288,2.75251202 C8.23729358,2.7482977 8.12075529,2.6475812 8.09631849,2.5143077 C8.06335201,2.28313133 7.98296703,2.05902158 7.86025062,1.85616098 C7.65713325,1.73359169 7.43273641,1.65329741 7.2012608,1.62035947 C7.06797908,1.59575373 6.9673698,1.47940513 6.96320889,1.34506671 C6.95904797,1.21072829 7.05255074,1.09763741 7.18444606,1.07748204 C7.41599123,1.0516313 7.6376403,0.978876138 7.83600755,0.863610339 C7.94810399,0.668819911 8.01790485,0.45122403 8.04110388,0.224246882 C8.05972477,0.0922341146 8.17177714,-0.00251545243 8.30624168,5.089704e-05 C8.43987839,0.00334026838 8.55647391,0.101897787 8.58286336,0.233877601 C8.61993042,0.464344927 8.70341768,0.687066016 8.82820981,0.888394549 C9.02996027,1.012115 9.25262444,1.09525963 9.4830002,1.13289867 C9.6152802,1.16003037 9.71342219,1.27735361 9.71566226,1.41103311 C9.71790232,1.5447126 9.62359245,1.65607713 9.49208487,1.67504141 C9.26444525,1.69743199 9.0462315
 3,1.76716948 8.85132417,1.87981789 C8.73635526,2.07809534 8.66369764,2.2993991 8.63768445,2.53053117 C8.61805481,2.66184983 8.50592239,2.75551697 8.37194288,2.75251202 Z" id="Shape" fill="context-fill" fill-opacity="context-fill-opacity"></path>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn
index cbf18156a455..5b41d65ebd45 100644
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -272,3 +272,6 @@
   skin/classic/browser/places/tree-icons.css                   (../shared/places/tree-icons.css)
   skin/classic/browser/privatebrowsing/aboutPrivateBrowsing.css (../shared/privatebrowsing/aboutPrivateBrowsing.css)
   skin/classic/browser/privatebrowsing/favicon.svg             (../shared/privatebrowsing/favicon.svg)
+
+  skin/classic/browser/new_circuit.svg                         (../shared/icons/new_circuit.svg)
+  skin/classic/browser/new_identity.svg                        (../shared/icons/new_identity.svg)
diff --git a/browser/themes/shared/menupanel.inc.css b/browser/themes/shared/menupanel.inc.css
index d5079e9881f2..91dae3481f2e 100644
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -195,3 +195,11 @@ toolbarpaletteitem[place="palette"] > #bookmarks-menu-button,
   list-style-image: url(chrome://browser/skin/fullscreen-exit.svg);
 }
 } /** END Proton **/
+
+#appMenuNewIdentity {
+  list-style-image: url("chrome://browser/skin/new_identity.svg");
+}
+
+#appMenuNewCircuit {
+  list-style-image: url("chrome://browser/skin/new_circuit.svg");
+}
diff --git a/browser/themes/shared/toolbarbutton-icons.inc.css b/browser/themes/shared/toolbarbutton-icons.inc.css
index 3d4fd8192898..d81d7f7b2dcf 100644
--- a/browser/themes/shared/toolbarbutton-icons.inc.css
+++ b/browser/themes/shared/toolbarbutton-icons.inc.css
@@ -203,6 +203,14 @@ toolbar[brighttext] {
   list-style-image: url("chrome://browser/skin/new-tab.svg");
 }
 
+#new-identity-button {
+  list-style-image: url("chrome://browser/skin/new_identity.svg");
+}
+
+#new-circuit-button {
+  list-style-image: url("chrome://browser/skin/new_circuit.svg");
+}
+
 #privatebrowsing-button {
   list-style-image: url("chrome://browser/skin/privateBrowsing.svg");
 }
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 31607: App menu items stop working on macOS
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit 873a2a8500178775f1138b377c2258b9a9064b2d
Author: Kathy Brade <brade(a)pearlcrescent.com>
Date:   Thu Oct 3 10:53:43 2019 -0400
    Bug 31607: App menu items stop working on macOS
    
    Avoid re-creating the hidden window, since this causes the nsMenuBarX
    object that is associated with the app menu to be freed (which in
    turn causes all of the app menu items to stop working).
    
    More detail: There should only be one hidden window.
    XREMain::XRE_mainRun() contains an explicit call to create the
    hidden window and that is the normal path by which it is created.
    However, when Tor Launcher's wizard/progress window is opened during
    startup, a hidden window is created earlier as a side effect of
    calls to nsAppShellService::GetHiddenWindow(). Then, when
    XREMain::XRE_mainRun() creates its hidden window, the original one
    is freed which also causes the app menu's nsMenuBarX object which
    is associated with that window to be destroyed. When that happens,
    the menuGroupOwner property within each Cocoa menu items's MenuItemInfo
    object is cleared. This breaks the link that is necessary for
    NativeMenuItemTarget's menuItemHit method to dispatch a menu item
    event.
---
 xpfe/appshell/nsAppShellService.cpp | 4 ++++
 1 file changed, 4 insertions(+)
diff --git a/xpfe/appshell/nsAppShellService.cpp b/xpfe/appshell/nsAppShellService.cpp
index c7c3da49d86e..1cb1c6f5a7f9 100644
--- a/xpfe/appshell/nsAppShellService.cpp
+++ b/xpfe/appshell/nsAppShellService.cpp
@@ -93,6 +93,10 @@ void nsAppShellService::EnsureHiddenWindow() {
 
 NS_IMETHODIMP
 nsAppShellService::CreateHiddenWindow() {
+  if (mHiddenWindow) {
+    return NS_OK;
+  }
+
   if (!XRE_IsParentProcess()) {
     return NS_ERROR_NOT_IMPLEMENTED;
   }
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-88.0b4-10.5-1] Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#tor
                        
                        
by sysrqb@torproject.org 20 Apr '21
                    by sysrqb@torproject.org 20 Apr '21
20 Apr '21
                    
                        commit 3e4d4ae387bafeea70b851df5ae1e6e6238589a2
Author: Richard Pospesel <richard(a)torproject.org>
Date:   Mon Sep 16 15:25:39 2019 -0700
    Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#tor
    
    This patch adds a new about:preferences#tor page which allows modifying
    bridge, proxy, and firewall settings from within Tor Browser. All of the
    functionality present in tor-launcher's Network Configuration panel is
    present:
    
     - Setting built-in bridges
     - Requesting bridges from BridgeDB via moat
     - Using user-provided bridges
     - Configuring SOCKS4, SOCKS5, and HTTP/HTTPS proxies
     - Setting firewall ports
     - Viewing and Copying Tor's logs
     - The Networking Settings in General preferences has been removed
---
 browser/components/moz.build                       |   1 +
 browser/components/preferences/main.inc.xhtml      |  55 --
 browser/components/preferences/main.js             |  14 -
 browser/components/preferences/preferences.js      |   9 +
 browser/components/preferences/preferences.xhtml   |   5 +
 browser/components/preferences/privacy.js          |   1 +
 .../torpreferences/content/parseFunctions.jsm      |  89 +++
 .../torpreferences/content/requestBridgeDialog.jsm | 204 +++++
 .../content/requestBridgeDialog.xhtml              |  35 +
 .../torpreferences/content/torBridgeSettings.jsm   | 325 ++++++++
 .../torpreferences/content/torCategory.inc.xhtml   |   9 +
 .../torpreferences/content/torFirewallSettings.jsm |  72 ++
 .../torpreferences/content/torLogDialog.jsm        |  66 ++
 .../torpreferences/content/torLogDialog.xhtml      |  23 +
 .../components/torpreferences/content/torPane.js   | 857 +++++++++++++++++++++
 .../torpreferences/content/torPane.xhtml           | 123 +++
 .../torpreferences/content/torPreferences.css      |  77 ++
 .../torpreferences/content/torPreferencesIcon.svg  |   5 +
 .../torpreferences/content/torProxySettings.jsm    | 245 ++++++
 browser/components/torpreferences/jar.mn           |  14 +
 browser/components/torpreferences/moz.build        |   1 +
 browser/modules/BridgeDB.jsm                       | 110 +++
 browser/modules/TorProtocolService.jsm             | 212 +++++
 browser/modules/moz.build                          |   2 +
 24 files changed, 2485 insertions(+), 69 deletions(-)
diff --git a/browser/components/moz.build b/browser/components/moz.build
index ef09055b990a..e99fa19d896a 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -55,6 +55,7 @@ DIRS += [
     "syncedtabs",
     "uitour",
     "urlbar",
+    "torpreferences",
     "translation",
 ]
 
diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml
index 2d55e165c5ba..78d0cea8b8a2 100644
--- a/browser/components/preferences/main.inc.xhtml
+++ b/browser/components/preferences/main.inc.xhtml
@@ -665,59 +665,4 @@
     <label id="cfrFeaturesLearnMore" class="learnMore" data-l10n-id="browsing-cfr-recommendations-learn-more" is="text-link"/>
   </hbox>
 </groupbox>
-
-<hbox id="networkProxyCategory"
-      class="subcategory"
-      hidden="true"
-      data-category="paneGeneral">
-  <html:h1 data-l10n-id="network-settings-title"/>
-</hbox>
-
-<!-- Network Settings-->
-<groupbox id="connectionGroup" data-category="paneGeneral" hidden="true">
-  <label class="search-header" hidden="true"><html:h2 data-l10n-id="network-settings-title"/></label>
-
-  <hbox align="center">
-    <hbox align="center" flex="1">
-      <description id="connectionSettingsDescription" control="connectionSettings"/>
-      <spacer width="5"/>
-      <label id="connectionSettingsLearnMore" class="learnMore" is="text-link"
-        data-l10n-id="network-proxy-connection-learn-more">
-      </label>
-      <separator orient="vertical"/>
-    </hbox>
-
-    <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
-    <hbox>
-      <button id="connectionSettings"
-              is="highlightable-button"
-              class="accessory-button"
-              data-l10n-id="network-proxy-connection-settings"
-              searchkeywords="doh trr"
-              search-l10n-ids="
-                connection-window.title,
-                connection-proxy-option-no.label,
-                connection-proxy-option-auto.label,
-                connection-proxy-option-system.label,
-                connection-proxy-option-manual.label,
-                connection-proxy-http,
-                connection-proxy-https,
-                connection-proxy-ftp,
-                connection-proxy-http-port,
-                connection-proxy-socks,
-                connection-proxy-socks4,
-                connection-proxy-socks5,
-                connection-proxy-noproxy,
-                connection-proxy-noproxy-desc,
-                connection-proxy-http-sharing.label,
-                connection-proxy-autotype.label,
-                connection-proxy-reload.label,
-                connection-proxy-autologin.label,
-                connection-proxy-socks-remote-dns.label,
-                connection-dns-over-https.label,
-                connection-dns-over-https-url-custom.label,
-            " />
-    </hbox>
-  </hbox>
-</groupbox>
 </html:template>
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
index b5a192fc274a..6daf5b329878 100644
--- a/browser/components/preferences/main.js
+++ b/browser/components/preferences/main.js
@@ -364,15 +364,6 @@ var gMainPane = {
     });
     this.updatePerformanceSettingsBox({ duringChangeEvent: false });
     this.displayUseSystemLocale();
-    let connectionSettingsLink = document.getElementById(
-      "connectionSettingsLearnMore"
-    );
-    let connectionSettingsUrl =
-      Services.urlFormatter.formatURLPref("app.support.baseURL") +
-      "prefs-connection-settings";
-    connectionSettingsLink.setAttribute("href", connectionSettingsUrl);
-    this.updateProxySettingsUI();
-    initializeProxyUI(gMainPane);
 
     if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
       gMainPane.initBrowserLocale();
@@ -506,11 +497,6 @@ var gMainPane = {
       "change",
       gMainPane.updateHardwareAcceleration.bind(gMainPane)
     );
-    setEventListener(
-      "connectionSettings",
-      "command",
-      gMainPane.showConnections
-    );
     setEventListener(
       "browserContainersCheckbox",
       "command",
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
index 91e9e469cea2..a89fddd0306d 100644
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
@@ -13,6 +13,7 @@
 /* import-globals-from findInPage.js */
 /* import-globals-from ../../base/content/utilityOverlay.js */
 /* import-globals-from ../../../toolkit/content/preferencesBindings.js */
+/* import-globals-from ../torpreferences/content/torPane.js */
 
 "use strict";
 
@@ -136,6 +137,14 @@ function init_all() {
     register_module("paneSync", gSyncPane);
   }
   register_module("paneSearchResults", gSearchResultsPane);
+  if (gTorPane.enabled) {
+    document.getElementById("category-tor").hidden = false;
+    register_module("paneTor", gTorPane);
+  } else {
+    // Remove the pane from the DOM so it doesn't get incorrectly included in search results.
+    document.getElementById("template-paneTor").remove();
+  }
+
   gSearchResultsPane.init();
   gMainPane.preInit();
 
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
index c1322ea10357..fd4098471966 100644
--- a/browser/components/preferences/preferences.xhtml
+++ b/browser/components/preferences/preferences.xhtml
@@ -13,6 +13,7 @@
 <?xml-stylesheet href="chrome://browser/skin/preferences/containers.css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?>
 <?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
 
 <!DOCTYPE html [
 <!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd">
@@ -155,6 +156,9 @@
           <image class="category-icon"/>
           <label class="category-name" flex="1" data-l10n-id="pane-experimental-title"></label>
         </richlistitem>
+
+#include ../torpreferences/content/torCategory.inc.xhtml
+
       </richlistbox>
 
       <spacer flex="1"/>
@@ -208,6 +212,7 @@
 #include containers.inc.xhtml
 #include sync.inc.xhtml
 #include experimental.inc.xhtml
+#include ../torpreferences/content/torPane.xhtml
         </vbox>
       </vbox>
     </vbox>
diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js
index 8401d4d101e4..1c5f31934d82 100644
--- a/browser/components/preferences/privacy.js
+++ b/browser/components/preferences/privacy.js
@@ -80,6 +80,7 @@ XPCOMUtils.defineLazyGetter(this, "AlertsServiceDND", function() {
   }
 });
 
+// TODO: module import via ChromeUtils.defineModuleGetter
 XPCOMUtils.defineLazyScriptGetter(
   this,
   ["SecurityLevelPreferences"],
diff --git a/browser/components/torpreferences/content/parseFunctions.jsm b/browser/components/torpreferences/content/parseFunctions.jsm
new file mode 100644
index 000000000000..954759de63a5
--- /dev/null
+++ b/browser/components/torpreferences/content/parseFunctions.jsm
@@ -0,0 +1,89 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "parsePort",
+  "parseAddrPort",
+  "parseUsernamePassword",
+  "parseAddrPortList",
+  "parseBridgeStrings",
+  "parsePortList",
+];
+
+// expects a string representation of an integer from 1 to 65535
+let parsePort = function(aPort) {
+  // ensure port string is a valid positive integer
+  const validIntRegex = /^[0-9]+$/;
+  if (!validIntRegex.test(aPort)) {
+    throw new Error(`Invalid PORT string : '${aPort}'`);
+  }
+
+  // ensure port value is on valid range
+  let port = Number.parseInt(aPort);
+  if (port < 1 || port > 65535) {
+    throw new Error(
+      `Invalid PORT value, needs to be on range [1,65535] : '${port}'`
+    );
+  }
+
+  return port;
+};
+// expects a string in the format: "ADDRESS:PORT"
+let parseAddrPort = function(aAddrColonPort) {
+  let tokens = aAddrColonPort.split(":");
+  if (tokens.length != 2) {
+    throw new Error(`Invalid ADDRESS:PORT string : '${aAddrColonPort}'`);
+  }
+  let address = tokens[0];
+  let port = parsePort(tokens[1]);
+  return [address, port];
+};
+
+// expects a string in the format: "USERNAME:PASSWORD"
+// split on the first colon and any subsequent go into password
+let parseUsernamePassword = function(aUsernameColonPassword) {
+  let colonIndex = aUsernameColonPassword.indexOf(":");
+  if (colonIndex < 0) {
+    // we don't log the contents of the potentially password containing string
+    throw new Error("Invalid USERNAME:PASSWORD string");
+  }
+
+  let username = aUsernameColonPassword.substring(0, colonIndex);
+  let password = aUsernameColonPassword.substring(colonIndex + 1);
+
+  return [username, password];
+};
+
+// expects a string in the format: ADDRESS:PORT,ADDRESS:PORT,...
+// returns array of ports (as ints)
+let parseAddrPortList = function(aAddrPortList) {
+  let addrPorts = aAddrPortList.split(",");
+  // parse ADDRESS:PORT string and only keep the port (second element in returned array)
+  let retval = addrPorts.map(addrPort => parseAddrPort(addrPort)[1]);
+  return retval;
+};
+
+// expects a '/n' or '/r/n' delimited bridge string, which we split and trim
+// each bridge string can also optionally have 'bridge' at the beginning ie:
+// bridge $(type) $(address):$(port) $(certificate)
+// we strip out the 'bridge' prefix here
+let parseBridgeStrings = function(aBridgeStrings) {
+
+  // replace carriage returns ('\r') with new lines ('\n')
+  aBridgeStrings = aBridgeStrings.replace(/\r/g, "\n");
+  // then replace contiguous new lines ('\n') with a single one
+  aBridgeStrings = aBridgeStrings.replace(/[\n]+/g, "\n");
+
+  // split on the newline and for each bridge string: trim, remove starting 'bridge' string
+  // finally discard entries that are empty strings; empty strings could occur if we receive
+  // a new line containing only whitespace
+  let splitStrings = aBridgeStrings.split("\n");
+  return splitStrings.map(val => val.trim().replace(/^bridge\s+/i, ""))
+                     .filter(bridgeString => bridgeString != "");
+};
+
+// expecting a ',' delimited list of ints with possible white space between
+// returns an array of ints
+let parsePortList = function(aPortListString) {
+  let splitStrings = aPortListString.split(",");
+  return splitStrings.map(val => parsePort(val.trim()));
+};
diff --git a/browser/components/torpreferences/content/requestBridgeDialog.jsm b/browser/components/torpreferences/content/requestBridgeDialog.jsm
new file mode 100644
index 000000000000..807d46cdfb18
--- /dev/null
+++ b/browser/components/torpreferences/content/requestBridgeDialog.jsm
@@ -0,0 +1,204 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["RequestBridgeDialog"];
+
+const { BridgeDB } = ChromeUtils.import("resource:///modules/BridgeDB.jsm");
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+class RequestBridgeDialog {
+  constructor() {
+    this._dialog = null;
+    this._submitButton = null;
+    this._dialogDescription = null;
+    this._captchaImage = null;
+    this._captchaEntryTextbox = null;
+    this._captchaRefreshButton = null;
+    this._incorrectCaptchaHbox = null;
+    this._incorrectCaptchaLabel = null;
+    this._bridges = [];
+    this._proxyURI = null;
+  }
+
+  static get selectors() {
+    return {
+      submitButton:
+        "accept" /* not really a selector but a key for dialog's getButton */,
+      dialogDescription: "description#torPreferences-requestBridge-description",
+      captchaImage: "image#torPreferences-requestBridge-captchaImage",
+      captchaEntryTextbox: "input#torPreferences-requestBridge-captchaTextbox",
+      refreshCaptchaButton:
+        "button#torPreferences-requestBridge-refreshCaptchaButton",
+      incorrectCaptchaHbox:
+        "hbox#torPreferences-requestBridge-incorrectCaptchaHbox",
+      incorrectCaptchaLabel:
+        "label#torPreferences-requestBridge-incorrectCaptchaError",
+    };
+  }
+
+  _populateXUL(dialog) {
+    const selectors = RequestBridgeDialog.selectors;
+
+    this._dialog = dialog;
+    const dialogWin = dialog.parentElement;
+    dialogWin.setAttribute(
+      "title",
+      TorStrings.settings.requestBridgeDialogTitle
+    );
+    // user may have opened a Request Bridge dialog in another tab, so update the
+    // CAPTCHA image or close out the dialog if we have a bridge list
+    this._dialog.addEventListener("focusin", () => {
+      const uri = BridgeDB.currentCaptchaImage;
+      const bridges = BridgeDB.currentBridges;
+
+      // new captcha image
+      if (uri) {
+        this._setcaptchaImage(uri);
+      } else if (bridges) {
+        this._bridges = bridges;
+        this._submitButton.disabled = false;
+        this._dialog.cancelDialog();
+      }
+    });
+
+    this._submitButton = this._dialog.getButton(selectors.submitButton);
+    this._submitButton.setAttribute("label", TorStrings.settings.submitCaptcha);
+    this._submitButton.disabled = true;
+    this._dialog.addEventListener("dialogaccept", e => {
+      e.preventDefault();
+      this.onSubmitCaptcha();
+    });
+
+    this._dialogDescription = this._dialog.querySelector(
+      selectors.dialogDescription
+    );
+    this._dialogDescription.textContent =
+      TorStrings.settings.contactingBridgeDB;
+
+    this._captchaImage = this._dialog.querySelector(selectors.captchaImage);
+
+    // request captcha from bridge db
+    BridgeDB.requestNewCaptchaImage(this._proxyURI).then(uri => {
+      this._setcaptchaImage(uri);
+    });
+
+    this._captchaEntryTextbox = this._dialog.querySelector(
+      selectors.captchaEntryTextbox
+    );
+    this._captchaEntryTextbox.setAttribute(
+      "placeholder",
+      TorStrings.settings.captchaTextboxPlaceholder
+    );
+    this._captchaEntryTextbox.disabled = true;
+    // disable submit if entry textbox is empty
+    this._captchaEntryTextbox.oninput = () => {
+      this._submitButton.disabled = this._captchaEntryTextbox.value == "";
+    };
+
+    this._captchaRefreshButton = this._dialog.querySelector(
+      selectors.refreshCaptchaButton
+    );
+    this._captchaRefreshButton.disabled = true;
+
+    this._incorrectCaptchaHbox = this._dialog.querySelector(
+      selectors.incorrectCaptchaHbox
+    );
+    this._incorrectCaptchaLabel = this._dialog.querySelector(
+      selectors.incorrectCaptchaLabel
+    );
+    this._incorrectCaptchaLabel.setAttribute(
+      "value",
+      TorStrings.settings.incorrectCaptcha
+    );
+
+    return true;
+  }
+
+  _setcaptchaImage(uri) {
+    if (uri != this._captchaImage.src) {
+      this._captchaImage.src = uri;
+      this._dialogDescription.textContent = TorStrings.settings.solveTheCaptcha;
+      this._setUIDisabled(false);
+      this._captchaEntryTextbox.focus();
+      this._captchaEntryTextbox.select();
+    }
+  }
+
+  _setUIDisabled(disabled) {
+    this._submitButton.disabled = this._captchaGuessIsEmpty() || disabled;
+    this._captchaEntryTextbox.disabled = disabled;
+    this._captchaRefreshButton.disabled = disabled;
+  }
+
+  _captchaGuessIsEmpty() {
+    return this._captchaEntryTextbox.value == "";
+  }
+
+  init(window, dialog) {
+    // defer to later until firefox has populated the dialog with all our elements
+    window.setTimeout(() => {
+      this._populateXUL(dialog);
+    }, 0);
+  }
+
+  close() {
+    BridgeDB.close();
+  }
+
+  /*
+    Event Handlers
+  */
+  onSubmitCaptcha() {
+    let captchaText = this._captchaEntryTextbox.value.trim();
+    // noop if the field is empty
+    if (captchaText == "") {
+      return;
+    }
+
+    // freeze ui while we make request
+    this._setUIDisabled(true);
+    this._incorrectCaptchaHbox.style.visibility = "hidden";
+
+    BridgeDB.submitCaptchaGuess(captchaText)
+      .then(aBridges => {
+        this._bridges = aBridges;
+
+        this._submitButton.disabled = false;
+        // This was successful, but use cancelDialog() to close, since
+        // we intercept the `dialogaccept` event.
+        this._dialog.cancelDialog();
+      })
+      .catch(aError => {
+        this._bridges = [];
+        this._setUIDisabled(false);
+        this._incorrectCaptchaHbox.style.visibility = "visible";
+      });
+  }
+
+  onRefreshCaptcha() {
+    this._setUIDisabled(true);
+    this._captchaImage.src = "";
+    this._dialogDescription.textContent =
+      TorStrings.settings.contactingBridgeDB;
+    this._captchaEntryTextbox.value = "";
+    this._incorrectCaptchaHbox.style.visibility = "hidden";
+
+    BridgeDB.requestNewCaptchaImage(this._proxyURI).then(uri => {
+      this._setcaptchaImage(uri);
+    });
+  }
+
+  openDialog(gSubDialog, aProxyURI, aCloseCallback) {
+    this._proxyURI = aProxyURI;
+    gSubDialog.open(
+      "chrome://browser/content/torpreferences/requestBridgeDialog.xhtml",
+      {
+        features: "resizable=yes",
+        closingCallback: () => {
+          this.close();
+          aCloseCallback(this._bridges);
+        }
+      },
+      this,
+    );
+  }
+}
diff --git a/browser/components/torpreferences/content/requestBridgeDialog.xhtml b/browser/components/torpreferences/content/requestBridgeDialog.xhtml
new file mode 100644
index 000000000000..64c4507807fb
--- /dev/null
+++ b/browser/components/torpreferences/content/requestBridgeDialog.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
+
+<window type="child"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml">
+<dialog id="torPreferences-requestBridge-dialog"
+        buttons="accept,cancel">
+  <!-- ok, so ​ is a zero-width space. We need to have *something* in the innerText so that XUL knows how tall the
+       description node is so that it can determine how large to make the dialog element's inner draw area. If we have
+       nothing in the innerText, then it collapse to 0 height, and the contents of the dialog ends up partially hidden >:( -->
+  <description id="torPreferences-requestBridge-description">​</description>
+  <!-- init to transparent 400x125 png -->
+  <image id="torPreferences-requestBridge-captchaImage" flex="1"/>
+  <hbox id="torPreferences-requestBridge-inputHbox">
+    <html:input id="torPreferences-requestBridge-captchaTextbox" type="text" style="-moz-box-flex: 1;"/>
+    <button id="torPreferences-requestBridge-refreshCaptchaButton"
+            image="chrome://browser/skin/reload.svg"
+            oncommand="requestBridgeDialog.onRefreshCaptcha();"/>
+  </hbox>
+  <hbox id="torPreferences-requestBridge-incorrectCaptchaHbox" align="center">
+    <image id="torPreferences-requestBridge-errorIcon" />
+    <label id="torPreferences-requestBridge-incorrectCaptchaError" flex="1"/>
+  </hbox>
+  <script type="application/javascript"><![CDATA[
+    "use strict";
+
+    let requestBridgeDialog = window.arguments[0];
+    let dialog = document.getElementById("torPreferences-requestBridge-dialog");
+    requestBridgeDialog.init(window, dialog);
+  ]]></script>
+</dialog>
+</window>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torBridgeSettings.jsm b/browser/components/torpreferences/content/torBridgeSettings.jsm
new file mode 100644
index 000000000000..ceb61d3ec972
--- /dev/null
+++ b/browser/components/torpreferences/content/torBridgeSettings.jsm
@@ -0,0 +1,325 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "TorBridgeSource",
+  "TorBridgeSettings",
+  "makeTorBridgeSettingsNone",
+  "makeTorBridgeSettingsBuiltin",
+  "makeTorBridgeSettingsBridgeDB",
+  "makeTorBridgeSettingsUserProvided",
+];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+const TorBridgeSource = {
+  NONE: "NONE",
+  BUILTIN: "BUILTIN",
+  BRIDGEDB: "BRIDGEDB",
+  USERPROVIDED: "USERPROVIDED",
+};
+
+class TorBridgeSettings {
+  constructor() {
+    this._bridgeSource = TorBridgeSource.NONE;
+    this._selectedDefaultBridgeType = null;
+    this._bridgeStrings = [];
+  }
+
+  get selectedDefaultBridgeType() {
+    if (this._bridgeSource == TorBridgeSource.BUILTIN) {
+      return this._selectedDefaultBridgeType;
+    }
+    return undefined;
+  }
+
+  get bridgeSource() {
+    return this._bridgeSource;
+  }
+
+  // for display
+  get bridgeStrings() {
+    return this._bridgeStrings.join("\n");
+  }
+
+  // raw
+  get bridgeStringsArray() {
+    return this._bridgeStrings;
+  }
+
+  static get defaultBridgeTypes() {
+    if (TorBridgeSettings._defaultBridgeTypes) {
+      return TorBridgeSettings._defaultBridgeTypes;
+    }
+
+    let bridgeListBranch = Services.prefs.getBranch(
+      TorStrings.preferenceBranches.defaultBridge
+    );
+    let bridgePrefs = bridgeListBranch.getChildList("", {});
+
+    // an unordered set for shoving bridge types into
+    let bridgeTypes = new Set();
+    // look for keys ending in ".N" and treat string before that as the bridge type
+    const pattern = /\.[0-9]+$/;
+    for (const key of bridgePrefs) {
+      const offset = key.search(pattern);
+      if (offset != -1) {
+        const bt = key.substring(0, offset);
+        bridgeTypes.add(bt);
+      }
+    }
+
+    // recommended bridge type goes first in the list
+    let recommendedBridgeType = Services.prefs.getCharPref(
+      TorStrings.preferenceKeys.recommendedBridgeType,
+      null
+    );
+
+    let retval = [];
+    if (recommendedBridgeType && bridgeTypes.has(recommendedBridgeType)) {
+      retval.push(recommendedBridgeType);
+    }
+
+    for (const bridgeType of bridgeTypes.values()) {
+      if (bridgeType != recommendedBridgeType) {
+        retval.push(bridgeType);
+      }
+    }
+
+    // cache off
+    TorBridgeSettings._defaultBridgeTypes = retval;
+    return retval;
+  }
+
+  _readDefaultBridges(aBridgeType) {
+    let bridgeBranch = Services.prefs.getBranch(
+      TorStrings.preferenceBranches.defaultBridge
+    );
+    let bridgeBranchPrefs = bridgeBranch.getChildList("", {});
+
+    let retval = [];
+
+    // regex matches against strings ending in ".N" where N is a positive integer
+    let pattern = /\.[0-9]+$/;
+    for (const key of bridgeBranchPrefs) {
+      // verify the location of the match is the correct offset required for aBridgeType
+      // to fit, and that the string begins with aBridgeType
+      if (
+        key.search(pattern) == aBridgeType.length &&
+        key.startsWith(aBridgeType)
+      ) {
+        let bridgeStr = bridgeBranch.getCharPref(key);
+        retval.push(bridgeStr);
+      }
+    }
+
+    // fisher-yates shuffle
+    // shuffle so that Tor Browser users don't all try the built-in bridges in the same order
+    for (let i = retval.length - 1; i > 0; --i) {
+      // number n such that 0.0 <= n < 1.0
+      const n = Math.random();
+      // integer j such that 0 <= j <= i
+      const j = Math.floor(n * (i + 1));
+
+      // swap values at indices i and j
+      const tmp = retval[i];
+      retval[i] = retval[j];
+      retval[j] = tmp;
+    }
+
+    return retval;
+  }
+
+  _readBridgeDBBridges() {
+    let bridgeBranch = Services.prefs.getBranch(
+      `${TorStrings.preferenceBranches.bridgeDBBridges}`
+    );
+    let bridgeBranchPrefs = bridgeBranch.getChildList("", {});
+    // the child prefs do not come in any particular order so sort the keys
+    // so the values can be compared to what we get out off torrc
+    bridgeBranchPrefs.sort();
+
+    // just assume all of the prefs under the parent point to valid bridge string
+    let retval = bridgeBranchPrefs.map(key =>
+      bridgeBranch.getCharPref(key).trim()
+    );
+
+    return retval;
+  }
+
+  _readTorrcBridges() {
+    let bridgeList = TorProtocolService.readStringArraySetting(
+      TorStrings.configKeys.bridgeList
+    );
+
+    let retval = [];
+    for (const line of bridgeList) {
+      let trimmedLine = line.trim();
+      if (trimmedLine) {
+        retval.push(trimmedLine);
+      }
+    }
+
+    return retval;
+  }
+
+  // analagous to initBridgeSettings()
+  readSettings() {
+    // restore to defaults
+    this._bridgeSource = TorBridgeSource.NONE;
+    this._selectedDefaultBridgeType = null;
+    this._bridgeStrings = [];
+
+    // So the way tor-launcher determines the origin of the configured bridges is a bit
+    // weird and depends on inferring our scenario based on some firefox prefs and the
+    // relationship between the saved list of bridges in about:config vs the list saved in torrc
+
+    // first off, if "extensions.torlauncher.default_bridge_type" is set to one of our
+    // builtin default types (obfs4, meek-azure, snowflake, etc) then we provide the
+    // bridges in "extensions.torlauncher.default_bridge.*" (filtered by our default_bridge_type)
+
+    // next, we compare the list of bridges saved in torrc to the bridges stored in the
+    // "extensions.torlauncher.bridgedb_bridge."" branch. If they match *exactly* then we assume
+    // the bridges were retrieved from BridgeDB and use those. If the torrc list is empty then we know
+    // we have no bridge settings
+
+    // finally, if none of the previous conditions are not met, it is assumed the bridges stored in
+    // torrc are user-provided
+
+    // what we should(?) do once we excise tor-launcher entirely is explicitly store an int/enum in
+    // about:config that tells us which scenario we are in so we don't have to guess
+
+    let defaultBridgeType = Services.prefs.getCharPref(
+      TorStrings.preferenceKeys.defaultBridgeType,
+      null
+    );
+
+    // check if source is BUILTIN
+    if (defaultBridgeType) {
+      this._bridgeStrings = this._readDefaultBridges(defaultBridgeType);
+      this._bridgeSource = TorBridgeSource.BUILTIN;
+      this._selectedDefaultBridgeType = defaultBridgeType;
+      return;
+    }
+
+    let torrcBridges = this._readTorrcBridges();
+
+    // no stored bridges means no bridge is in use
+    if (torrcBridges.length == 0) {
+      this._bridgeStrings = [];
+      this._bridgeSource = TorBridgeSource.NONE;
+      return;
+    }
+
+    let bridgedbBridges = this._readBridgeDBBridges();
+
+    // if these two lists are equal then we got our bridges from bridgedb
+    // ie: same element in identical order
+    let arraysEqual = (left, right) => {
+      if (left.length != right.length) {
+        return false;
+      }
+      const length = left.length;
+      for (let i = 0; i < length; ++i) {
+        if (left[i] != right[i]) {
+          return false;
+        }
+      }
+      return true;
+    };
+
+    // agreement between prefs and torrc means bridgedb bridges
+    if (arraysEqual(torrcBridges, bridgedbBridges)) {
+      this._bridgeStrings = torrcBridges;
+      this._bridgeSource = TorBridgeSource.BRIDGEDB;
+      return;
+    }
+
+    // otherwise they must be user provided
+    this._bridgeStrings = torrcBridges;
+    this._bridgeSource = TorBridgeSource.USERPROVIDED;
+  }
+
+  writeSettings() {
+    let settingsObject = new Map();
+
+    // init tor bridge settings to null
+    settingsObject.set(TorStrings.configKeys.useBridges, null);
+    settingsObject.set(TorStrings.configKeys.bridgeList, null);
+
+    // clear bridge related firefox prefs
+    Services.prefs.setCharPref(TorStrings.preferenceKeys.defaultBridgeType, "");
+    let bridgeBranch = Services.prefs.getBranch(
+      `${TorStrings.preferenceBranches.bridgeDBBridges}`
+    );
+    let bridgeBranchPrefs = bridgeBranch.getChildList("", {});
+    for (const pref of bridgeBranchPrefs) {
+      Services.prefs.clearUserPref(
+        `${TorStrings.preferenceBranches.bridgeDBBridges}${pref}`
+      );
+    }
+
+    switch (this._bridgeSource) {
+      case TorBridgeSource.BUILTIN:
+        // set builtin bridge type to use in prefs
+        Services.prefs.setCharPref(
+          TorStrings.preferenceKeys.defaultBridgeType,
+          this._selectedDefaultBridgeType
+        );
+        break;
+      case TorBridgeSource.BRIDGEDB:
+        // save bridges off to prefs
+        for (let i = 0; i < this.bridgeStringsArray.length; ++i) {
+          Services.prefs.setCharPref(
+            `${TorStrings.preferenceBranches.bridgeDBBridges}${i}`,
+            this.bridgeStringsArray[i]
+          );
+        }
+        break;
+    }
+
+    // write over our bridge list if bridges are enabled
+    if (this._bridgeSource != TorBridgeSource.NONE) {
+      settingsObject.set(TorStrings.configKeys.useBridges, true);
+      settingsObject.set(
+        TorStrings.configKeys.bridgeList,
+        this.bridgeStringsArray
+      );
+    }
+    TorProtocolService.writeSettings(settingsObject);
+  }
+}
+
+function makeTorBridgeSettingsNone() {
+  return new TorBridgeSettings();
+}
+
+function makeTorBridgeSettingsBuiltin(aBridgeType) {
+  let retval = new TorBridgeSettings();
+  retval._bridgeSource = TorBridgeSource.BUILTIN;
+  retval._selectedDefaultBridgeType = aBridgeType;
+  retval._bridgeStrings = retval._readDefaultBridges(aBridgeType);
+
+  return retval;
+}
+
+function makeTorBridgeSettingsBridgeDB(aBridges) {
+  let retval = new TorBridgeSettings();
+  retval._bridgeSource = TorBridgeSource.BRIDGEDB;
+  retval._selectedDefaultBridgeType = null;
+  retval._bridgeStrings = aBridges;
+
+  return retval;
+}
+
+function makeTorBridgeSettingsUserProvided(aBridges) {
+  let retval = new TorBridgeSettings();
+  retval._bridgeSource = TorBridgeSource.USERPROVIDED;
+  retval._selectedDefaultBridgeType = null;
+  retval._bridgeStrings = aBridges;
+
+  return retval;
+}
diff --git a/browser/components/torpreferences/content/torCategory.inc.xhtml b/browser/components/torpreferences/content/torCategory.inc.xhtml
new file mode 100644
index 000000000000..abe56200f571
--- /dev/null
+++ b/browser/components/torpreferences/content/torCategory.inc.xhtml
@@ -0,0 +1,9 @@
+<richlistitem id="category-tor"
+              class="category"
+              value="paneTor"
+              helpTopic="prefs-tor"
+              align="center"
+              hidden="true">
+  <image class="category-icon"/>
+  <label id="torPreferences-labelCategory" class="category-name" flex="1" value="Tor"/>
+</richlistitem>
diff --git a/browser/components/torpreferences/content/torFirewallSettings.jsm b/browser/components/torpreferences/content/torFirewallSettings.jsm
new file mode 100644
index 000000000000..e77f18ef2fae
--- /dev/null
+++ b/browser/components/torpreferences/content/torFirewallSettings.jsm
@@ -0,0 +1,72 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "TorFirewallSettings",
+  "makeTorFirewallSettingsNone",
+  "makeTorFirewallSettingsCustom",
+];
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+const { parseAddrPortList } = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/parseFunctions.jsm"
+);
+
+class TorFirewallSettings {
+  constructor() {
+    this._allowedPorts = [];
+  }
+
+  get portsConfigurationString() {
+    let portStrings = this._allowedPorts.map(port => `*:${port}`);
+    return portStrings.join(",");
+  }
+
+  get commaSeparatedListString() {
+    return this._allowedPorts.join(",");
+  }
+
+  get hasPorts() {
+    return this._allowedPorts.length > 0;
+  }
+
+  readSettings() {
+    let addressPortList = TorProtocolService.readStringSetting(
+      TorStrings.configKeys.reachableAddresses
+    );
+
+    let allowedPorts = [];
+    if (addressPortList) {
+      allowedPorts = parseAddrPortList(addressPortList);
+    }
+    this._allowedPorts = allowedPorts;
+  }
+
+  writeSettings() {
+    let settingsObject = new Map();
+
+    // init to null so Tor daemon resets if no ports
+    settingsObject.set(TorStrings.configKeys.reachableAddresses, null);
+
+    if (this._allowedPorts.length > 0) {
+      settingsObject.set(
+        TorStrings.configKeys.reachableAddresses,
+        this.portsConfigurationString
+      );
+    }
+
+    TorProtocolService.writeSettings(settingsObject);
+  }
+}
+
+function makeTorFirewallSettingsNone() {
+  return new TorFirewallSettings();
+}
+
+function makeTorFirewallSettingsCustom(aPortsList) {
+  let retval = new TorFirewallSettings();
+  retval._allowedPorts = aPortsList;
+  return retval;
+}
diff --git a/browser/components/torpreferences/content/torLogDialog.jsm b/browser/components/torpreferences/content/torLogDialog.jsm
new file mode 100644
index 000000000000..ecc684d878c2
--- /dev/null
+++ b/browser/components/torpreferences/content/torLogDialog.jsm
@@ -0,0 +1,66 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorLogDialog"];
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+class TorLogDialog {
+  constructor() {
+    this._dialog = null;
+    this._logTextarea = null;
+    this._copyLogButton = null;
+  }
+
+  static get selectors() {
+    return {
+      copyLogButton: "extra1",
+      logTextarea: "textarea#torPreferences-torDialog-textarea",
+    };
+  }
+
+  _populateXUL(aDialog) {
+    this._dialog = aDialog;
+    const dialogWin = this._dialog.parentElement;
+    dialogWin.setAttribute("title", TorStrings.settings.torLogDialogTitle);
+
+    this._logTextarea = this._dialog.querySelector(
+      TorLogDialog.selectors.logTextarea
+    );
+
+    this._copyLogButton = this._dialog.getButton(
+      TorLogDialog.selectors.copyLogButton
+    );
+    this._copyLogButton.setAttribute("label", TorStrings.settings.copyLog);
+    this._copyLogButton.addEventListener("command", () => {
+      this.copyTorLog();
+    });
+
+    this._logTextarea.value = TorProtocolService.getLog();
+  }
+
+  init(window, aDialog) {
+    // defer to later until firefox has populated the dialog with all our elements
+    window.setTimeout(() => {
+      this._populateXUL(aDialog);
+    }, 0);
+  }
+
+  copyTorLog() {
+    // Copy tor log messages to the system clipboard.
+    let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+      Ci.nsIClipboardHelper
+    );
+    clipboard.copyString(this._logTextarea.value);
+  }
+
+  openDialog(gSubDialog) {
+    gSubDialog.open(
+      "chrome://browser/content/torpreferences/torLogDialog.xhtml",
+      { features: "resizable=yes" },
+      this
+    );
+  }
+}
diff --git a/browser/components/torpreferences/content/torLogDialog.xhtml b/browser/components/torpreferences/content/torLogDialog.xhtml
new file mode 100644
index 000000000000..9c17f8132978
--- /dev/null
+++ b/browser/components/torpreferences/content/torLogDialog.xhtml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
+
+<window type="child"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml">
+<dialog id="torPreferences-torLog-dialog"
+    buttons="accept,extra1">
+  <html:textarea
+    id="torPreferences-torDialog-textarea"
+    multiline="true"
+    readonly="true"/>
+  <script type="application/javascript"><![CDATA[
+    "use strict";
+
+    let torLogDialog = window.arguments[0];
+    let dialog = document.getElementById("torPreferences-torLog-dialog");
+    torLogDialog.init(window, dialog);
+  ]]></script>
+</dialog>
+</window>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torPane.js b/browser/components/torpreferences/content/torPane.js
new file mode 100644
index 000000000000..49054b5dac6a
--- /dev/null
+++ b/browser/components/torpreferences/content/torPane.js
@@ -0,0 +1,857 @@
+"use strict";
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+
+const {
+  TorBridgeSource,
+  TorBridgeSettings,
+  makeTorBridgeSettingsNone,
+  makeTorBridgeSettingsBuiltin,
+  makeTorBridgeSettingsBridgeDB,
+  makeTorBridgeSettingsUserProvided,
+} = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/torBridgeSettings.jsm"
+);
+
+const {
+  TorProxyType,
+  TorProxySettings,
+  makeTorProxySettingsNone,
+  makeTorProxySettingsSocks4,
+  makeTorProxySettingsSocks5,
+  makeTorProxySettingsHTTPS,
+} = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/torProxySettings.jsm"
+);
+const {
+  TorFirewallSettings,
+  makeTorFirewallSettingsNone,
+  makeTorFirewallSettingsCustom,
+} = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/torFirewallSettings.jsm"
+);
+
+const { TorLogDialog } = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/torLogDialog.jsm"
+);
+
+const { RequestBridgeDialog } = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/requestBridgeDialog.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorStrings",
+  "resource:///modules/TorStrings.jsm"
+);
+
+const { parsePort, parseBridgeStrings, parsePortList } = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/parseFunctions.jsm"
+);
+
+/*
+  Tor Pane
+
+  Code for populating the XUL in about:preferences#tor, handling input events, interfacing with tor-launcher
+*/
+const gTorPane = (function() {
+  /* CSS selectors for all of the Tor Network DOM elements we need to access */
+  const selectors = {
+    category: {
+      title: "label#torPreferences-labelCategory",
+    },
+    torPreferences: {
+      header: "h1#torPreferences-header",
+      description: "span#torPreferences-description",
+      learnMore: "label#torPreferences-learnMore",
+    },
+    bridges: {
+      header: "h2#torPreferences-bridges-header",
+      description: "span#torPreferences-bridges-description",
+      learnMore: "label#torPreferences-bridges-learnMore",
+      useBridgeCheckbox: "checkbox#torPreferences-bridges-toggle",
+      bridgeSelectionRadiogroup:
+        "radiogroup#torPreferences-bridges-bridgeSelection",
+      builtinBridgeOption: "radio#torPreferences-bridges-radioBuiltin",
+      builtinBridgeList: "menulist#torPreferences-bridges-builtinList",
+      requestBridgeOption: "radio#torPreferences-bridges-radioRequestBridge",
+      requestBridgeButton: "button#torPreferences-bridges-buttonRequestBridge",
+      requestBridgeTextarea:
+        "textarea#torPreferences-bridges-textareaRequestBridge",
+      provideBridgeOption: "radio#torPreferences-bridges-radioProvideBridge",
+      provideBridgeDescription:
+        "description#torPreferences-bridges-descriptionProvideBridge",
+      provideBridgeTextarea:
+        "textarea#torPreferences-bridges-textareaProvideBridge",
+    },
+    advanced: {
+      header: "h2#torPreferences-advanced-header",
+      description: "span#torPreferences-advanced-description",
+      learnMore: "label#torPreferences-advanced-learnMore",
+      useProxyCheckbox: "checkbox#torPreferences-advanced-toggleProxy",
+      proxyTypeLabel: "label#torPreferences-localProxy-type",
+      proxyTypeList: "menulist#torPreferences-localProxy-builtinList",
+      proxyAddressLabel: "label#torPreferences-localProxy-address",
+      proxyAddressTextbox: "input#torPreferences-localProxy-textboxAddress",
+      proxyPortLabel: "label#torPreferences-localProxy-port",
+      proxyPortTextbox: "input#torPreferences-localProxy-textboxPort",
+      proxyUsernameLabel: "label#torPreferences-localProxy-username",
+      proxyUsernameTextbox: "input#torPreferences-localProxy-textboxUsername",
+      proxyPasswordLabel: "label#torPreferences-localProxy-password",
+      proxyPasswordTextbox: "input#torPreferences-localProxy-textboxPassword",
+      useFirewallCheckbox: "checkbox#torPreferences-advanced-toggleFirewall",
+      firewallAllowedPortsLabel: "label#torPreferences-advanced-allowedPorts",
+      firewallAllowedPortsTextbox:
+        "input#torPreferences-advanced-textboxAllowedPorts",
+      torLogsLabel: "label#torPreferences-torLogs",
+      torLogsButton: "button#torPreferences-buttonTorLogs",
+    },
+  }; /* selectors */
+
+  let retval = {
+    // cached frequently accessed DOM elements
+    _useBridgeCheckbox: null,
+    _bridgeSelectionRadiogroup: null,
+    _builtinBridgeOption: null,
+    _builtinBridgeMenulist: null,
+    _requestBridgeOption: null,
+    _requestBridgeButton: null,
+    _requestBridgeTextarea: null,
+    _provideBridgeOption: null,
+    _provideBridgeTextarea: null,
+    _useProxyCheckbox: null,
+    _proxyTypeLabel: null,
+    _proxyTypeMenulist: null,
+    _proxyAddressLabel: null,
+    _proxyAddressTextbox: null,
+    _proxyPortLabel: null,
+    _proxyPortTextbox: null,
+    _proxyUsernameLabel: null,
+    _proxyUsernameTextbox: null,
+    _proxyPasswordLabel: null,
+    _proxyPasswordTextbox: null,
+    _useFirewallCheckbox: null,
+    _allowedPortsLabel: null,
+    _allowedPortsTextbox: null,
+
+    // tor network settings
+    _bridgeSettings: null,
+    _proxySettings: null,
+    _firewallSettings: null,
+
+    // disables the provided list of elements
+    _setElementsDisabled(elements, disabled) {
+      for (let currentElement of elements) {
+        currentElement.disabled = disabled;
+      }
+    },
+
+    // populate xul with strings and cache the relevant elements
+    _populateXUL() {
+      // saves tor settings to disk when navigate away from about:preferences
+      window.addEventListener("blur", val => {
+        TorProtocolService.flushSettings();
+      });
+
+      document
+        .querySelector(selectors.category.title)
+        .setAttribute("value", TorStrings.settings.categoryTitle);
+
+      let prefpane = document.getElementById("mainPrefPane");
+
+      // Heading
+      prefpane.querySelector(selectors.torPreferences.header).innerText =
+        TorStrings.settings.torPreferencesHeading;
+      prefpane.querySelector(selectors.torPreferences.description).textContent =
+        TorStrings.settings.torPreferencesDescription;
+      {
+        let learnMore = prefpane.querySelector(
+          selectors.torPreferences.learnMore
+        );
+        learnMore.setAttribute("value", TorStrings.settings.learnMore);
+        learnMore.setAttribute(
+          "href",
+          TorStrings.settings.learnMoreTorBrowserURL
+        );
+      }
+
+      // Bridge setup
+      prefpane.querySelector(selectors.bridges.header).innerText =
+        TorStrings.settings.bridgesHeading;
+      prefpane.querySelector(selectors.bridges.description).textContent =
+        TorStrings.settings.bridgesDescription;
+      {
+        let learnMore = prefpane.querySelector(selectors.bridges.learnMore);
+        learnMore.setAttribute("value", TorStrings.settings.learnMore);
+        learnMore.setAttribute("href", TorStrings.settings.learnMoreBridgesURL);
+      }
+
+      this._useBridgeCheckbox = prefpane.querySelector(
+        selectors.bridges.useBridgeCheckbox
+      );
+      this._useBridgeCheckbox.setAttribute(
+        "label",
+        TorStrings.settings.useBridge
+      );
+      this._useBridgeCheckbox.addEventListener("command", e => {
+        const checked = this._useBridgeCheckbox.checked;
+        gTorPane.onToggleBridge(checked).onUpdateBridgeSettings();
+      });
+      this._bridgeSelectionRadiogroup = prefpane.querySelector(
+        selectors.bridges.bridgeSelectionRadiogroup
+      );
+      this._bridgeSelectionRadiogroup.value = TorBridgeSource.BUILTIN;
+      this._bridgeSelectionRadiogroup.addEventListener("command", e => {
+        const value = this._bridgeSelectionRadiogroup.value;
+        gTorPane.onSelectBridgeOption(value).onUpdateBridgeSettings();
+      });
+
+      // Builtin bridges
+      this._builtinBridgeOption = prefpane.querySelector(
+        selectors.bridges.builtinBridgeOption
+      );
+      this._builtinBridgeOption.setAttribute(
+        "label",
+        TorStrings.settings.selectBridge
+      );
+      this._builtinBridgeOption.setAttribute("value", TorBridgeSource.BUILTIN);
+      this._builtinBridgeMenulist = prefpane.querySelector(
+        selectors.bridges.builtinBridgeList
+      );
+      this._builtinBridgeMenulist.addEventListener("command", e => {
+        gTorPane.onUpdateBridgeSettings();
+      });
+
+      // Request bridge
+      this._requestBridgeOption = prefpane.querySelector(
+        selectors.bridges.requestBridgeOption
+      );
+      this._requestBridgeOption.setAttribute(
+        "label",
+        TorStrings.settings.requestBridgeFromTorProject
+      );
+      this._requestBridgeOption.setAttribute("value", TorBridgeSource.BRIDGEDB);
+      this._requestBridgeButton = prefpane.querySelector(
+        selectors.bridges.requestBridgeButton
+      );
+      this._requestBridgeButton.setAttribute(
+        "label",
+        TorStrings.settings.requestNewBridge
+      );
+      this._requestBridgeButton.addEventListener("command", () =>
+        gTorPane.onRequestBridge()
+      );
+      this._requestBridgeTextarea = prefpane.querySelector(
+        selectors.bridges.requestBridgeTextarea
+      );
+
+      // Provide a bridge
+      this._provideBridgeOption = prefpane.querySelector(
+        selectors.bridges.provideBridgeOption
+      );
+      this._provideBridgeOption.setAttribute(
+        "label",
+        TorStrings.settings.provideBridge
+      );
+      this._provideBridgeOption.setAttribute(
+        "value",
+        TorBridgeSource.USERPROVIDED
+      );
+      prefpane.querySelector(
+        selectors.bridges.provideBridgeDescription
+      ).textContent = TorStrings.settings.provideBridgeDirections;
+      this._provideBridgeTextarea = prefpane.querySelector(
+        selectors.bridges.provideBridgeTextarea
+      );
+      this._provideBridgeTextarea.setAttribute(
+        "placeholder",
+        TorStrings.settings.provideBridgePlaceholder
+      );
+      this._provideBridgeTextarea.addEventListener("blur", () => {
+        gTorPane.onUpdateBridgeSettings();
+      });
+
+      // Advanced setup
+      prefpane.querySelector(selectors.advanced.header).innerText =
+        TorStrings.settings.advancedHeading;
+      prefpane.querySelector(selectors.advanced.description).textContent =
+        TorStrings.settings.advancedDescription;
+      {
+        let learnMore = prefpane.querySelector(selectors.advanced.learnMore);
+        learnMore.setAttribute("value", TorStrings.settings.learnMore);
+        learnMore.setAttribute(
+          "href",
+          TorStrings.settings.learnMoreNetworkSettingsURL
+        );
+      }
+
+      // Local Proxy
+      this._useProxyCheckbox = prefpane.querySelector(
+        selectors.advanced.useProxyCheckbox
+      );
+      this._useProxyCheckbox.setAttribute(
+        "label",
+        TorStrings.settings.useLocalProxy
+      );
+      this._useProxyCheckbox.addEventListener("command", e => {
+        const checked = this._useProxyCheckbox.checked;
+        gTorPane.onToggleProxy(checked).onUpdateProxySettings();
+      });
+      this._proxyTypeLabel = prefpane.querySelector(
+        selectors.advanced.proxyTypeLabel
+      );
+      this._proxyTypeLabel.setAttribute("value", TorStrings.settings.proxyType);
+
+      let mockProxies = [
+        {
+          value: TorProxyType.SOCKS4,
+          label: TorStrings.settings.proxyTypeSOCKS4,
+        },
+        {
+          value: TorProxyType.SOCKS5,
+          label: TorStrings.settings.proxyTypeSOCKS5,
+        },
+        { value: TorProxyType.HTTPS, label: TorStrings.settings.proxyTypeHTTP },
+      ];
+      this._proxyTypeMenulist = prefpane.querySelector(
+        selectors.advanced.proxyTypeList
+      );
+      this._proxyTypeMenulist.addEventListener("command", e => {
+        const value = this._proxyTypeMenulist.value;
+        gTorPane.onSelectProxyType(value).onUpdateProxySettings();
+      });
+      for (let currentProxy of mockProxies) {
+        let menuEntry = document.createXULElement("menuitem");
+        menuEntry.setAttribute("value", currentProxy.value);
+        menuEntry.setAttribute("label", currentProxy.label);
+        this._proxyTypeMenulist
+          .querySelector("menupopup")
+          .appendChild(menuEntry);
+      }
+
+      this._proxyAddressLabel = prefpane.querySelector(
+        selectors.advanced.proxyAddressLabel
+      );
+      this._proxyAddressLabel.setAttribute(
+        "value",
+        TorStrings.settings.proxyAddress
+      );
+      this._proxyAddressTextbox = prefpane.querySelector(
+        selectors.advanced.proxyAddressTextbox
+      );
+      this._proxyAddressTextbox.setAttribute(
+        "placeholder",
+        TorStrings.settings.proxyAddressPlaceholder
+      );
+      this._proxyAddressTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateProxySettings();
+      });
+      this._proxyPortLabel = prefpane.querySelector(
+        selectors.advanced.proxyPortLabel
+      );
+      this._proxyPortLabel.setAttribute("value", TorStrings.settings.proxyPort);
+      this._proxyPortTextbox = prefpane.querySelector(
+        selectors.advanced.proxyPortTextbox
+      );
+      this._proxyPortTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateProxySettings();
+      });
+      this._proxyUsernameLabel = prefpane.querySelector(
+        selectors.advanced.proxyUsernameLabel
+      );
+      this._proxyUsernameLabel.setAttribute(
+        "value",
+        TorStrings.settings.proxyUsername
+      );
+      this._proxyUsernameTextbox = prefpane.querySelector(
+        selectors.advanced.proxyUsernameTextbox
+      );
+      this._proxyUsernameTextbox.setAttribute(
+        "placeholder",
+        TorStrings.settings.proxyUsernamePasswordPlaceholder
+      );
+      this._proxyUsernameTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateProxySettings();
+      });
+      this._proxyPasswordLabel = prefpane.querySelector(
+        selectors.advanced.proxyPasswordLabel
+      );
+      this._proxyPasswordLabel.setAttribute(
+        "value",
+        TorStrings.settings.proxyPassword
+      );
+      this._proxyPasswordTextbox = prefpane.querySelector(
+        selectors.advanced.proxyPasswordTextbox
+      );
+      this._proxyPasswordTextbox.setAttribute(
+        "placeholder",
+        TorStrings.settings.proxyUsernamePasswordPlaceholder
+      );
+      this._proxyPasswordTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateProxySettings();
+      });
+
+      // Local firewall
+      this._useFirewallCheckbox = prefpane.querySelector(
+        selectors.advanced.useFirewallCheckbox
+      );
+      this._useFirewallCheckbox.setAttribute(
+        "label",
+        TorStrings.settings.useFirewall
+      );
+      this._useFirewallCheckbox.addEventListener("command", e => {
+        const checked = this._useFirewallCheckbox.checked;
+        gTorPane.onToggleFirewall(checked).onUpdateFirewallSettings();
+      });
+      this._allowedPortsLabel = prefpane.querySelector(
+        selectors.advanced.firewallAllowedPortsLabel
+      );
+      this._allowedPortsLabel.setAttribute(
+        "value",
+        TorStrings.settings.allowedPorts
+      );
+      this._allowedPortsTextbox = prefpane.querySelector(
+        selectors.advanced.firewallAllowedPortsTextbox
+      );
+      this._allowedPortsTextbox.setAttribute(
+        "placeholder",
+        TorStrings.settings.allowedPortsPlaceholder
+      );
+      this._allowedPortsTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateFirewallSettings();
+      });
+
+      // Tor logs
+      prefpane
+        .querySelector(selectors.advanced.torLogsLabel)
+        .setAttribute("value", TorStrings.settings.showTorDaemonLogs);
+      let torLogsButton = prefpane.querySelector(
+        selectors.advanced.torLogsButton
+      );
+      torLogsButton.setAttribute("label", TorStrings.settings.showLogs);
+      torLogsButton.addEventListener("command", () => {
+        gTorPane.onViewTorLogs();
+      });
+
+      // Disable all relevant elements by default
+      this._setElementsDisabled(
+        [
+          this._builtinBridgeOption,
+          this._builtinBridgeMenulist,
+          this._requestBridgeOption,
+          this._requestBridgeButton,
+          this._requestBridgeTextarea,
+          this._provideBridgeOption,
+          this._provideBridgeTextarea,
+          this._proxyTypeLabel,
+          this._proxyTypeMenulist,
+          this._proxyAddressLabel,
+          this._proxyAddressTextbox,
+          this._proxyPortLabel,
+          this._proxyPortTextbox,
+          this._proxyUsernameLabel,
+          this._proxyUsernameTextbox,
+          this._proxyPasswordLabel,
+          this._proxyPasswordTextbox,
+          this._allowedPortsLabel,
+          this._allowedPortsTextbox,
+        ],
+        true
+      );
+
+      // load bridge settings
+      let torBridgeSettings = new TorBridgeSettings();
+      torBridgeSettings.readSettings();
+
+      // populate the bridge list
+      for (let currentBridge of TorBridgeSettings.defaultBridgeTypes) {
+        let menuEntry = document.createXULElement("menuitem");
+        menuEntry.setAttribute("value", currentBridge);
+        menuEntry.setAttribute("label", currentBridge);
+        this._builtinBridgeMenulist
+          .querySelector("menupopup")
+          .appendChild(menuEntry);
+      }
+
+      this.onSelectBridgeOption(torBridgeSettings.bridgeSource);
+      this.onToggleBridge(
+        torBridgeSettings.bridgeSource != TorBridgeSource.NONE
+      );
+      switch (torBridgeSettings.bridgeSource) {
+        case TorBridgeSource.NONE:
+          break;
+        case TorBridgeSource.BUILTIN:
+          this._builtinBridgeMenulist.value =
+            torBridgeSettings.selectedDefaultBridgeType;
+          break;
+        case TorBridgeSource.BRIDGEDB:
+          this._requestBridgeTextarea.value = torBridgeSettings.bridgeStrings;
+          break;
+        case TorBridgeSource.USERPROVIDED:
+          this._provideBridgeTextarea.value = torBridgeSettings.bridgeStrings;
+          break;
+      }
+
+      this._bridgeSettings = torBridgeSettings;
+
+      // load proxy settings
+      let torProxySettings = new TorProxySettings();
+      torProxySettings.readSettings();
+
+      if (torProxySettings.type != TorProxyType.NONE) {
+        this.onToggleProxy(true);
+        this.onSelectProxyType(torProxySettings.type);
+        this._proxyAddressTextbox.value = torProxySettings.address;
+        this._proxyPortTextbox.value = torProxySettings.port;
+        this._proxyUsernameTextbox.value = torProxySettings.username;
+        this._proxyPasswordTextbox.value = torProxySettings.password;
+      }
+
+      this._proxySettings = torProxySettings;
+
+      // load firewall settings
+      let torFirewallSettings = new TorFirewallSettings();
+      torFirewallSettings.readSettings();
+
+      if (torFirewallSettings.hasPorts) {
+        this.onToggleFirewall(true);
+        this._allowedPortsTextbox.value =
+          torFirewallSettings.commaSeparatedListString;
+      }
+
+      this._firewallSettings = torFirewallSettings;
+    },
+
+    init() {
+      this._populateXUL();
+    },
+
+    // whether the page should be present in about:preferences
+    get enabled() {
+      return TorProtocolService.ownsTorDaemon;
+    },
+
+    //
+    // Callbacks
+    //
+
+    // callback when using bridges toggled
+    onToggleBridge(enabled) {
+      this._useBridgeCheckbox.checked = enabled;
+      let disabled = !enabled;
+
+      // first disable all the bridge related elements
+      this._setElementsDisabled(
+        [
+          this._builtinBridgeOption,
+          this._builtinBridgeMenulist,
+          this._requestBridgeOption,
+          this._requestBridgeButton,
+          this._requestBridgeTextarea,
+          this._provideBridgeOption,
+          this._provideBridgeTextarea,
+        ],
+        disabled
+      );
+
+      // and selectively re-enable based on the radiogroup's current value
+      if (enabled) {
+        this.onSelectBridgeOption(this._bridgeSelectionRadiogroup.value);
+      } else {
+        this.onSelectBridgeOption(TorBridgeSource.NONE);
+      }
+      return this;
+    },
+
+    // callback when a bridge option is selected
+    onSelectBridgeOption(source) {
+      // disable all of the bridge elements under radio buttons
+      this._setElementsDisabled(
+        [
+          this._builtinBridgeMenulist,
+          this._requestBridgeButton,
+          this._requestBridgeTextarea,
+          this._provideBridgeTextarea,
+        ],
+        true
+      );
+
+      if (source != TorBridgeSource.NONE) {
+        this._bridgeSelectionRadiogroup.value = source;
+      }
+
+      switch (source) {
+        case TorBridgeSource.BUILTIN: {
+          this._setElementsDisabled([this._builtinBridgeMenulist], false);
+          break;
+        }
+        case TorBridgeSource.BRIDGEDB: {
+          this._setElementsDisabled(
+            [this._requestBridgeButton, this._requestBridgeTextarea],
+            false
+          );
+          break;
+        }
+        case TorBridgeSource.USERPROVIDED: {
+          this._setElementsDisabled([this._provideBridgeTextarea], false);
+          break;
+        }
+      }
+      return this;
+    },
+
+    // called when the request bridge button is activated
+    onRequestBridge() {
+      let requestBridgeDialog = new RequestBridgeDialog();
+      requestBridgeDialog.openDialog(
+        gSubDialog,
+        this._proxySettings.proxyURI,
+        aBridges => {
+          if (aBridges.length > 0) {
+            let bridgeSettings = makeTorBridgeSettingsBridgeDB(aBridges);
+            bridgeSettings.writeSettings();
+            this._bridgeSettings = bridgeSettings;
+
+            this._requestBridgeTextarea.value = bridgeSettings.bridgeStrings;
+          }
+        }
+      );
+      return this;
+    },
+
+    // pushes bridge settings from UI to tor
+    onUpdateBridgeSettings() {
+      let bridgeSettings = null;
+
+      let source = this._useBridgeCheckbox.checked
+        ? this._bridgeSelectionRadiogroup.value
+        : TorBridgeSource.NONE;
+      switch (source) {
+        case TorBridgeSource.NONE: {
+          bridgeSettings = makeTorBridgeSettingsNone();
+          break;
+        }
+        case TorBridgeSource.BUILTIN: {
+          // if there is a built-in bridge already selected, use that
+          let bridgeType = this._builtinBridgeMenulist.value;
+          if (bridgeType) {
+            bridgeSettings = makeTorBridgeSettingsBuiltin(bridgeType);
+          } else {
+            bridgeSettings = makeTorBridgeSettingsNone();
+          }
+          break;
+        }
+        case TorBridgeSource.BRIDGEDB: {
+          // if there are bridgedb bridges saved in the text area, use them
+          let bridgeStrings = this._requestBridgeTextarea.value;
+          if (bridgeStrings) {
+            let bridgeStringList = parseBridgeStrings(bridgeStrings);
+            bridgeSettings = makeTorBridgeSettingsBridgeDB(bridgeStringList);
+          } else {
+            bridgeSettings = makeTorBridgeSettingsNone();
+          }
+          break;
+        }
+        case TorBridgeSource.USERPROVIDED: {
+          // if bridges already exist in the text area, use them
+          let bridgeStrings = this._provideBridgeTextarea.value;
+          if (bridgeStrings) {
+            let bridgeStringList = parseBridgeStrings(bridgeStrings);
+            bridgeSettings = makeTorBridgeSettingsUserProvided(
+              bridgeStringList
+            );
+          } else {
+            bridgeSettings = makeTorBridgeSettingsNone();
+          }
+          break;
+        }
+      }
+      bridgeSettings.writeSettings();
+      this._bridgeSettings = bridgeSettings;
+      return this;
+    },
+
+    // callback when proxy is toggled
+    onToggleProxy(enabled) {
+      this._useProxyCheckbox.checked = enabled;
+      let disabled = !enabled;
+
+      this._setElementsDisabled(
+        [
+          this._proxyTypeLabel,
+          this._proxyTypeMenulist,
+          this._proxyAddressLabel,
+          this._proxyAddressTextbox,
+          this._proxyPortLabel,
+          this._proxyPortTextbox,
+          this._proxyUsernameLabel,
+          this._proxyUsernameTextbox,
+          this._proxyPasswordLabel,
+          this._proxyPasswordTextbox,
+        ],
+        disabled
+      );
+      this.onSelectProxyType(this._proxyTypeMenulist.value);
+      return this;
+    },
+
+    // callback when proxy type is changed
+    onSelectProxyType(value) {
+      if (value == "") {
+        value = TorProxyType.NONE;
+      }
+      this._proxyTypeMenulist.value = value;
+      switch (value) {
+        case TorProxyType.NONE: {
+          this._setElementsDisabled(
+            [
+              this._proxyAddressLabel,
+              this._proxyAddressTextbox,
+              this._proxyPortLabel,
+              this._proxyPortTextbox,
+              this._proxyUsernameLabel,
+              this._proxyUsernameTextbox,
+              this._proxyPasswordLabel,
+              this._proxyPasswordTextbox,
+            ],
+            true
+          ); // DISABLE
+
+          this._proxyAddressTextbox.value = "";
+          this._proxyPortTextbox.value = "";
+          this._proxyUsernameTextbox.value = "";
+          this._proxyPasswordTextbox.value = "";
+          break;
+        }
+        case TorProxyType.SOCKS4: {
+          this._setElementsDisabled(
+            [
+              this._proxyAddressLabel,
+              this._proxyAddressTextbox,
+              this._proxyPortLabel,
+              this._proxyPortTextbox,
+            ],
+            false
+          ); // ENABLE
+          this._setElementsDisabled(
+            [
+              this._proxyUsernameLabel,
+              this._proxyUsernameTextbox,
+              this._proxyPasswordLabel,
+              this._proxyPasswordTextbox,
+            ],
+            true
+          ); // DISABLE
+
+          this._proxyUsernameTextbox.value = "";
+          this._proxyPasswordTextbox.value = "";
+          break;
+        }
+        case TorProxyType.SOCKS5:
+        case TorProxyType.HTTPS: {
+          this._setElementsDisabled(
+            [
+              this._proxyAddressLabel,
+              this._proxyAddressTextbox,
+              this._proxyPortLabel,
+              this._proxyPortTextbox,
+              this._proxyUsernameLabel,
+              this._proxyUsernameTextbox,
+              this._proxyPasswordLabel,
+              this._proxyPasswordTextbox,
+            ],
+            false
+          ); // ENABLE
+          break;
+        }
+      }
+      return this;
+    },
+
+    // pushes proxy settings from UI to tor
+    onUpdateProxySettings() {
+      const proxyType = this._useProxyCheckbox.checked
+        ? this._proxyTypeMenulist.value
+        : TorProxyType.NONE;
+      const addressString = this._proxyAddressTextbox.value;
+      const portString = this._proxyPortTextbox.value;
+      const usernameString = this._proxyUsernameTextbox.value;
+      const passwordString = this._proxyPasswordTextbox.value;
+
+      let proxySettings = null;
+
+      switch (proxyType) {
+        case TorProxyType.NONE:
+          proxySettings = makeTorProxySettingsNone();
+          break;
+        case TorProxyType.SOCKS4:
+          proxySettings = makeTorProxySettingsSocks4(
+            addressString,
+            parsePort(portString)
+          );
+          break;
+        case TorProxyType.SOCKS5:
+          proxySettings = makeTorProxySettingsSocks5(
+            addressString,
+            parsePort(portString),
+            usernameString,
+            passwordString
+          );
+          break;
+        case TorProxyType.HTTPS:
+          proxySettings = makeTorProxySettingsHTTPS(
+            addressString,
+            parsePort(portString),
+            usernameString,
+            passwordString
+          );
+          break;
+      }
+
+      proxySettings.writeSettings();
+      this._proxySettings = proxySettings;
+      return this;
+    },
+
+    // callback when firewall proxy is toggled
+    onToggleFirewall(enabled) {
+      this._useFirewallCheckbox.checked = enabled;
+      let disabled = !enabled;
+
+      this._setElementsDisabled(
+        [this._allowedPortsLabel, this._allowedPortsTextbox],
+        disabled
+      );
+
+      return this;
+    },
+
+    // pushes firewall settings from UI to tor
+    onUpdateFirewallSettings() {
+      let portListString = this._useFirewallCheckbox.checked
+        ? this._allowedPortsTextbox.value
+        : "";
+      let firewallSettings = null;
+
+      if (portListString) {
+        firewallSettings = makeTorFirewallSettingsCustom(
+          parsePortList(portListString)
+        );
+      } else {
+        firewallSettings = makeTorFirewallSettingsNone();
+      }
+
+      firewallSettings.writeSettings();
+      this._firewallSettings = firewallSettings;
+      return this;
+    },
+
+    onViewTorLogs() {
+      let torLogDialog = new TorLogDialog();
+      torLogDialog.openDialog(gSubDialog);
+    },
+  };
+  return retval;
+})(); /* gTorPane */
diff --git a/browser/components/torpreferences/content/torPane.xhtml b/browser/components/torpreferences/content/torPane.xhtml
new file mode 100644
index 000000000000..3c966b2b3726
--- /dev/null
+++ b/browser/components/torpreferences/content/torPane.xhtml
@@ -0,0 +1,123 @@
+<!-- Tor panel -->
+
+<script type="application/javascript"
+        src="chrome://browser/content/torpreferences/torPane.js"/>
+<html:template id="template-paneTor">
+<hbox id="torPreferencesCategory"
+      class="subcategory"
+      data-category="paneTor"
+      hidden="true">
+  <html:h1 id="torPreferences-header"/>
+</hbox>
+
+<groupbox data-category="paneTor"
+          hidden="true">
+  <description flex="1">
+    <html:span id="torPreferences-description" class="tail-with-learn-more"/>
+    <label id="torPreferences-learnMore" class="learnMore text-link" is="text-link"/>
+  </description>
+</groupbox>
+
+<!-- Bridges -->
+<groupbox id="torPreferences-bridges-group"
+          data-category="paneTor"
+          hidden="true">
+  <html:h2 id="torPreferences-bridges-header"/>
+  <description flex="1">
+    <html:span id="torPreferences-bridges-description" class="tail-with-learn-more"/>
+    <label id="torPreferences-bridges-learnMore" class="learnMore text-link" is="text-link"/>
+  </description>
+  <checkbox id="torPreferences-bridges-toggle"/>
+  <radiogroup id="torPreferences-bridges-bridgeSelection">
+    <hbox class="indent">
+      <radio id="torPreferences-bridges-radioBuiltin"/>
+      <spacer flex="1"/>
+      <menulist id="torPreferences-bridges-builtinList" class="torMarginFix">
+        <menupopup/>
+      </menulist>
+    </hbox>
+    <vbox class="indent">
+      <hbox>
+        <radio id="torPreferences-bridges-radioRequestBridge"/>
+        <space flex="1"/>
+        <button id="torPreferences-bridges-buttonRequestBridge" class="torMarginFix"/>
+      </hbox>
+      <html:textarea
+        id="torPreferences-bridges-textareaRequestBridge"
+        class="indent torMarginFix"
+        multiline="true"
+        rows="3"
+        readonly="true"/>
+    </vbox>
+    <hbox class="indent" flex="1">
+      <vbox flex="1">
+        <radio id="torPreferences-bridges-radioProvideBridge"/>
+        <description id="torPreferences-bridges-descriptionProvideBridge" class="indent"/>
+        <html:textarea
+          id="torPreferences-bridges-textareaProvideBridge"
+          class="indent torMarginFix"
+          multiline="true"
+          rows="3"/>
+      </vbox>
+    </hbox>
+  </radiogroup>
+</groupbox>
+
+<!-- Advanced -->
+<groupbox id="torPreferences-advanced-group"
+          data-category="paneTor"
+          hidden="true">
+  <html:h2 id="torPreferences-advanced-header"/>
+  <description flex="1">
+    <html:span id="torPreferences-advanced-description" class="tail-with-learn-more"/>
+    <label id="torPreferences-advanced-learnMore" class="learnMore text-link" is="text-link" style="display:none"/>
+  </description>
+  <box id="torPreferences-advanced-grid">
+    <!-- Local Proxy -->
+    <hbox class="torPreferences-advanced-checkbox-container">
+      <checkbox id="torPreferences-advanced-toggleProxy"/>
+    </hbox>
+    <hbox class="indent" align="center">
+      <label id="torPreferences-localProxy-type"/>
+    </hbox>
+    <hbox align="center">
+      <spacer flex="1"/>
+      <menulist id="torPreferences-localProxy-builtinList" class="torMarginFix">
+        <menupopup/>
+      </menulist>
+    </hbox>
+    <hbox class="indent" align="center">
+      <label id="torPreferences-localProxy-address"/>
+    </hbox>
+    <hbox align="center">
+      <html:input id="torPreferences-localProxy-textboxAddress" type="text" class="torMarginFix"/>
+      <label id="torPreferences-localProxy-port"/>
+      <!-- proxy-port-input class style pulled from preferences.css and used in the vanilla proxy setup menu -->
+      <html:input id="torPreferences-localProxy-textboxPort" class="proxy-port-input torMarginFix" hidespinbuttons="true" type="number" min="0" max="65535" maxlength="5"/>
+    </hbox>
+    <hbox class="indent" align="center">
+      <label id="torPreferences-localProxy-username"/>
+    </hbox>
+    <hbox align="center">
+      <html:input id="torPreferences-localProxy-textboxUsername" type="text" class="torMarginFix"/>
+      <label id="torPreferences-localProxy-password"/>
+      <html:input id="torPreferences-localProxy-textboxPassword" class="torMarginFix" type="password"/>
+    </hbox>
+    <!-- Firewall -->
+    <hbox class="torPreferences-advanced-checkbox-container">
+      <checkbox id="torPreferences-advanced-toggleFirewall"/>
+    </hbox>
+    <hbox class="indent" align="center">
+      <label id="torPreferences-advanced-allowedPorts"/>
+    </hbox>
+    <hbox align="center">
+      <html:input id="torPreferences-advanced-textboxAllowedPorts" type="text" class="torMarginFix" value="80,443"/>
+    </hbox>
+  </box>
+  <hbox id="torPreferences-torDaemon-hbox" align="center">
+    <label id="torPreferences-torLogs"/>
+    <spacer flex="1"/>
+    <button id="torPreferences-buttonTorLogs" class="torMarginFix"/>
+  </hbox>
+</groupbox>
+</html:template>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torPreferences.css b/browser/components/torpreferences/content/torPreferences.css
new file mode 100644
index 000000000000..4dac2c457823
--- /dev/null
+++ b/browser/components/torpreferences/content/torPreferences.css
@@ -0,0 +1,77 @@
+#category-tor > .category-icon {
+  list-style-image: url("chrome://browser/content/torpreferences/torPreferencesIcon.svg");
+}
+
+#torPreferences-advanced-grid {
+  display: grid;
+  grid-template-columns: auto 1fr;
+}
+
+.torPreferences-advanced-checkbox-container {
+  grid-column: 1 / 3;
+}
+
+#torPreferences-localProxy-textboxAddress,
+#torPreferences-localProxy-textboxUsername,
+#torPreferences-localProxy-textboxPassword,
+#torPreferences-advanced-textboxAllowedPorts {
+  -moz-box-flex: 1;
+}
+
+hbox#torPreferences-torDaemon-hbox {
+  margin-top: 20px;
+}
+
+description#torPreferences-requestBridge-description {
+  /*margin-bottom: 1em;*/
+  min-height: 2em;
+}
+
+image#torPreferences-requestBridge-captchaImage {
+  margin: 1em;
+  min-height: 125px;
+}
+
+button#torPreferences-requestBridge-refreshCaptchaButton {
+  min-width: initial;
+}
+
+dialog#torPreferences-requestBridge-dialog > hbox {
+  margin-bottom: 1em;
+}
+
+/*
+ Various elements that really should be lining up don't because they have inconsistent margins
+*/
+.torMarginFix {
+  margin-left : 4px;
+  margin-right : 4px;
+}
+
+/*
+  This hbox is hidden by css here by default so that the
+  xul dialog allocates enough screen space for the error message
+  element, otherwise it gets cut off since dialog's overflow is hidden
+*/
+hbox#torPreferences-requestBridge-incorrectCaptchaHbox {
+  visibility: hidden;
+}
+
+image#torPreferences-requestBridge-errorIcon {
+  list-style-image: url("chrome://browser/skin/warning.svg");
+}
+
+groupbox#torPreferences-bridges-group textarea {
+  white-space: pre;
+  overflow: auto;
+}
+
+textarea#torPreferences-torDialog-textarea {
+  -moz-box-flex: 1;
+  font-family: monospace;
+  font-size: 0.8em;
+  white-space: pre;
+  overflow: auto;
+  /* 10 lines */
+  min-height: 20em;
+}
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torPreferencesIcon.svg b/browser/components/torpreferences/content/torPreferencesIcon.svg
new file mode 100644
index 000000000000..d7895f1107c5
--- /dev/null
+++ b/browser/components/torpreferences/content/torPreferencesIcon.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g fill="context-fill" fill-opacity="context-fill-opacity" fill-rule="nonzero">
+    <path d="M12.0246161,21.8174863 L12.0246161,20.3628098 C16.6324777,20.3495038 20.3634751,16.6108555 20.3634751,11.9996673 C20.3634751,7.38881189 16.6324777,3.65016355 12.0246161,3.63685757 L12.0246161,2.18218107 C17.4358264,2.1958197 21.8178189,6.58546322 21.8178189,11.9996673 C21.8178189,17.4142042 17.4358264,21.8041803 12.0246161,21.8174863 L12.0246161,21.8174863 Z M12.0246161,16.7259522 C14.623607,16.7123136 16.7272828,14.6023175 16.7272828,11.9996673 C16.7272828,9.39734991 14.623607,7.28735377 12.0246161,7.27371516 L12.0246161,5.81937131 C15.4272884,5.8326773 18.1819593,8.59400123 18.1819593,11.9996673 C18.1819593,15.4056661 15.4272884,18.1669901 12.0246161,18.1802961 L12.0246161,16.7259522 Z M12.0246161,9.45556355 C13.4187503,9.46886953 14.5454344,10.6022066 14.5454344,11.9996673 C14.5454344,13.3974608 13.4187503,14.5307978 12.0246161,14.5441038 L12.0246161,9.45556355 Z M0,11.9996673 C0,18.6273771 5.37229031,24 12,24 C18.6273771,24 24,18.6273771 24,11.9996673 C24,5.37229031
  18.6273771,0 12,0 C5.37229031,0 0,5.37229031 0,11.9996673 Z"/>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torProxySettings.jsm b/browser/components/torpreferences/content/torProxySettings.jsm
new file mode 100644
index 000000000000..98bb5e8d5cbf
--- /dev/null
+++ b/browser/components/torpreferences/content/torProxySettings.jsm
@@ -0,0 +1,245 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "TorProxyType",
+  "TorProxySettings",
+  "makeTorProxySettingsNone",
+  "makeTorProxySettingsSocks4",
+  "makeTorProxySettingsSocks5",
+  "makeTorProxySettingsHTTPS",
+];
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+const { parseAddrPort, parseUsernamePassword } = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/parseFunctions.jsm"
+);
+
+const TorProxyType = {
+  NONE: "NONE",
+  SOCKS4: "SOCKS4",
+  SOCKS5: "SOCKS5",
+  HTTPS: "HTTPS",
+};
+
+class TorProxySettings {
+  constructor() {
+    this._proxyType = TorProxyType.NONE;
+    this._proxyAddress = undefined;
+    this._proxyPort = undefined;
+    this._proxyUsername = undefined;
+    this._proxyPassword = undefined;
+  }
+
+  get type() {
+    return this._proxyType;
+  }
+  get address() {
+    return this._proxyAddress;
+  }
+  get port() {
+    return this._proxyPort;
+  }
+  get username() {
+    return this._proxyUsername;
+  }
+  get password() {
+    return this._proxyPassword;
+  }
+  get proxyURI() {
+    switch (this._proxyType) {
+      case TorProxyType.SOCKS4:
+        return `socks4a://${this._proxyAddress}:${this._proxyPort}`;
+      case TorProxyType.SOCKS5:
+        if (this._proxyUsername) {
+          return `socks5://${this._proxyUsername}:${this._proxyPassword}@${
+            this._proxyAddress
+          }:${this._proxyPort}`;
+        }
+        return `socks5://${this._proxyAddress}:${this._proxyPort}`;
+      case TorProxyType.HTTPS:
+        if (this._proxyUsername) {
+          return `http://${this._proxyUsername}:${this._proxyPassword}@${
+            this._proxyAddress
+          }:${this._proxyPort}`;
+        }
+        return `http://${this._proxyAddress}:${this._proxyPort}`;
+    }
+    return undefined;
+  }
+
+  // attempts to read proxy settings from Tor daemon
+  readSettings() {
+    // SOCKS4
+    {
+      let addressPort = TorProtocolService.readStringSetting(
+        TorStrings.configKeys.socks4Proxy
+      );
+      if (addressPort) {
+        // address+port
+        let [proxyAddress, proxyPort] = parseAddrPort(addressPort);
+
+        this._proxyType = TorProxyType.SOCKS4;
+        this._proxyAddress = proxyAddress;
+        this._proxyPort = proxyPort;
+        this._proxyUsername = "";
+        this._proxyPassword = "";
+
+        return;
+      }
+    }
+
+    // SOCKS5
+    {
+      let addressPort = TorProtocolService.readStringSetting(
+        TorStrings.configKeys.socks5Proxy
+      );
+
+      if (addressPort) {
+        // address+port
+        let [proxyAddress, proxyPort] = parseAddrPort(addressPort);
+        // username
+        let proxyUsername = TorProtocolService.readStringSetting(
+          TorStrings.configKeys.socks5ProxyUsername
+        );
+        // password
+        let proxyPassword = TorProtocolService.readStringSetting(
+          TorStrings.configKeys.socks5ProxyPassword
+        );
+
+        this._proxyType = TorProxyType.SOCKS5;
+        this._proxyAddress = proxyAddress;
+        this._proxyPort = proxyPort;
+        this._proxyUsername = proxyUsername;
+        this._proxyPassword = proxyPassword;
+
+        return;
+      }
+    }
+
+    // HTTP
+    {
+      let addressPort = TorProtocolService.readStringSetting(
+        TorStrings.configKeys.httpsProxy
+      );
+
+      if (addressPort) {
+        // address+port
+        let [proxyAddress, proxyPort] = parseAddrPort(addressPort);
+
+        // username:password
+        let proxyAuthenticator = TorProtocolService.readStringSetting(
+          TorStrings.configKeys.httpsProxyAuthenticator
+        );
+
+        let [proxyUsername, proxyPassword] = ["", ""];
+        if (proxyAuthenticator) {
+          [proxyUsername, proxyPassword] = parseUsernamePassword(
+            proxyAuthenticator
+          );
+        }
+
+        this._proxyType = TorProxyType.HTTPS;
+        this._proxyAddress = proxyAddress;
+        this._proxyPort = proxyPort;
+        this._proxyUsername = proxyUsername;
+        this._proxyPassword = proxyPassword;
+      }
+    }
+    // no proxy settings
+  } /* TorProxySettings::ReadFromTor() */
+
+  // attempts to write proxy settings to Tor daemon
+  // throws on error
+  writeSettings() {
+    let settingsObject = new Map();
+
+    // init proxy related settings to null so Tor daemon resets them
+    settingsObject.set(TorStrings.configKeys.socks4Proxy, null);
+    settingsObject.set(TorStrings.configKeys.socks5Proxy, null);
+    settingsObject.set(TorStrings.configKeys.socks5ProxyUsername, null);
+    settingsObject.set(TorStrings.configKeys.socks5ProxyPassword, null);
+    settingsObject.set(TorStrings.configKeys.httpsProxy, null);
+    settingsObject.set(TorStrings.configKeys.httpsProxyAuthenticator, null);
+
+    switch (this._proxyType) {
+      case TorProxyType.SOCKS4:
+        settingsObject.set(
+          TorStrings.configKeys.socks4Proxy,
+          `${this._proxyAddress}:${this._proxyPort}`
+        );
+        break;
+      case TorProxyType.SOCKS5:
+        settingsObject.set(
+          TorStrings.configKeys.socks5Proxy,
+          `${this._proxyAddress}:${this._proxyPort}`
+        );
+        settingsObject.set(
+          TorStrings.configKeys.socks5ProxyUsername,
+          this._proxyUsername
+        );
+        settingsObject.set(
+          TorStrings.configKeys.socks5ProxyPassword,
+          this._proxyPassword
+        );
+        break;
+      case TorProxyType.HTTPS:
+        settingsObject.set(
+          TorStrings.configKeys.httpsProxy,
+          `${this._proxyAddress}:${this._proxyPort}`
+        );
+        settingsObject.set(
+          TorStrings.configKeys.httpsProxyAuthenticator,
+          `${this._proxyUsername}:${this._proxyPassword}`
+        );
+        break;
+    }
+
+    TorProtocolService.writeSettings(settingsObject);
+  } /* TorProxySettings::WriteToTor() */
+}
+
+// factory methods for our various supported proxies
+function makeTorProxySettingsNone() {
+  return new TorProxySettings();
+}
+
+function makeTorProxySettingsSocks4(aProxyAddress, aProxyPort) {
+  let retval = new TorProxySettings();
+  retval._proxyType = TorProxyType.SOCKS4;
+  retval._proxyAddress = aProxyAddress;
+  retval._proxyPort = aProxyPort;
+  return retval;
+}
+
+function makeTorProxySettingsSocks5(
+  aProxyAddress,
+  aProxyPort,
+  aProxyUsername,
+  aProxyPassword
+) {
+  let retval = new TorProxySettings();
+  retval._proxyType = TorProxyType.SOCKS5;
+  retval._proxyAddress = aProxyAddress;
+  retval._proxyPort = aProxyPort;
+  retval._proxyUsername = aProxyUsername;
+  retval._proxyPassword = aProxyPassword;
+  return retval;
+}
+
+function makeTorProxySettingsHTTPS(
+  aProxyAddress,
+  aProxyPort,
+  aProxyUsername,
+  aProxyPassword
+) {
+  let retval = new TorProxySettings();
+  retval._proxyType = TorProxyType.HTTPS;
+  retval._proxyAddress = aProxyAddress;
+  retval._proxyPort = aProxyPort;
+  retval._proxyUsername = aProxyUsername;
+  retval._proxyPassword = aProxyPassword;
+  return retval;
+}
diff --git a/browser/components/torpreferences/jar.mn b/browser/components/torpreferences/jar.mn
new file mode 100644
index 000000000000..857bc9ee3eac
--- /dev/null
+++ b/browser/components/torpreferences/jar.mn
@@ -0,0 +1,14 @@
+browser.jar:
+    content/browser/torpreferences/parseFunctions.jsm                (content/parseFunctions.jsm)
+    content/browser/torpreferences/requestBridgeDialog.xhtml           (content/requestBridgeDialog.xhtml)
+    content/browser/torpreferences/requestBridgeDialog.jsm           (content/requestBridgeDialog.jsm)
+    content/browser/torpreferences/torBridgeSettings.jsm             (content/torBridgeSettings.jsm)
+    content/browser/torpreferences/torCategory.inc.xhtml               (content/torCategory.inc.xhtml)
+    content/browser/torpreferences/torFirewallSettings.jsm           (content/torFirewallSettings.jsm)
+    content/browser/torpreferences/torLogDialog.jsm                  (content/torLogDialog.jsm)
+    content/browser/torpreferences/torLogDialog.xhtml                  (content/torLogDialog.xhtml)
+    content/browser/torpreferences/torPane.js                        (content/torPane.js)
+    content/browser/torpreferences/torPane.xhtml                       (content/torPane.xhtml)
+    content/browser/torpreferences/torPreferences.css                (content/torPreferences.css)
+    content/browser/torpreferences/torPreferencesIcon.svg            (content/torPreferencesIcon.svg)
+    content/browser/torpreferences/torProxySettings.jsm              (content/torProxySettings.jsm)
diff --git a/browser/components/torpreferences/moz.build b/browser/components/torpreferences/moz.build
new file mode 100644
index 000000000000..2661ad7cb9f3
--- /dev/null
+++ b/browser/components/torpreferences/moz.build
@@ -0,0 +1 @@
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/browser/modules/BridgeDB.jsm b/browser/modules/BridgeDB.jsm
new file mode 100644
index 000000000000..2caa26b4e2e0
--- /dev/null
+++ b/browser/modules/BridgeDB.jsm
@@ -0,0 +1,110 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["BridgeDB"];
+
+const { TorLauncherBridgeDB } = ChromeUtils.import(
+  "resource://torlauncher/modules/tl-bridgedb.jsm"
+);
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+var BridgeDB = {
+  _moatRequestor: null,
+  _currentCaptchaInfo: null,
+  _bridges: null,
+
+  get currentCaptchaImage() {
+    if (this._currentCaptchaInfo) {
+      return this._currentCaptchaInfo.captchaImage;
+    }
+    return null;
+  },
+
+  get currentBridges() {
+    return this._bridges;
+  },
+
+  submitCaptchaGuess(aCaptchaSolution) {
+    if (this._moatRequestor && this._currentCaptchaInfo) {
+      return this._moatRequestor
+        .finishFetch(
+          this._currentCaptchaInfo.transport,
+          this._currentCaptchaInfo.challenge,
+          aCaptchaSolution
+        )
+        .then(aBridgeInfo => {
+          this._moatRequestor.close();
+          this._moatRequestor = null;
+          this._currentCaptchaInfo = null;
+          this._bridges = aBridgeInfo.bridges;
+          // array of bridge strings
+          return this._bridges;
+        });
+    }
+
+    return new Promise((aResponse, aReject) => {
+      aReject(new Error("Invalid _moatRequestor or _currentCaptchaInfo"));
+    });
+  },
+
+  requestNewCaptchaImage(aProxyURI) {
+    // close and clear out existing state on captcha request
+    this.close();
+
+    let transportPlugins = TorProtocolService.readStringArraySetting(
+      TorStrings.configKeys.clientTransportPlugin
+    );
+
+    let meekClientPath;
+    let meekTransport; // We support both "meek" and "meek_lite".
+    let meekClientArgs;
+    // TODO: shouldn't this early out once meek settings are found?
+    for (const line of transportPlugins) {
+      // Parse each ClientTransportPlugin line and look for the meek or
+      // meek_lite transport. This code works a lot like the Tor daemon's
+      // parse_transport_line() function.
+      let tokens = line.split(" ");
+      if (tokens.length > 2 && tokens[1] == "exec") {
+        let transportArray = tokens[0].split(",").map(aStr => aStr.trim());
+        let transport = transportArray.find(
+          aTransport => aTransport === "meek"
+        );
+        if (!transport) {
+          transport = transportArray.find(
+            aTransport => aTransport === "meek_lite"
+          );
+        }
+        if (transport) {
+          meekTransport = transport;
+          meekClientPath = tokens[2];
+          meekClientArgs = tokens.slice(3);
+        }
+      }
+    }
+
+    this._moatRequestor = TorLauncherBridgeDB.createMoatRequestor();
+
+    return this._moatRequestor
+      .init(aProxyURI, meekTransport, meekClientPath, meekClientArgs)
+      .then(() => {
+        // TODO: get this from TorLauncherUtil
+        let bridgeType = "obfs4";
+        return this._moatRequestor.fetchBridges([bridgeType]);
+      })
+      .then(aCaptchaInfo => {
+        // cache off the current captcha info as the challenge is needed for response
+        this._currentCaptchaInfo = aCaptchaInfo;
+        return aCaptchaInfo.captchaImage;
+      });
+  },
+
+  close() {
+    if (this._moatRequestor) {
+      this._moatRequestor.close();
+      this._moatRequestor = null;
+    }
+    this._currentCaptchaInfo = null;
+  },
+};
diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm
new file mode 100644
index 000000000000..b4e6ed9a3253
--- /dev/null
+++ b/browser/modules/TorProtocolService.jsm
@@ -0,0 +1,212 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorProtocolService"];
+
+const { TorLauncherUtil } = ChromeUtils.import(
+  "resource://torlauncher/modules/tl-util.jsm"
+);
+
+var TorProtocolService = {
+  _tlps: Cc["@torproject.org/torlauncher-protocol-service;1"].getService(
+    Ci.nsISupports
+  ).wrappedJSObject,
+
+  // maintain a map of tor settings set by Tor Browser so that we don't
+  // repeatedly set the same key/values over and over
+  // this map contains string keys to primitive or array values
+  _settingsCache: new Map(),
+
+  _typeof(aValue) {
+    switch (typeof aValue) {
+      case "boolean":
+        return "boolean";
+      case "string":
+        return "string";
+      case "object":
+        if (aValue == null) {
+          return "null";
+        } else if (Array.isArray(aValue)) {
+          return "array";
+        }
+        return "object";
+    }
+    return "unknown";
+  },
+
+  _assertValidSettingKey(aSetting) {
+    // ensure the 'key' is a string
+    if (typeof aSetting != "string") {
+      throw new Error(
+        `Expected setting of type string but received ${typeof aSetting}`
+      );
+    }
+  },
+
+  _assertValidSetting(aSetting, aValue) {
+    this._assertValidSettingKey(aSetting);
+
+    const valueType = this._typeof(aValue);
+    switch (valueType) {
+      case "boolean":
+      case "string":
+      case "null":
+        return;
+      case "array":
+        for (const element of aValue) {
+          if (typeof element != "string") {
+            throw new Error(
+              `Setting '${aSetting}' array contains value of invalid type '${typeof element}'`
+            );
+          }
+        }
+        return;
+      default:
+        throw new Error(
+          `Invalid object type received for setting '${aSetting}'`
+        );
+    }
+  },
+
+  // takes a Map containing tor settings
+  // throws on error
+  writeSettings(aSettingsObj) {
+    // only write settings that have changed
+    let newSettings = new Map();
+    for (const [setting, value] of aSettingsObj) {
+      let saveSetting = false;
+
+      // make sure we have valid data here
+      this._assertValidSetting(setting, value);
+
+      if (!this._settingsCache.has(setting)) {
+        // no cached setting, so write
+        saveSetting = true;
+      } else {
+        const cachedValue = this._settingsCache.get(setting);
+        if (value != cachedValue) {
+          // compare arrays member-wise
+          if (Array.isArray(value) && Array.isArray(cachedValue)) {
+            if (value.length != cachedValue.length) {
+              saveSetting = true;
+            } else {
+              const arrayLength = value.length;
+              for (let i = 0; i < arrayLength; ++i) {
+                if (value[i] != cachedValue[i]) {
+                  saveSetting = true;
+                  break;
+                }
+              }
+            }
+          } else {
+            // some other different values
+            saveSetting = true;
+          }
+        }
+      }
+
+      if (saveSetting) {
+        newSettings.set(setting, value);
+      }
+    }
+
+    // only write if new setting to save
+    if (newSettings.size > 0) {
+      // convert settingsObject map to js object for torlauncher-protocol-service
+      let settingsObject = {};
+      for (const [setting, value] of newSettings) {
+        settingsObject[setting] = value;
+      }
+
+      let errorObject = {};
+      if (!this._tlps.TorSetConfWithReply(settingsObject, errorObject)) {
+        throw new Error(errorObject.details);
+      }
+
+      // save settings to cache after successfully writing to Tor
+      for (const [setting, value] of newSettings) {
+        this._settingsCache.set(setting, value);
+      }
+    }
+  },
+
+  _readSetting(aSetting) {
+    this._assertValidSettingKey(aSetting);
+    let reply = this._tlps.TorGetConf(aSetting);
+    if (this._tlps.TorCommandSucceeded(reply)) {
+      return reply.lineArray;
+    }
+    throw new Error(reply.lineArray.join("\n"));
+  },
+
+  _readBoolSetting(aSetting) {
+    let lineArray = this._readSetting(aSetting);
+    if (lineArray.length != 1) {
+      throw new Error(
+        `Expected an array with length 1 but received array of length ${
+          lineArray.length
+        }`
+      );
+    }
+
+    let retval = lineArray[0];
+    switch (retval) {
+      case "0":
+        return false;
+      case "1":
+        return true;
+      default:
+        throw new Error(`Expected boolean (1 or 0) but received '${retval}'`);
+    }
+  },
+
+  _readStringSetting(aSetting) {
+    let lineArray = this._readSetting(aSetting);
+    if (lineArray.length != 1) {
+      throw new Error(
+        `Expected an array with length 1 but received array of length ${
+          lineArray.length
+        }`
+      );
+    }
+    return lineArray[0];
+  },
+
+  _readStringArraySetting(aSetting) {
+    let lineArray = this._readSetting(aSetting);
+    return lineArray;
+  },
+
+  readBoolSetting(aSetting) {
+    let value = this._readBoolSetting(aSetting);
+    this._settingsCache.set(aSetting, value);
+    return value;
+  },
+
+  readStringSetting(aSetting) {
+    let value = this._readStringSetting(aSetting);
+    this._settingsCache.set(aSetting, value);
+    return value;
+  },
+
+  readStringArraySetting(aSetting) {
+    let value = this._readStringArraySetting(aSetting);
+    this._settingsCache.set(aSetting, value);
+    return value;
+  },
+
+  // writes current tor settings to disk
+  flushSettings() {
+    this._tlps.TorSendCommand("SAVECONF");
+  },
+
+  getLog() {
+    let countObj = { value: 0 };
+    let torLog = this._tlps.TorGetLog(countObj);
+    return torLog;
+  },
+
+  // true if we launched and control tor, false if using system tor
+  get ownsTorDaemon() {
+    return TorLauncherUtil.shouldStartAndOwnTor;
+  },
+};
diff --git a/browser/modules/moz.build b/browser/modules/moz.build
index 3774c12f4ef0..1f7c6bc4c67e 100644
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -128,6 +128,7 @@ EXTRA_JS_MODULES += [
     "AboutNewTab.jsm",
     "AppUpdater.jsm",
     "AsyncTabSwitcher.jsm",
+    "BridgeDB.jsm",
     "BrowserUIUtils.jsm",
     "BrowserUsageTelemetry.jsm",
     "BrowserWindowTracker.jsm",
@@ -152,6 +153,7 @@ EXTRA_JS_MODULES += [
     "TabsList.jsm",
     "TabUnloader.jsm",
     "ThemeVariableMap.jsm",
+    "TorProtocolService.jsm",
     "TorStrings.jsm",
     "TransientPrefs.jsm",
     "webrtcUI.jsm",
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0