[tbb-commits] [tor-browser/tor-browser-68.1.0esr-9.0-2] Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#tor

gk at torproject.org gk at torproject.org
Sat Oct 12 13:14:32 UTC 2019


commit bd4082f2f1db8a1f1c135d6d1689242f7c659a19
Author: Richard Pospesel <richard at 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
    
    In addition the following changes have been made:
    
     - The Networking Settings in General preferences has been removed
     - TorStrings has been removed from the SecurityLevel component and
       moved into a common shared module also used by about:preferences#tor
---
 browser/components/moz.build                       |   1 +
 browser/components/preferences/in-content/main.js  |  15 -
 browser/components/preferences/in-content/main.xul |  56 --
 .../preferences/in-content/preferences.js          |   2 +
 .../preferences/in-content/preferences.xul         |   5 +
 .../components/preferences/in-content/privacy.js   |   1 +
 .../securitylevel/content/securityLevel.js         | 155 +---
 .../torpreferences/content/parseFunctions.jsm      |  76 ++
 .../torpreferences/content/requestBridgeDialog.jsm | 220 ++++++
 .../torpreferences/content/requestBridgeDialog.xul |  35 +
 .../torpreferences/content/torBridgeSettings.jsm   | 325 +++++++++
 .../torpreferences/content/torCategory.inc.xul     |   8 +
 .../torpreferences/content/torFirewallSettings.jsm |  72 ++
 .../torpreferences/content/torLogDialog.jsm        |  65 ++
 .../torpreferences/content/torLogDialog.xul        |  22 +
 .../components/torpreferences/content/torPane.js   | 802 +++++++++++++++++++++
 .../components/torpreferences/content/torPane.xul  | 119 +++
 .../torpreferences/content/torPreferences.css      |  63 ++
 .../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             | 203 ++++++
 browser/modules/TorStrings.jsm                     | 326 +++++++++
 browser/modules/moz.build                          |   3 +
 26 files changed, 2755 insertions(+), 194 deletions(-)

diff --git a/browser/components/moz.build b/browser/components/moz.build
index 111794a7532c..c0c9629cac65 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -56,6 +56,7 @@ DIRS += [
     'syncedtabs',
     'uitour',
     'urlbar',
+    'torpreferences',
     'translation',
 ]
 
diff --git a/browser/components/preferences/in-content/main.js b/browser/components/preferences/in-content/main.js
index 845ef2f61e30..98c73c5ac119 100644
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -365,16 +365,6 @@ var gMainPane = {
     });
     this.updatePerformanceSettingsBox({ duringChangeEvent: false });
 
-    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();
     }
@@ -464,11 +454,6 @@ var gMainPane = {
       gMainPane.updateHardwareAcceleration.bind(gMainPane)
     );
     setEventListener(
-      "connectionSettings",
-      "command",
-      gMainPane.showConnections
-    );
-    setEventListener(
       "browserContainersCheckbox",
       "command",
       gMainPane.checkBrowserContainers
diff --git a/browser/components/preferences/in-content/main.xul b/browser/components/preferences/in-content/main.xul
index 83d64f26a62d..85a219ff783b 100644
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -669,60 +669,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"
-              icon="network"
-              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-ssl,
-                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-share.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/in-content/preferences.js b/browser/components/preferences/in-content/preferences.js
index d2851f20d2ca..b5b5f87af459 100644
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/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 */
 /* global MozXULElement */
 
 "use strict";
@@ -92,6 +93,7 @@ function init_all() {
     document.getElementById("template-paneSync").remove();
   }
   register_module("paneSearchResults", gSearchResultsPane);
+  register_module("paneTor", gTorPane);
   gSearchResultsPane.init();
   gMainPane.preInit();
 
diff --git a/browser/components/preferences/in-content/preferences.xul b/browser/components/preferences/in-content/preferences.xul
index 3b07e4596907..7a01443ab048 100644
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -16,6 +16,7 @@
 <?xml-stylesheet href="chrome://browser/skin/preferences/in-content/containers.css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/in-content/privacy.css"?>
 <?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
 
 <!DOCTYPE page [
 <!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd">
@@ -138,6 +139,9 @@
           <image class="category-icon"/>
           <label class="category-name" flex="1" data-l10n-id="pane-sync-title2"></label>
         </richlistitem>
+
+#include ../../torpreferences/content/torCategory.inc.xul
+
       </richlistbox>
 
       <spacer flex="1"/>
@@ -195,6 +199,7 @@
 #include privacy.xul
 #include containers.xul
 #include sync.xul
+#include ../../torpreferences/content/torPane.xul
         </vbox>
       </vbox>
     </vbox>
diff --git a/browser/components/preferences/in-content/privacy.js b/browser/components/preferences/in-content/privacy.js
index e9112a2c467e..297d07fadf1f 100644
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -62,6 +62,7 @@ XPCOMUtils.defineLazyGetter(this, "AlertsServiceDND", function() {
   }
 });
 
+// TODO: module import via ChromeUtils.defineModuleGetter
 XPCOMUtils.defineLazyScriptGetter(
   this,
   ["SecurityLevelPreferences"],
diff --git a/browser/components/securitylevel/content/securityLevel.js b/browser/components/securitylevel/content/securityLevel.js
index 9965046a7d15..7f307c5df43a 100644
--- a/browser/components/securitylevel/content/securityLevel.js
+++ b/browser/components/securitylevel/content/securityLevel.js
@@ -8,102 +8,11 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   PanelMultiView: "resource:///modules/PanelMultiView.jsm",
 });
 
-XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser"]);
-XPCOMUtils.defineLazyGetter(this, "domParser", () => {
-  const parser = new DOMParser();
-  parser.forceEnableDTD();
-  return parser;
-});
-
-/*
- Security Level Strings
-
- Strings loaded from torbutton, but en-US defaults provided in case torbutton addon not enabled
-*/
-XPCOMUtils.defineLazyGetter(this, "SecurityLevelStrings", function() {
-  // copied from testing/marionette/l10n.js
-  let localizeEntity = function(urls, id) {
-    // Build a string which contains all possible entity locations
-    let locations = [];
-    urls.forEach((url, index) => {
-      locations.push(`<!ENTITY % dtd_${index} SYSTEM "${url}">%dtd_${index};`);
-    });
-
-    // Use the DOM parser to resolve the entity and extract its real value
-    let header = `<?xml version="1.0"?><!DOCTYPE elem [${locations.join("")}]>`;
-    let elem = `<elem id="elementID">&${id};</elem>`;
-    let doc = domParser.parseFromString(header + elem, "text/xml");
-    let element = doc.querySelector("elem[id='elementID']");
-
-    if (element === null) {
-      throw new Error(`Entity with id='${id}' hasn't been found`);
-    }
-
-    return element.textContent;
-  };
-
-  let getString = function(key, fallback) {
-    try {
-      return localizeEntity(
-        ['chrome://torbutton/locale/torbutton.dtd'],
-        `torbutton.prefs.sec_${key}`
-      );
-    } catch (e) { }
-    return fallback;
-  };
-
-  // read localized strings from torbutton; but use hard-coded en-US strings as fallbacks in case of error
-  let retval = {
-    securityLevel : getString("caption", "Security Level"),
-    customWarning : getString("custom_warning", "Custom"),
-    overview : getString("overview", "Disable certain web features that can be used to attack your security and anonymity."),
-    standard : {
-      level : getString("standard_label", "Standard"),
-      tooltip : getString("standard_tooltip", "Security Level : Standard"),
-      summary : getString("standard_description", "All Tor Browser and website features are enabled."),
-    },
-    safer : {
-      level : getString("safer_label", "Safer"),
-      tooltip : getString("safer_tooltip", "Security Level : Safer"),
-      summary : getString("safer_description", "Disables website features that are often dangerous, causing some sites to lose functionality."),
-      description1 : getString("js_on_https_sites_only", "JavaScript is disabled on non-HTTPS sites."),
-      description2 : getString("limit_typography", "Some fonts and math symbols are disabled."),
-      description3 : getString("click_to_play_media", "Audio and video (HTML5 media), and WebGL are click-to-play."),
-    },
-    safest : {
-      level : getString("safest_label", "Safest"),
-      tooltip : getString("safest_tooltip", "Security Level : Safest"),
-      summary : getString("safest_description", "Only allows website features required for static sites and basic services. These changes affect images, media, and scripts."),
-      description1 : getString("js_disabled", "JavaScript is disabled by default on all sites."),
-      description2 : getString("limit_graphics_and_typography", "Some fonts, icons, math symbols, and images are disabled."),
-      description3 : getString("click_to_play_media", "Audio and video (HTML5 media), and WebGL are click-to-play."),
-    },
-    custom : {
-      summary : getString("custom_summary", "Your custom browser preferences have resulted in unusual security settings. For security and privacy reasons, we recommend you choose one of the default security levels."),
-    },
-    learnMore : getString("learn_more_label", "Learn more"),
-    learnMoreURL : function() {
-        let locale = "";
-        try {
-          let { getLocale } =
-            Cu.import("resource://torbutton/modules/utils.js", {});
-          locale = getLocale();
-        } catch(e) {}
-
-        if (locale == "") {
-          locale = "en-US";
-        }
-
-        return "https://tb-manual.torproject.org/" + locale + "/security-settings/";
-      }(),
-    restoreDefaults : getString("restore_defaults", "Restore Defaults"),
-    advancedSecuritySettings : getString("advanced_security_settings", "Advanced Security Settings\u2026"),
-  };
-
-
-  return retval;
-});
-
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorStrings",
+  "resource:///modules/TorStrings.jsm"
+);
 
 /*
   Security Level Prefs
@@ -158,8 +67,8 @@ const SecurityLevelButton = {
 
   _populateXUL : function(securityLevelButton) {
     if (securityLevelButton != null) {
-      securityLevelButton.setAttribute("tooltiptext", SecurityLevelStrings.securityLevel);
-      securityLevelButton.setAttribute("label", SecurityLevelStrings.securityLevel);
+      securityLevelButton.setAttribute("tooltiptext", TorStrings.securityLevel.securityLevel);
+      securityLevelButton.setAttribute("label", TorStrings.securityLevel.securityLevel);
     }
   },
 
@@ -171,15 +80,15 @@ const SecurityLevelButton = {
       switch(securitySlider) {
         case 4:
           classList.add("standard");
-          securityLevelButton.setAttribute("tooltiptext", SecurityLevelStrings.standard.tooltip);
+          securityLevelButton.setAttribute("tooltiptext", TorStrings.securityLevel.standard.tooltip);
           break;
         case 2:
           classList.add("safer");
-          securityLevelButton.setAttribute("tooltiptext", SecurityLevelStrings.safer.tooltip);
+          securityLevelButton.setAttribute("tooltiptext", TorStrings.securityLevel.safer.tooltip);
           break;
         case 1:
           classList.add("safest");
-          securityLevelButton.setAttribute("tooltiptext", SecurityLevelStrings.safest.tooltip);
+          securityLevelButton.setAttribute("tooltiptext", TorStrings.securityLevel.safest.tooltip);
           break;
       }
     }
@@ -294,12 +203,12 @@ const SecurityLevelPanel = {
     let buttonRestoreDefaults = panelview.querySelector("#securityLevel-restoreDefaults");
     let buttonAdvancedSecuritySettings = panelview.querySelector("#securityLevel-advancedSecuritySettings");
 
-    labelHeader.setAttribute("value", SecurityLevelStrings.securityLevel);
-    labelCustomWarning.setAttribute("value", SecurityLevelStrings.customWarning);
-    labelLearnMore.setAttribute("value", SecurityLevelStrings.learnMore);
-    labelLearnMore.setAttribute("href", SecurityLevelStrings.learnMoreURL);
-    buttonRestoreDefaults.setAttribute("label", SecurityLevelStrings.restoreDefaults);
-    buttonAdvancedSecuritySettings.setAttribute("label", SecurityLevelStrings.advancedSecuritySettings);
+    labelHeader.setAttribute("value", TorStrings.securityLevel.securityLevel);
+    labelCustomWarning.setAttribute("value", TorStrings.securityLevel.customWarning);
+    labelLearnMore.setAttribute("value", TorStrings.securityLevel.learnMore);
+    labelLearnMore.setAttribute("href", TorStrings.securityLevel.learnMoreURL);
+    buttonRestoreDefaults.setAttribute("label", TorStrings.securityLevel.restoreDefaults);
+    buttonAdvancedSecuritySettings.setAttribute("label", TorStrings.securityLevel.advancedSecuritySettings);
 
     // rest of the XUL is set based on security prefs
     this._configUIFromPrefs();
@@ -328,24 +237,24 @@ const SecurityLevelPanel = {
     switch(securitySlider) {
       // standard
       case 4:
-        labelLevel.setAttribute("value", SecurityLevelStrings.standard.level);
-        summary.textContent = SecurityLevelStrings.standard.summary;
+        labelLevel.setAttribute("value", TorStrings.securityLevel.standard.level);
+        summary.textContent = TorStrings.securityLevel.standard.summary;
         break;
       // safer
       case 2:
-        labelLevel.setAttribute("value", SecurityLevelStrings.safer.level);
-        summary.textContent = SecurityLevelStrings.safer.summary;
+        labelLevel.setAttribute("value", TorStrings.securityLevel.safer.level);
+        summary.textContent = TorStrings.securityLevel.safer.summary;
         break;
       // safest
       case 1:
-        labelLevel.setAttribute("value", SecurityLevelStrings.safest.level);
-        summary.textContent = SecurityLevelStrings.safest.summary;
+        labelLevel.setAttribute("value", TorStrings.securityLevel.safest.level);
+        summary.textContent = TorStrings.securityLevel.safest.summary;
         break;
     }
 
     // override the summary text with custom warning
     if (securityCustom) {
-      summary.textContent = SecurityLevelStrings.custom.summary;
+      summary.textContent = TorStrings.securityLevel.custom.summary;
     }
   },
 
@@ -425,14 +334,14 @@ const SecurityLevelPreferences =
     let groupbox = document.getElementById("securityLevel-groupbox");
 
     let labelHeader = groupbox.querySelector("#securityLevel-header");
-    labelHeader.setAttribute("value", SecurityLevelStrings.securityLevel);
+    labelHeader.setAttribute("value", TorStrings.securityLevel.securityLevel);
 
     let spanOverview = groupbox.querySelector("#securityLevel-overview");
-    spanOverview.textContent = SecurityLevelStrings.overview;
+    spanOverview.textContent = TorStrings.securityLevel.overview;
 
     let labelLearnMore = groupbox.querySelector("#securityLevel-learnMore");
-    labelLearnMore.setAttribute("value", SecurityLevelStrings.learnMore);
-    labelLearnMore.setAttribute("href", SecurityLevelStrings.learnMoreURL);
+    labelLearnMore.setAttribute("value", TorStrings.securityLevel.learnMore);
+    labelLearnMore.setAttribute("href", TorStrings.securityLevel.learnMoreURL);
 
     let populateRadioElements = function(vboxQuery, stringStruct) {
       let vbox = groupbox.querySelector(vboxQuery);
@@ -441,13 +350,13 @@ const SecurityLevelPreferences =
       radio.setAttribute("label", stringStruct.level);
 
       let customWarning = vbox.querySelector("#securityLevel-customWarning");
-      customWarning.setAttribute("value", SecurityLevelStrings.customWarning);
+      customWarning.setAttribute("value", TorStrings.securityLevel.customWarning);
 
       let labelSummary = vbox.querySelector("#securityLevel-summary");
       labelSummary.textContent = stringStruct.summary;
 
       let labelRestoreDefaults = vbox.querySelector("#securityLevel-restoreDefaults");
-      labelRestoreDefaults.setAttribute("value", SecurityLevelStrings.restoreDefaults);
+      labelRestoreDefaults.setAttribute("value", TorStrings.securityLevel.restoreDefaults);
 
       let description1 = vbox.querySelector("#securityLevel-description1");
       if (description1) {
@@ -463,9 +372,9 @@ const SecurityLevelPreferences =
       }
     };
 
-    populateRadioElements("#securityLevel-vbox-standard", SecurityLevelStrings.standard);
-    populateRadioElements("#securityLevel-vbox-safer", SecurityLevelStrings.safer);
-    populateRadioElements("#securityLevel-vbox-safest", SecurityLevelStrings.safest);
+    populateRadioElements("#securityLevel-vbox-standard", TorStrings.securityLevel.standard);
+    populateRadioElements("#securityLevel-vbox-safer", TorStrings.securityLevel.safer);
+    populateRadioElements("#securityLevel-vbox-safest", TorStrings.securityLevel.safest);
   },
 
   _configUIFromPrefs : function() {
diff --git a/browser/components/torpreferences/content/parseFunctions.jsm b/browser/components/torpreferences/content/parseFunctions.jsm
new file mode 100644
index 000000000000..a6e6c554ca63
--- /dev/null
+++ b/browser/components/torpreferences/content/parseFunctions.jsm
@@ -0,0 +1,76 @@
+"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 tring 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' delimited string of bridge string, which we split and trim
+let parseBridgeStrings = function(aBridgeStrings) {
+  let splitStrings = aBridgeStrings.split("\n");
+  return splitStrings.map(val => val.trim());
+};
+
+// 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..b66272edf880
--- /dev/null
+++ b/browser/components/torpreferences/content/requestBridgeDialog.jsm
@@ -0,0 +1,220 @@
+"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._submitCommand = null;
+    this._submitButton = null;
+    this._dialogDescription = null;
+    this._captchaImage = null;
+    this._captchaEntryTextbox = null;
+    this._captchaRefreshCommand = 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",
+      submitCommand: "command#torPreferences-requestBridge-submitCommand",
+      captchaImage: "image#torPreferences-requestBridge-captchaImage",
+      captchaEntryTextbox:
+        "textbox#torPreferences-requestBridge-captchaTextbox",
+      refreshCaptchaCommand:
+        "command#torPreferences-requestBridge-refreshCaptchaCommand",
+      refreshCaptchaButton:
+        "button#torPreferences-requestBridge-refreshCaptchaButton",
+      incorrectCaptchaHbox:
+        "hbox#torPreferences-requestBridge-incorrectCaptchaHbox",
+      incorrectCaptchaLabel:
+        "label#torPreferences-requestBridge-incorrectCaptchaError",
+    };
+  }
+
+  _populateXUL(dialog) {
+    const selectors = RequestBridgeDialog.selectors;
+
+    this._dialog = dialog;
+    this._dialog.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.acceptDialog();
+      }
+    });
+
+    this._submitCommand = this._dialog.querySelector(selectors.submitCommand);
+
+    this._submitButton = this._dialog.getButton(selectors.submitButton);
+    this._submitButton.setAttribute("label", TorStrings.settings.submitCaptcha);
+    this._submitButton.setAttribute("command", this._submitCommand.id);
+    this._submitButton.disabled = true;
+
+    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;
+    this._captchaEntryTextbox.onkeypress = evt => {
+      const ENTER_KEY = 13;
+      if (evt.keyCode == ENTER_KEY) {
+        // logically same as pressing the 'submit' button of the parent dialog
+        this.onSubmitCaptcha();
+        return false;
+      }
+      return true;
+    };
+    // disable submit if entry textbox is empty
+    this._captchaEntryTextbox.oninput = () => {
+      this._submitButton.disabled = this._captchaEntryTextbox.value == "";
+    };
+
+    this._captchaRefreshCommand = this._dialog.querySelector(
+      selectors.refreshCaptchaCommand
+    );
+    this._captchaRefreshButton = this._dialog.querySelector(
+      selectors.refreshCaptchaButton
+    );
+    this._captchaRefreshButton.setAttribute(
+      "command",
+      this._captchaRefreshCommand.id
+    );
+    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._dialog.acceptDialog();
+      })
+      .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.xul",
+      "resizable=yes",
+      this,
+      () => {
+        this.close();
+        aCloseCallback(this._bridges);
+      }
+    );
+  }
+}
diff --git a/browser/components/torpreferences/content/requestBridgeDialog.xul b/browser/components/torpreferences/content/requestBridgeDialog.xul
new file mode 100644
index 000000000000..67cae40b9a48
--- /dev/null
+++ b/browser/components/torpreferences/content/requestBridgeDialog.xul
@@ -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"?>
+
+<dialog id="torPreferences-requestBridge-dialog" type="child" class="prefwindow"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+    xmlns:html="http://www.w3.org/1999/xhtml"
+    title="Request Bridge"
+    buttons="cancel,accept"
+    role="dialog">
+  <command id="torPreferences-requestBridge-submitCommand" oncommand="requestBridgeDialog.onSubmitCaptcha();"/>
+  <!-- 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">
+    <textbox id="torPreferences-requestBridge-captchaTextbox" flex="1" />
+    <command id="torPreferences-requestBridge-refreshCaptchaCommand" oncommand="requestBridgeDialog.onRefreshCaptcha();"/>
+    <button id="torPreferences-requestBridge-refreshCaptchaButton" image="chrome://browser/skin/reload.svg"/>
+  </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>
\ 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.xul b/browser/components/torpreferences/content/torCategory.inc.xul
new file mode 100644
index 000000000000..746059358d5f
--- /dev/null
+++ b/browser/components/torpreferences/content/torCategory.inc.xul
@@ -0,0 +1,8 @@
+<richlistitem id="category-tor"
+              class="category"
+              value="paneTor"
+              helpTopic="prefs-tor"
+              align="center">
+  <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..13a8c42884d2
--- /dev/null
+++ b/browser/components/torpreferences/content/torLogDialog.jsm
@@ -0,0 +1,65 @@
+"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;
+    this._dialog.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.xul",
+      "resizable=yes",
+      this
+    );
+  }
+}
diff --git a/browser/components/torpreferences/content/torLogDialog.xul b/browser/components/torpreferences/content/torLogDialog.xul
new file mode 100644
index 000000000000..ae0f4b294204
--- /dev/null
+++ b/browser/components/torpreferences/content/torLogDialog.xul
@@ -0,0 +1,22 @@
+<?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"?>
+
+<dialog id="torPreferences-torLog-dialog" type="child" class="prefwindow"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+    xmlns:html="http://www.w3.org/1999/xhtml"
+    buttons="accept,extra1"
+    role="dialog">
+  <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>
\ 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..08de0613e1d4
--- /dev/null
+++ b/browser/components/torpreferences/content/torPane.js
@@ -0,0 +1,802 @@
+"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: "textbox#torPreferences-localProxy-textboxAddress",
+      proxyPortLabel: "label#torPreferences-localProxy-port",
+      proxyPortTextbox: "input#torPreferences-localProxy-textboxPort",
+      proxyUsernameLabel: "label#torPreferences-localProxy-username",
+      proxyUsernameTextbox: "textbox#torPreferences-localProxy-textboxUsername",
+      proxyPasswordLabel: "label#torPreferences-localProxy-password",
+      proxyPasswordTextbox: "textbox#torPreferences-localProxy-textboxPassword",
+      useFirewallCheckbox: "checkbox#torPreferences-advanced-toggleFirewall",
+      firewallAllowedPortsLabel: "label#torPreferences-advanced-allowedPorts",
+      firewallAllowedPortsTextbox:
+        "textbox#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._bridgeSelectionRadiogroup = prefpane.querySelector(
+        selectors.bridges.bridgeSelectionRadiogroup
+      );
+      this._bridgeSelectionRadiogroup.value = TorBridgeSource.BUILTIN;
+
+      // 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
+      );
+
+      // 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._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
+      );
+
+      // 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._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
+      );
+      for (let currentProxy of mockProxies) {
+        let menuEntry = document.createElement("menuitem");
+        menuEntry.setAttribute("value", currentProxy.value);
+        menuEntry.setAttribute("label", currentProxy.label);
+        this._proxyTypeMenulist.querySelector("menupopup").append(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._proxyPortLabel = prefpane.querySelector(
+        selectors.advanced.proxyPortLabel
+      );
+      this._proxyPortLabel.setAttribute("value", TorStrings.settings.proxyPort);
+      this._proxyPortTextbox = prefpane.querySelector(
+        selectors.advanced.proxyPortTextbox
+      );
+      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._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
+      );
+
+      // Local firewall
+      this._useFirewallCheckbox = prefpane.querySelector(
+        selectors.advanced.useFirewallCheckbox
+      );
+      this._useFirewallCheckbox.setAttribute(
+        "label",
+        TorStrings.settings.useFirewall
+      );
+      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
+      );
+
+      // Tor logs
+      prefpane
+        .querySelector(selectors.advanced.torLogsLabel)
+        .setAttribute("value", TorStrings.settings.showTorDaemonLogs);
+      prefpane
+        .querySelector(selectors.advanced.torLogsButton)
+        .setAttribute("label", TorStrings.settings.showLogs);
+
+      // 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.createElement("menuitem");
+        menuEntry.setAttribute("value", currentBridge);
+        menuEntry.setAttribute("label", currentBridge);
+        this._builtinBridgeMenulist
+          .querySelector("menupopup")
+          .append(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();
+    },
+
+    //
+    // 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 brige 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.xul b/browser/components/torpreferences/content/torPane.xul
new file mode 100644
index 000000000000..298c148bcbf2
--- /dev/null
+++ b/browser/components/torpreferences/content/torPane.xul
@@ -0,0 +1,119 @@
+<!-- Tor panel -->
+
+<script type="application/javascript"
+        src="chrome://browser/content/torpreferences/torPane.js"/>
+
+<hbox id="torPreferencesCategory"
+      class="subcategory"
+      hidden="true"
+      data-category="paneTor">
+  <html:h1 id="torPreferences-header"/>
+</hbox>
+
+<groupbox data-category="paneTor" >
+  <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" >
+  <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" oncommand="gTorPane.onToggleBridge(this.checked).onUpdateBridgeSettings();"/>
+  <radiogroup id="torPreferences-bridges-bridgeSelection"
+              oncommand="gTorPane.onSelectBridgeOption(this.value).onUpdateBridgeSettings();">
+    <hbox class="indent">
+      <radio id="torPreferences-bridges-radioBuiltin"/>
+      <spacer flex="1"/>
+      <menulist id="torPreferences-bridges-builtinList" class="torMarginFix" oncommand="gTorPane.onUpdateBridgeSettings();">
+        <menupopup/>
+      </menulist>
+    </hbox>
+    <vbox class="indent">
+      <hbox>
+        <radio id="torPreferences-bridges-radioRequestBridge"/>
+        <space flex="1"/>
+        <button id="torPreferences-bridges-buttonRequestBridge" class="torMarginFix" oncommand="gTorPane.onRequestBridge();"/>
+      </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"
+          onblur="gTorPane.onUpdateBridgeSettings();"/>
+      </vbox>
+    </hbox>
+  </radiogroup>
+</groupbox>
+
+<!-- Advanced -->
+<groupbox id="torPreferences-advanced-group" data-category="paneTor">
+  <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"/>
+  </description>
+  <grid flex="1">
+    <columns>
+      <column flex="0"/>
+      <column flex="1"/>
+    </columns>
+    <rows>
+      <!-- Local Proxy -->
+      <checkbox id="torPreferences-advanced-toggleProxy" oncommand="gTorPane.onToggleProxy(this.checked).onUpdateProxySettings();"/>
+      <row class="indent" align="center">
+        <label id="torPreferences-localProxy-type"/>
+        <hbox>
+          <spacer flex="1"/>
+          <menulist id="torPreferences-localProxy-builtinList" class="torMarginFix" oncommand="gTorPane.onSelectProxyType(this.value).onUpdateProxySettings();">
+            <menupopup/>
+          </menulist>
+        </hbox>
+      </row>
+      <row class="indent" align="center">
+        <label id="torPreferences-localProxy-address"/>
+        <hbox align="center">
+          <textbox id="torPreferences-localProxy-textboxAddress" class="torMarginFix" flex="4" onblur="gTorPane.onUpdateProxySettings();"/>
+          <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" onblur="gTorPane.onUpdateProxySettings();"/>
+        </hbox>
+      </row>
+      <row class="indent" align="center">
+        <label id="torPreferences-localProxy-username"/>
+        <hbox align="center">
+          <textbox id="torPreferences-localProxy-textboxUsername" class="torMarginFix" flex="1" onblur="gTorPane.onUpdateProxySettings();"/>
+          <label id="torPreferences-localProxy-password"/>
+          <textbox id="torPreferences-localProxy-textboxPassword" class="torMarginFix" type="password" flex="1" onblur="gTorPane.onUpdateProxySettings();"/>
+        </hbox>
+      </row>
+      <!-- Firewall -->
+      <checkbox id="torPreferences-advanced-toggleFirewall" oncommand="gTorPane.onToggleFirewall(this.checked).onUpdateFirewallSettings();"/>
+      <row class="indent" align="center">
+        <label id="torPreferences-advanced-allowedPorts"/>
+        <textbox id="torPreferences-advanced-textboxAllowedPorts" class="torMarginFix" value="80,443" onblur="gTorPane.onUpdateFirewallSettings();"/>
+      </row>
+    </rows>
+  </grid>
+  <hbox id="torPreferences-torDaemon-hbox" align="center">
+    <label id="torPreferences-torLogs"/>
+    <spacer flex="1"/>
+    <button id="torPreferences-buttonTorLogs" class="torMarginFix" oncommand="gTorPane.onViewTorLogs();"/>
+  </hbox>
+</groupbox>
diff --git a/browser/components/torpreferences/content/torPreferences.css b/browser/components/torpreferences/content/torPreferences.css
new file mode 100644
index 000000000000..19c7421cd647
--- /dev/null
+++ b/browser/components/torpreferences/content/torPreferences.css
@@ -0,0 +1,63 @@
+#category-tor > .category-icon {
+  list-style-image: url("chrome://browser/content/torpreferences/torPreferencesIcon.svg");
+}
+
+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..44920322fe4f
--- /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.xul           (content/requestBridgeDialog.xul)
+    content/browser/torpreferences/requestBridgeDialog.jsm           (content/requestBridgeDialog.jsm)
+    content/browser/torpreferences/torBridgeSettings.jsm             (content/torBridgeSettings.jsm)
+    content/browser/torpreferences/torCategory.inc.xul               (content/torCategory.inc.xul)
+    content/browser/torpreferences/torFirewallSettings.jsm           (content/torFirewallSettings.jsm)
+    content/browser/torpreferences/torLogDialog.jsm                  (content/torLogDialog.jsm)
+    content/browser/torpreferences/torLogDialog.xul                  (content/torLogDialog.xul)
+    content/browser/torpreferences/torPane.js                        (content/torPane.js)
+    content/browser/torpreferences/torPane.xul                       (content/torPane.xul)
+    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..7e103239c8d6
--- /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..16bf02e6c688
--- /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..682e7be9de1a
--- /dev/null
+++ b/browser/modules/TorProtocolService.jsm
@@ -0,0 +1,203 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorProtocolService"];
+
+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) {
+        // console.log(`${setting} : ${value}`);
+        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;
+  },
+};
diff --git a/browser/modules/TorStrings.jsm b/browser/modules/TorStrings.jsm
new file mode 100644
index 000000000000..c7bd6f4236ae
--- /dev/null
+++ b/browser/modules/TorStrings.jsm
@@ -0,0 +1,326 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorStrings"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { getLocale } = ChromeUtils.import(
+  "resource://torbutton/modules/utils.js"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser"]);
+XPCOMUtils.defineLazyGetter(this, "domParser", () => {
+  const parser = new DOMParser();
+  parser.forceEnableDTD();
+  return parser;
+});
+
+/*
+  Tor String Bundle
+
+  Strings loaded from torbutton/tor-launcher, but provide a fallback in case they aren't available
+*/
+class TorStringBundle {
+  constructor(aBundleURLs, aPrefix) {
+    let locations = [];
+    for (const [index, url] of aBundleURLs.entries()) {
+      locations.push(`<!ENTITY % dtd_${index} SYSTEM "${url}">%dtd_${index};`);
+    }
+    this._locations = locations;
+    this._prefix = aPrefix;
+  }
+
+  // copied from testing/marionette/l10n.js
+  localizeEntity(urls, id) {
+    // Use the DOM parser to resolve the entity and extract its real value
+    let header = `<?xml version="1.0"?><!DOCTYPE elem [${this._locations.join(
+      ""
+    )}]>`;
+    let elem = `<elem id="elementID">&${id};</elem>`;
+    let doc = domParser.parseFromString(header + elem, "text/xml");
+    let element = doc.querySelector("elem[id='elementID']");
+
+    if (element === null) {
+      throw new Error(`Entity with id='${id}' hasn't been found`);
+    }
+
+    return element.textContent;
+  }
+
+  getString(key, fallback) {
+    if (key) {
+      try {
+        return this.localizeEntity(this._bundleURLs, `${this._prefix}${key}`);
+      } catch (e) {}
+    }
+
+    // on failure, assign the fallback if it exists
+    if (fallback) {
+      return fallback;
+    }
+    // otherwise return string key
+    return `$(${key})`;
+  }
+}
+
+/*
+  Security Level Strings
+*/
+var TorStrings = {
+  /*
+    Tor Browser Security Level Strings
+  */
+  securityLevel: (function() {
+    let tsb = new TorStringBundle(
+      ["chrome://torbutton/locale/torbutton.dtd"],
+      "torbutton.prefs.sec_"
+    );
+    let getString = function(key, fallback) {
+      return tsb.getString(key, fallback);
+    };
+
+    // read localized strings from torbutton; but use hard-coded en-US strings as fallbacks in case of error
+    let retval = {
+      securityLevel: getString("caption", "Security Level"),
+      customWarning: getString("custom_warning", "Custom"),
+      overview: getString(
+        "overview",
+        "Disable certain web features that can be used to attack your security and anonymity."
+      ),
+      standard: {
+        level: getString("standard_label", "Standard"),
+        tooltip: getString("standard_tooltip", "Security Level : Standard"),
+        summary: getString(
+          "standard_description",
+          "All Tor Browser and website features are enabled."
+        ),
+      },
+      safer: {
+        level: getString("safer_label", "Safer"),
+        tooltip: getString("safer_tooltip", "Security Level : Safer"),
+        summary: getString(
+          "safer_description",
+          "Disables website features that are often dangerous, causing some sites to lose functionality."
+        ),
+        description1: getString(
+          "js_on_https_sites_only",
+          "JavaScript is disabled on non-HTTPS sites."
+        ),
+        description2: getString(
+          "limit_typography",
+          "Some fonts and math symbols are disabled."
+        ),
+        description3: getString(
+          "click_to_play_media",
+          "Audio and video (HTML5 media), and WebGL are click-to-play."
+        ),
+      },
+      safest: {
+        level: getString("safest_label", "Safest"),
+        tooltip: getString("safest_tooltip", "Security Level : Safest"),
+        summary: getString(
+          "safest_description",
+          "Only allows website features required for static sites and basic services. These changes affect images, media, and scripts."
+        ),
+        description1: getString(
+          "js_disabled",
+          "JavaScript is disabled by default on all sites."
+        ),
+        description2: getString(
+          "limit_graphics_and_typography",
+          "Some fonts, icons, math symbols, and images are disabled."
+        ),
+        description3: getString(
+          "click_to_play_media",
+          "Audio and video (HTML5 media), and WebGL are click-to-play."
+        ),
+      },
+      custom: {
+        summary: getString(
+          "custom_summary",
+          "Your custom browser preferences have resulted in unusual security settings. For security and privacy reasons, we recommend you choose one of the default security levels."
+        ),
+      },
+      learnMore: getString("learn_more_label", "Learn more"),
+      learnMoreURL: `https://tb-manual.torproject.org/${getLocale()}/security-settings/`,
+      restoreDefaults: getString("restore_defaults", "Restore Defaults"),
+      advancedSecuritySettings: getString(
+        "advanced_security_settings",
+        "Advanced Security Settings\u2026"
+      ),
+    };
+    return retval;
+  })() /* Security Level Strings */,
+
+  /*
+    Tor about:preferences#tor Strings
+  */
+  settings: (function() {
+    let tsb = new TorStringBundle(
+      ["chrome://torlauncher/locale/network-settings.dtd"],
+      ""
+    );
+    let getString = function(key, fallback) {
+      return tsb.getString(key, fallback);
+    };
+
+    let retval = {
+      categoryTitle: getString("torPreferences.categoryTitle", "Tor"),
+      torPreferencesHeading: getString(
+        "torPreferences.torSettings",
+        "Tor Settings"
+      ),
+      torPreferencesDescription: getString(
+        "torPreferences.torSettingsDescription",
+        "Tor Browser routes your traffic over the Tor Network, run by thousands of volunteers around the world."
+      ),
+      learnMore: getString("torPreferences.learnMore", "Learn More"),
+      bridgesHeading: getString("torPreferences.bridges", "Bridges"),
+      bridgesDescription: getString(
+        "torPreferences.bridgesDescription",
+        "Bridges help you access the Tor Network in places where Tor is blocked. Depending on where you are, one bridge may work better than another."
+      ),
+      useBridge: getString("torPreferences.useBridge", "Use a bridge"),
+      selectBridge: getString(
+        "torsettings.useBridges.default",
+        "Select a bridge"
+      ),
+      requestBridgeFromTorProject: getString(
+        "torsettings.useBridges.bridgeDB",
+        "Request a bridge from torproject.org"
+      ),
+      requestNewBridge: getString(
+        "torPreferences.requestNewBridge",
+        "Request a New Bridge\u2026"
+      ),
+      provideBridge: getString(
+        "torPreferences.provideBridge",
+        "Provide a bridge"
+      ),
+      provideBridgeDirections: getString(
+        "torsettings.useBridges.label",
+        "Enter bridge information from a trusted source."
+      ),
+      provideBridgePlaceholder: getString(
+        "torsettings.useBridges.placeholder",
+        "type address:port (one per line)"
+      ),
+      advancedHeading: getString("torPreferences.advanced", "Advanced"),
+      advancedDescription: getString(
+        "torPreferences.advancedDescription",
+        "Configure how Tor Browser connects to the internet."
+      ),
+      useLocalProxy: getString("torPreferences.useProxy", "Use a local proxy"),
+      proxyType: getString("torsettings.useProxy.type", "Proxy Type"),
+      proxyTypeSOCKS4: getString("torsettings.useProxy.type.socks4", "SOCKS4"),
+      proxyTypeSOCKS5: getString("torsettings.useProxy.type.socks5", "SOCKS5"),
+      proxyTypeHTTP: getString("torsettings.useProxy.type.http", "HTTP/HTTPS"),
+      proxyAddress: getString("torsettings.useProxy.address", "Address"),
+      proxyAddressPlaceholder: getString(
+        "torsettings.useProxy.address.placeholder",
+        "IP address or hostname"
+      ),
+      proxyPort: getString("torsettings.useProxy.port", "Port"),
+      proxyUsername: getString("torsettings.useProxy.username", "Username"),
+      proxyPassword: getString("torsettings.useProxy.password", "Password"),
+      proxyUsernamePasswordPlaceholder: getString(
+        "torsettings.optional",
+        "Optional"
+      ),
+      useFirewall: getString(
+        "torsettings.firewall.checkbox",
+        "This computer goes through a firewall that only allows connections to certain ports"
+      ),
+      allowedPorts: getString(
+        "torsettings.firewall.allowedPorts",
+        "Allowed Ports"
+      ),
+      allowedPortsPlaceholder: getString(
+        "torPreferences.firewallPortsPlaceholder",
+        "Comma-seperated values"
+      ),
+      requestBridgeDialogTitle: getString(
+        "torPreferences.requestBridgeDialogTitle",
+        "Request Bridge"
+      ),
+      submitCaptcha: getString(
+        "torsettings.useBridges.captchaSubmit",
+        "Submit"
+      ),
+      contactingBridgeDB: getString(
+        "torPreferences.requestBridgeDialogWaitPrompt",
+        "Contacting BridgeDB. Please Wait."
+      ),
+      solveTheCaptcha: getString(
+        "torPreferences.requestBridgeDialogSolvePrompt",
+        "Solve the CAPTCHA to request a bridge."
+      ),
+      captchaTextboxPlaceholder: getString(
+        "torsettings.useBridges.captchaSolution.placeholder",
+        "Enter the characters from the image"
+      ),
+      incorrectCaptcha: getString(
+        "torPreferences.requestBridgeErrorBadSolution",
+        "The solution is not correct. Please try again."
+      ),
+      showTorDaemonLogs: getString(
+        "torPreferences.viewTorLogs",
+        "View the Tor logs."
+      ),
+      showLogs: getString("torPreferences.viewLogs", "View Logs\u2026"),
+      torLogDialogTitle: getString(
+        "torPreferences.torLogsDialogTitle",
+        "Tor Logs"
+      ),
+      copyLog: getString("torsettings.copyLog", "Copy Tor Log to Clipboard"),
+
+      learnMoreTorBrowserURL: `https://tb-manual.torproject.org/${getLocale()}/about/`,
+      learnMoreBridgesURL: `https://tb-manual.torproject.org/${getLocale()}/bridges/`,
+      learnMoreNetworkSettingsURL: `about:blank`,
+    };
+
+    return retval;
+  })() /* Tor Network Settings Strings */,
+
+  /*
+    Tor Deamon Configuration Key Strings
+  */
+
+  // TODO: proper camel case
+  configKeys: {
+    /* Bridge Conf Settings */
+    useBridges: "UseBridges",
+    bridgeList: "Bridge",
+    /* Proxy Conf Strings */
+    socks4Proxy: "Socks4Proxy",
+    socks5Proxy: "Socks5Proxy",
+    socks5ProxyUsername: "Socks5ProxyUsername",
+    socks5ProxyPassword: "Socks5ProxyPassword",
+    httpsProxy: "HTTPSProxy",
+    httpsProxyAuthenticator: "HTTPSProxyAuthenticator",
+    /* Firewall Conf Strings */
+    reachableAddresses: "ReachableAddresses",
+
+    /* BridgeDB Strings */
+    clientTransportPlugin: "ClientTransportPlugin",
+  },
+
+  /*
+    about:config preference keys
+  */
+
+  preferenceKeys: {
+    defaultBridgeType: "extensions.torlauncher.default_bridge_type",
+    recommendedBridgeType:
+      "extensions.torlauncher.default_bridge_recommended_type",
+  },
+
+  /*
+    about:config preference branches
+  */
+  preferenceBranches: {
+    defaultBridge: "extensions.torlauncher.default_bridge.",
+    bridgeDBBridges: "extensions.torlauncher.bridgedb_bridge.",
+  },
+};
diff --git a/browser/modules/moz.build b/browser/modules/moz.build
index 304bd34d2ff2..43f1524f4553 100644
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -134,6 +134,7 @@ XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 EXTRA_JS_MODULES += [
     'AboutNewTab.jsm',
     'AsyncTabSwitcher.jsm',
+    'BridgeDB.jsm',
     'BrowserUsageTelemetry.jsm',
     'BrowserWindowTracker.jsm',
     'ContentClick.jsm',
@@ -165,6 +166,8 @@ EXTRA_JS_MODULES += [
     'TabsList.jsm',
     'TabUnloader.jsm',
     'ThemeVariableMap.jsm',
+    'TorProtocolService.jsm',
+    'TorStrings.jsm',
     'TransientPrefs.jsm',
     'webrtcUI.jsm',
     'ZoomUI.jsm',



More information about the tbb-commits mailing list