[tor-commits] [tor-launcher/master] Bug 23136: Moat integration (fetch bridges for the user)

gk at torproject.org gk at torproject.org
Fri Mar 2 11:37:02 UTC 2018


commit e921bb15681ac54c9e937b564d31a2a6ec2ceb33
Author: Kathy Brade <brade at pearlcrescent.com>
Date:   Wed Feb 14 10:07:45 2018 -0500

    Bug 23136: Moat integration (fetch bridges for the user)
    
    Modify the setup wizard and Network Settings window to allow automated
    retrieval of bridges using Moat, a BridgeDB service which works
    over a meek transport and requires the user to solve a CAPTCHA to
    obtain bridges.
    
    The new tl-bridgedb.jsm JavaScript module handles all communication
    with BridgeDB, and it functions by starting a copy of
    meek-client-torbrowser and operating as a PT client parent process
    (see https://gitweb.torproject.org/torspec.git/tree/pt-spec.txt).
    
    This feature can be disabled (and the Moat-related Tor Launcher UI
    hidden) by setting the pref extensions.torlauncher.moat_service to
    an empty string.
---
 src/chrome/content/network-settings-overlay.xul |  60 +-
 src/chrome/content/network-settings-wizard.xul  |   9 +-
 src/chrome/content/network-settings.js          | 630 +++++++++++++++++++-
 src/chrome/content/network-settings.xul         |   7 +-
 src/chrome/locale/en/network-settings.dtd       |   4 +
 src/chrome/locale/en/torlauncher.properties     |  14 +
 src/chrome/skin/activity.svg                    |  17 +
 src/chrome/skin/network-settings.css            |  84 ++-
 src/chrome/skin/reload.svg                      |   6 +
 src/defaults/preferences/prefs.js               |   6 +
 src/modules/tl-bridgedb.jsm                     | 746 ++++++++++++++++++++++++
 src/modules/tl-util.jsm                         |  57 +-
 12 files changed, 1583 insertions(+), 57 deletions(-)

diff --git a/src/chrome/content/network-settings-overlay.xul b/src/chrome/content/network-settings-overlay.xul
index b49dbab..3d42c15 100644
--- a/src/chrome/content/network-settings-overlay.xul
+++ b/src/chrome/content/network-settings-overlay.xul
@@ -1,6 +1,6 @@
 <?xml version="1.0"?>
 <!--
-   - Copyright (c) 2017, The Tor Project, Inc.
+   - Copyright (c) 2018, The Tor Project, Inc.
    - See LICENSE for licensing information.
    - vim: set sw=2 sts=2 ts=8 et syntax=xml:
   -->
@@ -93,11 +93,11 @@
                 oncommand="toggleElemUI(this);"/>
     <groupbox id="bridgeSpecificSettings">
       <hbox align="end" pack="end">
-        <radiogroup id="bridgeTypeRadioGroup" flex="1" style="margin: 0px"
-                    oncommand="onBridgeTypeRadioChange()">
-          <hbox align="center">
+        <radiogroup id="bridgeTypeRadioGroup" flex="1" style="margin: 0px">
+          <hbox class="bridgeRadioContainer">
             <radio id="bridgeRadioDefault"
-                   label="&torsettings.useBridges.default;" selected="true"/>
+                   label="&torsettings.useBridges.default;" selected="true"
+                   oncommand="onBridgeTypeRadioChange()"/>
             <button class="helpButton"
                     oncommand="onOpenHelp('bridgeHelpContent')"/>
             <spacer style="width: 3em"/>
@@ -108,8 +108,24 @@
             <spring/>
           </hbox>
 
-          <radio align="start" id="bridgeRadioCustom"
-                 label="&torsettings.useBridges.custom;"/>
+          <vbox id="bridgeDBSettings">
+            <hbox class="bridgeRadioContainer">
+              <radio id="bridgeRadioBridgeDB"
+                     label="&torsettings.useBridges.bridgeDB;"
+                     oncommand="onBridgeTypeRadioChange()"/>
+            </hbox>
+            <vbox id="bridgeDBContainer" align="start">
+              <description id="bridgeDBResult"/>
+              <button id="bridgeDBRequestButton"
+                      oncommand="onOpenBridgeDBRequestPrompt()"/>
+            </vbox>
+          </vbox>
+
+          <hbox class="bridgeRadioContainer">
+            <radio align="start" id="bridgeRadioCustom"
+                   label="&torsettings.useBridges.custom;"
+                   oncommand="onBridgeTypeRadioChange()"/>
+          </hbox>
         </radiogroup>
       </hbox>
       <vbox id="bridgeCustomEntry">
@@ -153,6 +169,36 @@
     </hbox>
   </vbox>
 
+  <vbox id="bridgeDBRequestOverlayContent" align="center">
+    <vbox>
+      <label id="bridgeDBPrompt"/>
+      <image id="bridgeDBCaptchaImage"/>
+      <hbox>
+        <spacer id="bridgeDBReloadSpacer"/>
+        <spacer flex="1"/>
+        <textbox id="bridgeDBCaptchaSolution" size="35"
+          placeholder="&torsettings.useBridges.captchaSolution.placeholder;"
+          oninput="onCaptchaSolutionChange()"/>
+        <spacer flex="1"/>
+        <deck id="bridgeDBReloadDeck">
+          <button id="bridgeDBReloadCaptchaButton"
+                  tooltiptext="&torsettings.useBridges.reloadCaptcha.tooltip;"
+                  oncommand="onReloadCaptcha()"/>
+          <image id="bridgeDBNetworkActivity"/>
+        </deck>
+      </hbox>
+      <label id="bridgeDBCaptchaError"/>
+      <separator/>
+      <hbox pack="center">
+        <button id="bridgeDBCancelButton"
+                oncommand="onCancelBridgeDBRequestPrompt()"/>
+        <button id="bridgeDBSubmitButton" disabled="true"
+                label="&torsettings.useBridges.captchaSubmit;"
+                oncommand="onCaptchaSolutionSubmit()"/>
+      </hbox>
+    </vbox>
+  </vbox>
+
   <vbox id="errorOverlayContent">
     <hbox pack="center">
       <description errorElemId="message" flex="1"/>
diff --git a/src/chrome/content/network-settings-wizard.xul b/src/chrome/content/network-settings-wizard.xul
index 86c2e01..00145a8 100644
--- a/src/chrome/content/network-settings-wizard.xul
+++ b/src/chrome/content/network-settings-wizard.xul
@@ -1,6 +1,6 @@
 <?xml version="1.0"?>
 <!--
-   - Copyright (c) 2017, The Tor Project, Inc.
+   - Copyright (c) 2018, The Tor Project, Inc.
    - See LICENSE for licensing information.
    - vim: set sw=2 sts=2 ts=8 et syntax=xml:
   -->
@@ -33,7 +33,6 @@
       <image class="tbb-logo"/>
     </hbox>
 
-    <separator class="tall"/>
     <vbox class="firstResponses" align="center">
       <label>&torSettings.connectPrompt;</label>
       <label>&torSettings.configurePrompt;</label>
@@ -52,11 +51,13 @@
               torShowNavButtons="true">
     <stack flex="1">
       <vbox>
-        <separator class="tall"/>
         <vbox id="bridgeSettings"/>
-        <separator/>
         <vbox id="proxySettings"/>
       </vbox>
+      <vbox id="bridgeDBRequestOverlay" class="messagePanel" pack="center"
+            hidden="true">
+        <vbox id="bridgeDBRequestOverlayContent"/>
+      </vbox>
       <vbox id="configErrorOverlay" class="messagePanel" pack="center"
             hidden="true">
         <vbox id="errorOverlayContent"/>
diff --git a/src/chrome/content/network-settings.js b/src/chrome/content/network-settings.js
index 773a647..dc3c9ab 100644
--- a/src/chrome/content/network-settings.js
+++ b/src/chrome/content/network-settings.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2017, The Tor Project, Inc.
+// Copyright (c) 2018, The Tor Project, Inc.
 // See LICENSE for licensing information.
 //
 // vim: set sw=2 sts=2 ts=8 et syntax=javascript:
@@ -8,12 +8,15 @@
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
+const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TorLauncherUtil",
                           "resource://torlauncher/modules/tl-util.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TorLauncherLogger",
                           "resource://torlauncher/modules/tl-logger.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TorLauncherBridgeDB",
+                          "resource://torlauncher/modules/tl-bridgedb.jsm");
 
 const kPrefPromptForLocale = "extensions.torlauncher.prompt_for_locale";
 const kPrefLocale = "general.useragent.locale";
@@ -24,6 +27,14 @@ const kPrefDefaultBridgeRecommendedType =
                    "extensions.torlauncher.default_bridge_recommended_type";
 const kPrefDefaultBridgeType = "extensions.torlauncher.default_bridge_type";
 
+// The type of bridges to request from BridgeDB via Moat.
+const kPrefBridgeDBType = "extensions.torlauncher.bridgedb_bridge_type";
+
+// The bridges that we receive from BridgeDB via Moat are stored as
+// extensions.torlauncher.bridgedb_bridge.0,
+// extensions.torlauncher.bridgedb_bridge.1, and so on.
+const kPrefBranchBridgeDBBridge = "extensions.torlauncher.bridgedb_bridge.";
+
 // As of April 2016, no one is responding to help desk email. Hopefully this will change soon.
 //const kSupportAddr = "help at rt.torproject.org";
 const kSupportURL = "torproject.org/about/contact.html#support";
@@ -51,10 +62,34 @@ const kProxyPassword = "proxyPassword";
 const kUseFirewallPortsCheckbox = "useFirewallPorts";
 const kFirewallAllowedPorts = "firewallAllowedPorts";
 const kUseBridgesCheckbox = "useBridges";
+const kDefaultBridgesRadio = "bridgeRadioDefault";
 const kDefaultBridgeTypeMenuList = "defaultBridgeType";
+const kBridgeDBBridgesRadio = "bridgeRadioBridgeDB";
+const kBridgeDBContainer = "bridgeDBContainer";
+const kBridgeDBRequestButton = "bridgeDBRequestButton";
+const kBridgeDBResult = "bridgeDBResult";
 const kCustomBridgesRadio = "bridgeRadioCustom";
 const kBridgeList = "bridgeList";
-
+const kCopyLogFeedbackPanel = "copyLogFeedbackPanel";
+
+// BridgeDB Moat request overlay (for interaction with a CAPTCHA challenge).
+const kBridgeDBRequestOverlay = "bridgeDBRequestOverlay";
+const kBridgeDBPrompt = "bridgeDBPrompt";
+const kBridgeDBCaptchaImage = "bridgeDBCaptchaImage";
+const kCaptchaImageTransition = "height 250ms ease-in-out";
+const kBridgeDBCaptchaSolution = "bridgeDBCaptchaSolution";
+const kBridgeDBCaptchaError = "bridgeDBCaptchaError";
+const kBridgeDBSubmitButton = "bridgeDBSubmitButton";
+const kBridgeDBCancelButton = "bridgeDBCancelButton";
+const kBridgeDBReloadCaptchaButton = "bridgeDBReloadCaptchaButton";
+const kBridgeDBNetworkActivity = "bridgeDBNetworkActivity";
+
+// Custom event types.
+const kCaptchaSubmitEventType = "TorLauncherCaptchaSubmitEvent";
+const kCaptchaCancelEventType = "TorLauncherCaptchaCancelEvent";
+const kCaptchaReloadEventType = "TorLauncherCaptchaReloadEvent";
+
+// Tor SETCONF keywords.
 const kTorConfKeyDisableNetwork = "DisableNetwork";
 const kTorConfKeySocks4Proxy = "Socks4Proxy";
 const kTorConfKeySocks5Proxy = "Socks5Proxy";
@@ -77,6 +112,8 @@ var gRestoreAfterHelpPanelID = null;
 var gIsPostRestartBootstrapNeeded = false;
 var gIsWindowScheduledToClose = false;
 var gActiveTopics = [];  // Topics for which an observer is currently installed.
+var gBridgeDBBridges = undefined; // Array of bridge lines.
+var gBridgeDBRequestEventListeners = [];
 
 
 function initDialogCommon()
@@ -461,16 +498,218 @@ function onCustomBridgesTextInput()
 
 function onBridgeTypeRadioChange()
 {
-  var useCustom = getElemValue(kCustomBridgesRadio, false);
-  setBoolAttrForElemWithLabel(kDefaultBridgeTypeMenuList, "hidden", useCustom);
+  let useBridgeDB = getElemValue(kBridgeDBBridgesRadio, false);
+  let useCustom = getElemValue(kCustomBridgesRadio, false);
+  let useDefault = !useBridgeDB && !useCustom;
+  setBoolAttrForElemWithLabel(kDefaultBridgeTypeMenuList, "hidden",
+                              !useDefault);
+  setBoolAttrForElemWithLabel(kBridgeDBContainer, "hidden", !useBridgeDB);
   setBoolAttrForElemWithLabel(kBridgeList, "hidden", !useCustom);
-  var focusElemID = (useCustom) ? kBridgeList : kDefaultBridgeTypeMenuList;
-  var elem = document.getElementById(focusElemID);
+
+  let focusElemID;
+  if (useBridgeDB)
+    focusElemID = kBridgeDBRequestButton;
+  else if (useCustom)
+    focusElemID = kBridgeList;
+  else
+    focusElemID = kDefaultBridgeTypeMenuList;
+
+  let elem = document.getElementById(focusElemID);
   if (elem)
     elem.focus();
 }
 
 
+function onOpenBridgeDBRequestPrompt()
+{
+  // Obtain the meek client path and args from the tor configuration.
+  let reply = gProtocolSvc.TorGetConf("ClientTransportPlugin");
+  if (!gProtocolSvc.TorCommandSucceeded(reply))
+    return;
+
+  let meekClientPath;
+  let meekClientArgs;
+  reply.lineArray.forEach(aLine =>
+  {
+    let tokens = aLine.split(' ');
+    if ((tokens.length > 2) && (tokens[0] == "meek") && (tokens[1] == "exec"))
+    {
+      meekClientPath = tokens[2];
+      meekClientArgs = tokens.slice(3);
+    }
+  });
+
+  if (!meekClientPath)
+  {
+    reportMoatError(TorLauncherUtil.getLocalizedString("no_meek"));
+    return;
+  }
+
+  let proxySettings;
+  if (isProxyConfigured())
+  {
+    proxySettings = getAndValidateProxySettings(true);
+    if (!proxySettings)
+      return;
+  }
+
+  let overlay = document.getElementById(kBridgeDBRequestOverlay);
+  if (overlay)
+  {
+    let cancelBtn = document.getElementById(kBridgeDBCancelButton);
+    if (cancelBtn)
+      cancelBtn.setAttribute("label", gCancelLabelStr);
+
+    showOrHideDialogButtons(false);
+    resetBridgeDBRequestPrompt();
+    setBridgeDBRequestState("fetchingCaptcha");
+    overlay.hidden = false;
+    requestMoatCaptcha(proxySettings, meekClientPath, meekClientArgs);
+  }
+}
+
+
+// When aState is anything other than undefined, a network request is
+// in progress.
+function setBridgeDBRequestState(aState)
+{
+  let overlay = document.getElementById(kBridgeDBRequestOverlay);
+  if (overlay)
+  {
+    if (aState)
+      overlay.setAttribute("state", aState);
+    else
+      overlay.removeAttribute("state");
+  }
+
+  let key = (aState) ? "contacting_bridgedb" : "captcha_prompt";
+  setElemValue(kBridgeDBPrompt, TorLauncherUtil.getLocalizedString(key));
+
+  let textBox = document.getElementById(kBridgeDBCaptchaSolution);
+  if (textBox)
+  {
+    if (aState)
+      textBox.setAttribute("disabled", "true");
+    else
+      textBox.removeAttribute("disabled");
+  }
+
+  // Show the network activity spinner or the reload button, as appropriate.
+  let deckElem = document.getElementById("bridgeDBReloadDeck");
+  if (deckElem)
+  {
+    let panelID = aState ? kBridgeDBNetworkActivity
+                         : kBridgeDBReloadCaptchaButton;
+    deckElem.selectedPanel = document.getElementById(panelID);
+  }
+}
+
+
+function onDismissBridgeDBRequestPrompt()
+{
+  let overlay = document.getElementById(kBridgeDBRequestOverlay);
+  if (overlay)
+  {
+    overlay.hidden = true;
+    showOrHideDialogButtons(true);
+  }
+
+  setBridgeDBRequestState(undefined);
+}
+
+
+function onCancelBridgeDBRequestPrompt()
+{
+  // If an event listener is installed, the cancel of pending Moat requests
+  // and other necessary cleanup is handled in a cancel event listener.
+  if (gBridgeDBRequestEventListeners.length > 0)
+    document.dispatchEvent(new CustomEvent(kCaptchaCancelEventType, {}));
+  else
+    onDismissBridgeDBRequestPrompt();
+}
+
+
+function resetBridgeDBRequestPrompt()
+{
+  let textBox = document.getElementById(kBridgeDBCaptchaSolution);
+  if (textBox)
+    textBox.value = "";
+
+  let image = document.getElementById(kBridgeDBCaptchaImage);
+  if (image)
+  {
+    image.removeAttribute("src");
+    image.style.transition = "";
+    image.style.height = "0px";
+  }
+
+  onCaptchaSolutionChange();
+}
+
+
+function onCaptchaSolutionChange()
+{
+  let val = getElemValue(kBridgeDBCaptchaSolution, undefined);
+  enableButton(kBridgeDBSubmitButton, val && (val.length > 0));
+  setElemValue(kBridgeDBCaptchaError, undefined); // clear error
+}
+
+
+function onReloadCaptcha()
+{
+  document.dispatchEvent(new CustomEvent(kCaptchaReloadEventType, {}));
+}
+
+
+function onCaptchaSolutionSubmit()
+{
+  let val = getElemValue(kBridgeDBCaptchaSolution, undefined);
+  if (val)
+    document.dispatchEvent(new CustomEvent(kCaptchaSubmitEventType, {}));
+}
+
+
+function isShowingBridgeDBRequestPrompt()
+{
+  let overlay = document.getElementById(kBridgeDBRequestOverlay);
+  return overlay && !overlay.hasAttribute("hidden");
+}
+
+
+function showBridgeDBBridges()
+{
+  // Truncate the bridge info for display.
+  const kMaxLen = 65;
+  let val;
+  if (gBridgeDBBridges)
+  {
+    gBridgeDBBridges.forEach(aBridgeLine =>
+      {
+        let line;
+        if (aBridgeLine.length <= kMaxLen)
+          line = aBridgeLine;
+        else
+          line = aBridgeLine.substring(0, kMaxLen) + "\u2026"; // ellipsis;
+        if (val)
+          val += "\n" + line;
+        else
+          val = line;
+      });
+  }
+
+  setElemValue(kBridgeDBResult, val);
+
+  // Update the "Get a Bridge" button label.
+  let btn = document.getElementById(kBridgeDBRequestButton);
+  if (btn)
+  {
+    let btnLabelKey = val ? "request_a_new_bridge"
+                          : "request_a_bridge";
+    btn.label = TorLauncherUtil.getLocalizedString(btnLabelKey);
+  }
+}
+
+
 function onDeckSelect()
 {
   let deckElem = document.getElementById("deck");
@@ -960,7 +1199,9 @@ function setButtonAttr(aID, aAttr, aValue)
   if (!aID || !aAttr)
     return null;
 
-  var btn = document.documentElement.getButton(aID);
+  let btn = document.documentElement.getButton(aID);  // dialog buttons
+  if (!btn)
+    btn = document.getElementById(aID);               // other buttons
   if (btn)
   {
     if (aValue)
@@ -1159,6 +1400,12 @@ function onCancel()
     return false;
   }
 
+  if (isShowingBridgeDBRequestPrompt())
+  {
+    onCancelBridgeDBRequestPrompt();
+    return false;
+  }
+
   let wizard = getWizard();
   if (!wizard && isShowingProgress())
   {
@@ -1193,15 +1440,19 @@ function onWizardFinish()
     return false;
   }
 
-  if (isShowingProgress())
+  if (isShowingBridgeDBRequestPrompt())
   {
-    onProgressCancelOrReconfigure(getWizard());
+    onCaptchaSolutionSubmit();
     return false;
   }
-  else
+
+  if (isShowingProgress())
   {
-    return applySettings(false);
+    onProgressCancelOrReconfigure(getWizard());
+    return false;
   }
+
+  return applySettings(false);
 }
 
 
@@ -1219,6 +1470,12 @@ function onNetworkSettingsFinish()
     return false;
   }
 
+  if (isShowingBridgeDBRequestPrompt())
+  {
+    onCaptchaSolutionSubmit();
+    return false;
+  }
+
   return applySettings(false);
 }
 
@@ -1256,7 +1513,7 @@ function onCopyLog()
 
   // Display a feedback popup that fades away after a few seconds.
   let copyLogBtn = document.documentElement.getButton("extra2");
-  let panel = document.getElementById("copyLogFeedbackPanel");
+  let panel = document.getElementById(kCopyLogFeedbackPanel);
   if (copyLogBtn && panel)
   {
     panel.firstChild.textContent = TorLauncherUtil.getFormattedLocalizedString(
@@ -1268,7 +1525,7 @@ function onCopyLog()
 
 function closeCopyLogFeedbackPanel()
 {
-  let panel = document.getElementById("copyLogFeedbackPanel");
+  let panel = document.getElementById(kCopyLogFeedbackPanel);
   if (panel && (panel.state =="open"))
     panel.hidePopup();
 }
@@ -1463,6 +1720,9 @@ function initBridgeSettings()
   let canUseDefaultBridges = (typeList && (typeList.length > 0));
   let defaultType = TorLauncherUtil.getCharPref(kPrefDefaultBridgeType);
   let useDefault = canUseDefaultBridges && !!defaultType;
+  let isMoatConfigured = TorLauncherBridgeDB.isMoatConfigured;
+
+  showOrHideElemById("bridgeDBSettings", isMoatConfigured);
 
   // If not configured to use a default set of bridges, get UseBridges setting
   // from tor.
@@ -1477,25 +1737,72 @@ function initBridgeSettings()
 
     useBridges = reply.retVal;
 
-    // Get bridge list from tor.
+    // Get the list of configured bridges from tor.
     let bridgeReply = gProtocolSvc.TorGetConf(kTorConfKeyBridgeList);
     if (!gProtocolSvc.TorCommandSucceeded(bridgeReply))
       return false;
 
-    if (!setBridgeListElemValue(bridgeReply.lineArray))
+    let configuredBridges = [];
+    if (bridgeReply.lineArray)
     {
-      if (canUseDefaultBridges)
-        useDefault = true;  // We have no custom values... back to default.
-      else
-        useBridges = false; // No custom or default bridges are available.
+      bridgeReply.lineArray.forEach(aLine =>
+        {
+          let val = aLine.trim();
+          if (val.length > 0)
+            configuredBridges.push(val);
+        });
+    }
+
+    gBridgeDBBridges = undefined;
+
+    let prefBranch = TorLauncherUtil.getPrefBranch(kPrefBranchBridgeDBBridge);
+    if (isMoatConfigured)
+    {
+      // Determine if we are using a set of bridges that was obtained via Moat.
+      // This is done by checking each of the configured bridge lines against
+      // the values stored under the extensions.torlauncher.bridgedb_bridge.
+      // pref branch. The algorithm used here assumes there are no duplicate
+      // values.
+      let childPrefs = prefBranch.getChildList("", []);
+
+      let bridgeCount = configuredBridges.length;
+      if ((bridgeCount > 0) && (bridgeCount == childPrefs.length))
+      {
+        let foundCount = 0;
+        childPrefs.forEach(aChild =>
+          {
+            if (configuredBridges.indexOf(prefBranch.getCharPref(aChild)) >= 0)
+              ++foundCount;
+          });
+
+        if (foundCount == bridgeCount)
+          gBridgeDBBridges = configuredBridges;
+      }
+    }
+
+    if (!gBridgeDBBridges)
+    {
+      // The stored bridges do not match what is now in torrc. Clear
+      // the stored info and treat the configured bridges as a set of
+      // custom bridges.
+      prefBranch.deleteBranch("");
+      if (!setBridgeListElemValue(configuredBridges))
+      {
+        if (canUseDefaultBridges)
+          useDefault = true;  // We have no custom values... back to default.
+        else
+          useBridges = false; // No custom or default bridges are available.
+      }
     }
   }
 
   setElemValue(kUseBridgesCheckbox, useBridges);
+  showBridgeDBBridges();
 
   showOrHideElemById("bridgeTypeRadioGroup", canUseDefaultBridges);
 
-  let radioID = (useDefault) ? "bridgeRadioDefault" : "bridgeRadioCustom";
+  let radioID = (useDefault) ? kDefaultBridgesRadio
+           : (gBridgeDBBridges) ? kBridgeDBBridgesRadio : kCustomBridgesRadio;
   let radio = document.getElementById(radioID);
   if (radio)
     radio.control.selectedItem = radio;
@@ -1536,6 +1843,18 @@ function useSettings()
   if (!didApply)
     return;
 
+  // Record the new BridgeDB bridge values in preferences so later we
+  // can detect that the bridges were received from BridgeDB via Moat.
+  TorLauncherUtil.getPrefBranch(kPrefBranchBridgeDBBridge).deleteBranch("");
+  if (isUsingBridgeDBBridges())
+  {
+    for (let i = 0; i < gBridgeDBBridges.length; ++i)
+    {
+      TorLauncherUtil.setCharPref(kPrefBranchBridgeDBBridge + i,
+                                  gBridgeDBBridges[i].trim());
+    }
+  }
+
   gIsPostRestartBootstrapNeeded = false;
 
   gProtocolSvc.TorSendCommand("SAVECONF");
@@ -1633,7 +1952,7 @@ function showProgressMeterIfNoError()
 function applyProxySettings(aUseDefaults)
 {
   let settings = aUseDefaults ? getDefaultProxySettings()
-                              : getAndValidateProxySettings();
+                              : getAndValidateProxySettings(false);
   if (!settings)
     return false;
 
@@ -1655,7 +1974,7 @@ function getDefaultProxySettings()
 
 
 // Return a settings object if successful and null if not.
-function getAndValidateProxySettings()
+function getAndValidateProxySettings(aIsForMoat)
 {
   var settings = getDefaultProxySettings();
 
@@ -1666,7 +1985,11 @@ function getAndValidateProxySettings()
     proxyType = getElemValue(kProxyTypeMenulist, null);
     if (!proxyType)
     {
-      reportValidationError("error_proxy_type_missing");
+      let key = "error_proxy_type_missing";
+      if (aIsForMoat)
+        reportMoatError(TorLauncherUtil.getLocalizedString(key));
+      else
+        reportValidationError(key);
       return null;
     }
 
@@ -1674,7 +1997,11 @@ function getAndValidateProxySettings()
                                    getElemValue(kProxyPort, null));
     if (!proxyAddrPort)
     {
-      reportValidationError("error_proxy_addr_missing");
+      let key = "error_proxy_addr_missing";
+      if (aIsForMoat)
+        reportMoatError(TorLauncherUtil.getLocalizedString(key));
+      else
+        reportValidationError(key);
       return null;
     }
 
@@ -1886,16 +2213,27 @@ function getDefaultBridgeSettings()
 // Return a settings object if successful and null if not.
 function getAndValidateBridgeSettings()
 {
-  var settings = getDefaultBridgeSettings();
-  var useBridges = isBridgeConfigured();
-  var defaultBridgeType;
-  var bridgeList;
+  let settings = getDefaultBridgeSettings();
+  let useBridges = isBridgeConfigured();
+  let defaultBridgeType;
+  let bridgeList;
   if (useBridges)
   {
-    var useCustom = getElemValue(kCustomBridgesRadio, false);
-    if (useCustom)
+    if (getElemValue(kBridgeDBBridgesRadio, false))
     {
-      var bridgeStr = getElemValue(kBridgeList, null);
+      if (gBridgeDBBridges)
+      {
+        bridgeList = gBridgeDBBridges;
+      }
+      else
+      {
+        reportValidationError("error_bridgedb_bridges_missing");
+        return null;
+      }
+    }
+    else if (getElemValue(kCustomBridgesRadio, false))
+    {
+      let bridgeStr = getElemValue(kBridgeList, null);
       bridgeList = parseAndValidateBridges(bridgeStr);
       if (!bridgeList)
       {
@@ -1939,6 +2277,13 @@ function isBridgeConfigured()
 }
 
 
+function isUsingBridgeDBBridges()
+{
+  return isBridgeConfigured() && getElemValue(kBridgeDBBridgesRadio, false) &&
+         gBridgeDBBridges;
+}
+
+
 // Returns an array or null.
 function parseAndValidateBridges(aStr)
 {
@@ -2028,8 +2373,15 @@ function setElemValue(aID, aValue)
         // fallthru
       case "menulist":
       case "listbox":
+      case "label":
         elem.value = (val) ? val : "";
         break;
+      case "description":
+        while (elem.firstChild)
+          elem.removeChild(elem.firstChild);
+        if (val)
+          elem.appendChild(document.createTextNode(val));
+        break;
     }
   }
 }
@@ -2140,3 +2492,219 @@ function createColonStr(aStr1, aStr2)
 
   return rv;
 }
+
+
+function requestMoatCaptcha(aProxySettings, aMeekClientPath, aMeekClientArgs)
+{
+  function cleanup(aMoatRequestor, aErr)
+  {
+    if (aMoatRequestor)
+      aMoatRequestor.close();
+    removeAllBridgeDBRequestEventListeners();
+    onDismissBridgeDBRequestPrompt();
+    if (aErr && (aErr != Cr.NS_ERROR_ABORT))
+    {
+      let details;
+      if (aErr.message)
+      {
+        details = aErr.message;
+      }
+      else if (aErr.code)
+      {
+        if (aErr.code < 1000)
+          details = aErr.code;                     // HTTP status code
+        else
+          details = "0x" + aErr.code.toString(16); // nsresult
+      }
+
+      reportMoatError(details);
+    }
+  }
+
+  let moatRequestor = TorLauncherBridgeDB.createMoatRequestor();
+
+  let cancelListener = function(aEvent) {
+    if (!moatRequestor.cancel())
+      cleanup(moatRequestor, undefined); // There was no network request to cancel.
+  };
+  addBridgeDBRequestEventListener(kCaptchaCancelEventType, cancelListener);
+
+  moatRequestor.init(proxyURLFromSettings(aProxySettings),
+                     aMeekClientPath, aMeekClientArgs)
+    .then(()=>
+    {
+      let bridgeType = TorLauncherUtil.getCharPref(kPrefBridgeDBType);
+      moatRequestor.fetchBridges([bridgeType])
+      .then(aCaptchaInfo =>
+      {
+        return waitForCaptchaResponse(moatRequestor, aCaptchaInfo);
+      })
+      .then(aBridgeInfo =>
+      {
+        // Success! Keep and display the received bridge information.
+        cleanup(moatRequestor, undefined);
+        gBridgeDBBridges = aBridgeInfo.bridges;
+        showBridgeDBBridges();
+      })
+      .catch(aErr =>
+      {
+        cleanup(moatRequestor, aErr);
+      });
+    })
+    .catch(aErr =>
+    {
+      cleanup(moatRequestor, aErr);
+    });
+} // requestMoatCaptcha
+
+
+function reportMoatError(aDetails)
+{
+  if (!aDetails)
+    aDetails = "";
+
+  let msg = TorLauncherUtil.getFormattedLocalizedString("unable_to_get_bridge",
+                                                        [aDetails], 1);
+  showErrorMessage({ message: msg }, false);
+}
+
+
+function proxyURLFromSettings(aProxySettings)
+{
+  if (!aProxySettings)
+    return undefined;
+
+  let proxyURL;
+  if (aProxySettings[kTorConfKeySocks4Proxy])
+  {
+    proxyURL = "socks4a://" + aProxySettings[kTorConfKeySocks4Proxy];
+  }
+  else if (aProxySettings[kTorConfKeySocks5Proxy])
+  {
+    proxyURL = "socks5://";
+    if (aProxySettings[kTorConfKeySocks5ProxyUsername])
+    {
+      proxyURL += createColonStr(
+                      aProxySettings[kTorConfKeySocks5ProxyUsername],
+                      aProxySettings[kTorConfKeySocks5ProxyPassword]);
+      proxyURL += "@";
+    }
+    proxyURL += aProxySettings[kTorConfKeySocks5Proxy];
+  }
+  else if (aProxySettings[kTorConfKeyHTTPSProxy])
+  {
+    proxyURL = "http://";
+    if (aProxySettings[kTorConfKeyHTTPSProxyAuthenticator])
+    {
+      proxyURL += aProxySettings[kTorConfKeyHTTPSProxyAuthenticator];
+      proxyURL += "@";
+    }
+    proxyURL += aProxySettings[kTorConfKeyHTTPSProxy];
+  }
+
+  return proxyURL;
+} // proxyURLFromSettings
+
+
+// Returns a promise that is resolved with a bridge info object that includes
+// a bridges property, which is an array of bridge configuration lines.
+function waitForCaptchaResponse(aMoatRequestor, aCaptchaInfo)
+{
+  let mCaptchaInfo;
+
+  function displayCaptcha(aCaptchaInfoArg)
+  {
+    mCaptchaInfo = aCaptchaInfoArg;
+    let image = document.getElementById(kBridgeDBCaptchaImage);
+    if (image)
+    {
+      image.setAttribute("src", mCaptchaInfo.captchaImage);
+      image.style.transition = kCaptchaImageTransition;
+      image.style.height = "125px";
+    }
+
+    setBridgeDBRequestState(undefined);
+    focusCaptchaSolutionTextbox();
+  }
+
+  displayCaptcha(aCaptchaInfo);
+
+  return new Promise((aResolve, aReject) =>
+  {
+    let reloadListener = function(aEvent) {
+      // Reset the UI and request a new CAPTCHA.
+      resetBridgeDBRequestPrompt();
+      setBridgeDBRequestState("fetchingCaptcha");
+      aMoatRequestor.fetchBridges([mCaptchaInfo.transport])
+      .then(aCaptchaInfoArg =>
+      {
+        displayCaptcha(aCaptchaInfoArg);
+      })
+      .catch(aErr =>
+      {
+        aReject(aErr);
+      });
+    };
+
+    let submitListener = function(aEvent) {
+      mCaptchaInfo.solution = getElemValue(kBridgeDBCaptchaSolution);
+      setBridgeDBRequestState("checkingSolution");
+      aMoatRequestor.finishFetch(mCaptchaInfo.transport,
+                               mCaptchaInfo.challenge, mCaptchaInfo.solution)
+      .then(aBridgeInfo =>
+      {
+        setBridgeDBRequestState(undefined);
+        aResolve(aBridgeInfo);
+      })
+      .catch(aErr =>
+      {
+        setBridgeDBRequestState(undefined);
+        if ((aErr instanceof TorLauncherBridgeDB.error) &&
+            (aErr.code == TorLauncherBridgeDB.errorCodeBadCaptcha))
+        {
+          // Incorrect solution was entered. Allow the user to try again.
+          let s = TorLauncherUtil.getLocalizedString("bad_captcha_solution");
+          setElemValue(kBridgeDBCaptchaError, s);
+          focusCaptchaSolutionTextbox();
+        }
+        else
+        {
+          aReject(aErr);
+        }
+      });
+    };
+
+    addBridgeDBRequestEventListener(kCaptchaReloadEventType, reloadListener);
+    addBridgeDBRequestEventListener(kCaptchaSubmitEventType, submitListener);
+  });
+} // waitForCaptchaResponse
+
+
+function addBridgeDBRequestEventListener(aEventType, aListener)
+{
+  document.addEventListener(aEventType, aListener, false);
+  gBridgeDBRequestEventListeners.push({type: aEventType, listener: aListener});
+}
+
+
+function removeAllBridgeDBRequestEventListeners()
+{
+  for (let i = gBridgeDBRequestEventListeners.length - 1; i >= 0; --i)
+  {
+      document.removeEventListener(gBridgeDBRequestEventListeners[i].type,
+                          gBridgeDBRequestEventListeners[i].listener, false);
+  }
+
+  gBridgeDBRequestEventListeners = [];
+}
+
+
+function focusCaptchaSolutionTextbox()
+{
+  let textBox = document.getElementById(kBridgeDBCaptchaSolution);
+  if (textBox)
+  {
+    textBox.focus();
+    textBox.select();
+  }
+}
diff --git a/src/chrome/content/network-settings.xul b/src/chrome/content/network-settings.xul
index 707990a..6f95183 100644
--- a/src/chrome/content/network-settings.xul
+++ b/src/chrome/content/network-settings.xul
@@ -1,6 +1,6 @@
 <?xml version="1.0"?>
 <!--
-   - Copyright (c) 2017, The Tor Project, Inc.
+   - Copyright (c) 2018, The Tor Project, Inc.
    - See LICENSE for licensing information.
    - vim: set sw=2 sts=2 ts=8 et syntax=xml:
   -->
@@ -74,6 +74,11 @@
       <panel id="copyLogFeedbackPanel"/>
     </vbox>
 
+    <vbox id="bridgeDBRequestOverlay" class="messagePanel" pack="center"
+          hidden="true">
+      <vbox id="bridgeDBRequestOverlayContent"/>
+    </vbox>
+
     <vbox id="errorOverlay" class="messagePanel" pack="center" hidden="true">
       <vbox id="errorOverlayContent"/>
     </vbox>
diff --git a/src/chrome/locale/en/network-settings.dtd b/src/chrome/locale/en/network-settings.dtd
index 85645d7..4615146 100644
--- a/src/chrome/locale/en/network-settings.dtd
+++ b/src/chrome/locale/en/network-settings.dtd
@@ -41,6 +41,10 @@
 <!ENTITY torsettings.useBridges.checkbox "Tor is censored in my country">
 <!ENTITY torsettings.useBridges.default "Select a built-in bridge">
 <!ENTITY torsettings.useBridges.default.placeholder "select a bridge">
+<!ENTITY torsettings.useBridges.bridgeDB "Request a bridge from torproject.org">
+<!ENTITY torsettings.useBridges.captchaSolution.placeholder "Enter the characters from the image">
+<!ENTITY torsettings.useBridges.reloadCaptcha.tooltip "Get a new challenge">
+<!ENTITY torsettings.useBridges.captchaSubmit "Submit">
 <!ENTITY torsettings.useBridges.custom "Provide a bridge I know">
 <!ENTITY torsettings.useBridges.label "Enter bridge information from a trusted source.">
 <!ENTITY torsettings.useBridges.placeholder "type address:port (one per line)">
diff --git a/src/chrome/locale/en/torlauncher.properties b/src/chrome/locale/en/torlauncher.properties
index b09753e..a4d097a 100644
--- a/src/chrome/locale/en/torlauncher.properties
+++ b/src/chrome/locale/en/torlauncher.properties
@@ -26,11 +26,21 @@ torlauncher.error_proxy_addr_missing=You must specify both an IP address or host
 torlauncher.error_proxy_type_missing=You must select the proxy type.
 torlauncher.error_bridges_missing=You must specify one or more bridges.
 torlauncher.error_default_bridges_type_missing=You must select a transport type for the provided bridges.
+torlauncher.error_bridgedb_bridges_missing=Please request a bridge.
 torlauncher.error_bridge_bad_default_type=No provided bridges that have the transport type %S are available. Please adjust your settings.
 
 torlauncher.bridge_suffix.meek-amazon=(works in China)
 torlauncher.bridge_suffix.meek-azure=(works in China)
 
+torlauncher.request_a_bridge=Request a Bridge…
+torlauncher.request_a_new_bridge=Request a New Bridge…
+torlauncher.contacting_bridgedb=Contacting BridgeDB. Please wait.
+torlauncher.captcha_prompt=Solve the CAPTCHA to request a bridge.
+torlauncher.bad_captcha_solution=The solution is not correct. Please try again.
+torlauncher.unable_to_get_bridge=Unable to obtain a bridge from BridgeDB.\n\n%S
+torlauncher.no_meek=This browser is not configured for meek, which is needed to obtain bridges.
+torlauncher.no_bridges_available=No bridges are available at this time. Sorry.
+
 torlauncher.connect=Connect
 torlauncher.restart_tor=Restart Tor
 torlauncher.quit=Quit
@@ -62,3 +72,7 @@ torlauncher.bootstrapWarning.timeout=connection timeout
 torlauncher.bootstrapWarning.noroute=no route to host
 torlauncher.bootstrapWarning.ioerror=read/write error
 torlauncher.bootstrapWarning.pt_missing=missing pluggable transport
+
+torlauncher.nsresult.NS_ERROR_NET_RESET=The connection to the server was lost.
+torlauncher.nsresult.NS_ERROR_CONNECTION_REFUSED=Could not connect to the server.
+torlauncher.nsresult.NS_ERROR_PROXY_CONNECTION_REFUSED=Could not connect to the proxy.
diff --git a/src/chrome/skin/activity.svg b/src/chrome/skin/activity.svg
new file mode 100644
index 0000000..3aae4aa
--- /dev/null
+++ b/src/chrome/skin/activity.svg
@@ -0,0 +1,17 @@
+<!-- Based on http://goo.gl/7AJzbL  By Sam Herbert -->
+<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg" stroke="#000">
+    <g fill="none" fill-rule="evenodd">
+        <g transform="translate(1 1)" stroke-width="6">
+            <circle stroke-opacity=".4" cx="18" cy="18" r="16"/>
+            <path d="M34 18c0-9.94-8.06-18-18-16">
+                <animateTransform
+                    attributeName="transform"
+                    type="rotate"
+                    from="0 18 18"
+                    to="360 18 18"
+                    dur="1s"
+                    repeatCount="indefinite"/>
+            </path>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/src/chrome/skin/network-settings.css b/src/chrome/skin/network-settings.css
index 259e38d..9a02493 100644
--- a/src/chrome/skin/network-settings.css
+++ b/src/chrome/skin/network-settings.css
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, The Tor Project, Inc.
+ * Copyright (c) 2018, The Tor Project, Inc.
  * See LICENSE for licensing information.
  *
  * vim: set sw=2 sts=2 ts=8 et syntax=css:
@@ -18,14 +18,14 @@ dialog.os-windows {
 
 wizard {
   width: 45em;
-  height: 36em;
+  height: 38em;
   font: -moz-dialog;
   padding-top: 0px;
 }
 
 wizard.os-windows {
   width: 49em;
-  height: 42em;
+  height: 44em;
 }
 
 .wizard-page-box {
@@ -67,8 +67,15 @@ wizard radiogroup {
   margin: 9px 40px;
 }
 
-separator.tall {
-  height: 2.1em;
+.firstResponses,
+wizard #bridgeSettings,
+wizard #proxySettings {
+  margin-top: 15px;
+}
+
+.bridgeRadioContainer {
+  min-height: 30px; /* ensure no height change when dropdown menu is hidden */
+  vertical-align: middle;
 }
 
 .help .heading,
@@ -105,6 +112,7 @@ wizard#TorLauncherLocalePicker button[dlgtype="next"] {
 
 #bridgeNote,
 #bridgeDefaultEntry,
+#bridgeDBContainer,
 #bridgeCustomEntry {
   margin-left: 1.8em;
 }
@@ -114,6 +122,15 @@ wizard.os-mac #bridgeList {
   font-size: 90%;
 }
 
+#bridgeDBResult {
+  font-size: 90%;
+  white-space: pre;
+}
+
+#bridgeDBResult[value=""] {
+  display: none;
+}
+
 /* reuse Mozilla's help button from the Firefox hamburger menu */
 .helpButton {
   list-style-image: url(chrome://browser/skin/menuPanel-help.png);
@@ -171,6 +188,7 @@ wizardpage[pageid="restartPanel"] description,
   text-align: start;
 }
 
+#bridgeDBRequestOverlayContent,
 #errorOverlayContent {
   margin: 50px;
   min-height: 12em;
@@ -178,6 +196,62 @@ wizardpage[pageid="restartPanel"] description,
   box-shadow: 0px 0px 50px rgba(0,0,0,0.9);
 }
 
+#bridgeDBRequestOverlayContent > vbox {
+  margin: 20px;
+}
+
+#bridgeDBPrompt {
+  text-align: center;
+}
+
+#bridgeDBCaptchaImage {
+  margin: 16px 0px;
+  width: 400px;
+  /* height is set via code so it can be animated. */
+}
+
+#bridgeDBReloadSpacer {
+  width: 20px;  /* matches the width of #bridgeDBReloadCaptchaButton */
+}
+
+#bridgeDBReloadCaptchaButton {
+  list-style-image: url("chrome://torlauncher/skin/reload.svg");
+  -moz-appearance: none;
+  width: 20px;  /* matches the width of #bridgeDBReloadSpacer */
+  height: 20px;
+  min-height: 20px;
+  min-width: 20px;
+  margin: 0;
+  background: none;
+  border: none;
+  box-shadow: none;
+}
+
+#bridgeDBNetworkActivity {
+  list-style-image: url("chrome://torlauncher/skin/activity.svg");
+  width: 20px;
+  height: 20px;
+}
+
+#bridgeDBCaptchaError {
+  color: red;
+  font-weight: bold;
+  text-align: center;
+}
+
+/* Hide BridgeDB overlay elements based on the state attribute. */
+#bridgeDBRequestOverlay[state="fetchingCaptcha"] #bridgeDBReloadCaptchaButton,
+#bridgeDBRequestOverlay[state="checkingSolution"] #bridgeDBReloadCaptchaButton,
+#bridgeDBRequestOverlay[state="fetchingCaptcha"] #bridgeDBCaptchaSolution {
+  visibility: hidden;
+}
+
+#bridgeDBRequestOverlay[state="fetchingCaptcha"] #bridgeDBCaptchaError,
+#bridgeDBRequestOverlay[state="fetchingCaptcha"] #bridgeDBSubmitButton,
+#bridgeDBRequestOverlay[state="checkingSolution"] #bridgeDBSubmitButton {
+  display: none;
+}
+
 #errorOverlayContent button[errorElemId="dismissButton"] {
   margin-bottom: 20px;
 }
diff --git a/src/chrome/skin/reload.svg b/src/chrome/skin/reload.svg
new file mode 100644
index 0000000..d218991
--- /dev/null
+++ b/src/chrome/skin/reload.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="#000000" d="M15 8H8l2.8-2.8a3.691 3.691 0 0 0-2.3-.7 4 4 0 0 0 0 8 3.9 3.9 0 0 0 3.4-1.9l2.3 1A6.5 6.5 0 1 1 8.5 2a6.773 6.773 0 0 1 4.1 1.4L15 1z"/>
+</svg>
diff --git a/src/defaults/preferences/prefs.js b/src/defaults/preferences/prefs.js
index 752514a..cab235a 100644
--- a/src/defaults/preferences/prefs.js
+++ b/src/defaults/preferences/prefs.js
@@ -45,6 +45,12 @@ pref("extensions.torlauncher.tor_path", "");
 pref("extensions.torlauncher.torrc_path", "");
 pref("extensions.torlauncher.tordatadir_path", "");
 
+// BridgeDB-related preferences (used for Moat).
+pref("extensions.torlauncher.bridgedb_front", "www.google.com");
+pref("extensions.torlauncher.bridgedb_reflector", "https://tor-bridges-hyphae-channel.appspot.com");
+pref("extensions.torlauncher.moat_service", "https://bridges.torproject.org/moat");
+pref("extensions.torlauncher.bridgedb_bridge_type", "obfs4");
+
 // Recommended default bridge type (can be set per localized bundle).
 // pref("extensions.torlauncher.default_bridge_recommended_type", "obfs3");
 
diff --git a/src/modules/tl-bridgedb.jsm b/src/modules/tl-bridgedb.jsm
new file mode 100644
index 0000000..339cb39
--- /dev/null
+++ b/src/modules/tl-bridgedb.jsm
@@ -0,0 +1,746 @@
+// Copyright (c) 2018, The Tor Project, Inc.
+// See LICENSE for licensing information.
+//
+// vim: set sw=2 sts=2 ts=8 et syntax=javascript:
+
+/*************************************************************************
+ * Tor Launcher BridgeDB Communication Module
+ * https://github.com/isislovecruft/bridgedb/#accessing-the-moat-interface
+ *************************************************************************/
+
+let EXPORTED_SYMBOLS = [ "TorLauncherBridgeDB" ];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Subprocess.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "TorLauncherUtil",
+                          "resource://torlauncher/modules/tl-util.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TorLauncherLogger",
+                          "resource://torlauncher/modules/tl-logger.jsm");
+
+let TorLauncherBridgeDB =  // Public
+{
+  get isMoatConfigured()
+  {
+    let pref = _MoatRequestor.prototype.kPrefMoatService;
+    return !!TorLauncherUtil.getCharPref(pref);
+  },
+
+  // Returns an _MoatRequestor object.
+  createMoatRequestor: function()
+  {
+    return new _MoatRequestor();
+  },
+
+  // Extended Error object which is used when we have a numeric code and
+  // a text error message.
+  error: function(aCode, aMessage)
+  {
+    this.code = aCode;
+    this.message = aMessage;
+  },
+
+  errorCodeBadCaptcha: 419
+};
+
+TorLauncherBridgeDB.error.prototype = Error.prototype;  // subclass Error
+
+Object.freeze(TorLauncherBridgeDB);
+
+
+function _MoatRequestor()
+{
+}
+
+_MoatRequestor.prototype =
+{
+  kMaxResponseLength: 1024 * 400,
+  kTransport: "meek",
+  kMoatContentType: "application/vnd.api+json",
+  kMoatVersion: "0.1.0",
+  kPrefBridgeDBFront: "extensions.torlauncher.bridgedb_front",
+  kPrefBridgeDBReflector: "extensions.torlauncher.bridgedb_reflector",
+  kPrefMoatService: "extensions.torlauncher.moat_service",
+  kMoatFetchURLPath: "/fetch",
+  kMoatFetchRequestType: "client-transports",
+  kMoatFetchResponseType: "moat-challenge",
+  kMoatCheckURLPath: "/check",
+  kMoatCheckRequestType: "moat-solution",
+  kMoatCheckResponseType: "moat-bridges",
+
+  kStateIdle: 0,
+  kStateWaitingForVersion: 1,
+  kStateWaitingForProxyDone: 2,
+  kStateWaitingForCMethod: 3,
+  kStateWaitingForCMethodsDone: 4,
+  kStateInitialized: 5,
+
+  mState: this.kStateIdle,
+
+  mLocalProxyURL: undefined,
+  mMeekFront: undefined,  // Frontend server, if we are using one.
+  mMeekClientProcess: undefined,
+  mMeekClientStdoutBuffer: undefined,
+  mMeekClientProxyType: undefined,  // contains Mozilla names such as socks4
+  mMeekClientIP: undefined,
+  mMeekClientPort: undefined,
+  mMoatResponseListener: undefined,
+  mUserCanceled: false,
+
+  // Returns a promise.
+  init: function(aProxyURL, aMeekClientPath, aMeekClientArgs)
+  {
+    this.mLocalProxyURL = aProxyURL;
+    return this._startMeekClient(aMeekClientPath, aMeekClientArgs);
+  },
+
+  close: function()
+  {
+    if (this.mMeekClientProcess)
+    {
+      this.mMeekClientProcess.kill();
+      this.mMeekClientProcess = undefined;
+    }
+  },
+
+  // Public function: request bridges via Moat.
+  // Returns a promise that is fulfilled with an object that contains:
+  //   transport
+  //   captchaImage
+  //   challenge
+  //
+  // aTransports is an array of transport strings. Supported values:
+  //   "vanilla"
+  //   "fte"
+  //   "obfs3"
+  //   "obfs4"
+  //   "scramblesuit"
+  fetchBridges: function(aTransports)
+  {
+    this.mUserCanceled = false;
+    if (!this.mMeekClientProcess)
+      return this._meekClientNotRunningError();
+
+    let requestObj = {
+      data: [{
+        version: this.kMoatVersion,
+        type: this.kMoatFetchRequestType,
+        supported: aTransports
+      }]
+    };
+    return this._sendMoatRequest(requestObj, false);
+  },
+
+  // Public function: check CAPTCHA and retrieve bridges via Moat.
+  // Returns a promise that is fulfilled with an object that contains:
+  //   bridges          // an array of strings (bridge lines)
+  finishFetch: function(aTransport, aChallenge, aSolution)
+  {
+    this.mUserCanceled = false;
+    if (!this.mMeekClientProcess)
+      return this._meekClientNotRunningError();
+
+    let requestObj = {
+      data: [{
+        id: "2",
+        type: this.kMoatCheckRequestType,
+        version: this.kMoatVersion,
+        transport: aTransport,
+        challenge: aChallenge,
+        solution: aSolution,
+        qrcode: "false"
+      }]
+    };
+    return this._sendMoatRequest(requestObj, true);
+  },
+
+  // Returns true if a promise is pending (which will be rejected), e.g.,
+  // if a network request is active or we are inside init().
+  cancel: function()
+  {
+    this.mUserCanceled = true;
+    if (this.mMoatResponseListener)
+      return this.mMoatResponseListener.cancelMoatRequest();
+
+    if (this.mState != this.kStateInitialized)
+    {
+      // close() will kill the meek client process, which will cause
+      // initialization to fail.
+      this.close();
+      return true;
+    }
+
+    return false;
+  },
+
+  // Returns a rejected promise.
+  _meekClientNotRunningError()
+  {
+    return Promise.reject(new Error("The meek client exited unexpectedly."));
+  },
+
+  // Returns a promise.
+  _startMeekClient: function(aMeekClientPath, aMeekClientArgs)
+  {
+    let workDir = TorLauncherUtil.getTorFile("pt-startup-dir", false);
+    if (!workDir)
+      return Promise.reject(new Error("Missing pt-startup-dir."));
+
+    // Ensure that we have an absolute path for the meek client program.
+    // This is necessary because Subprocess.call() checks for the existence
+    // of the file before it changes to the startup (working) directory.
+    let meekClientPath;
+    let re = (TorLauncherUtil.isWindows) ?  /^[A-Za-z]:\\/ : /^\//;
+    if (re.test(aMeekClientPath))
+    {
+      meekClientPath = aMeekClientPath; // We already have an absolute path.
+    }
+    else
+    {
+      let f = workDir.clone();
+      f.appendRelativePath(aMeekClientPath);
+      meekClientPath = f.path;
+    }
+
+    // Construct the args array.
+    let args = aMeekClientArgs.slice(); // make a copy
+    let meekReflector = TorLauncherUtil.getCharPref(this.kPrefBridgeDBReflector);
+    if (meekReflector)
+    {
+      args.push("-url");
+      args.push(meekReflector);
+    }
+    this.mMeekFront = TorLauncherUtil.getCharPref(this.kPrefBridgeDBFront);
+    if (this.mMeekFront)
+    {
+      args.push("-front");
+      args.push(this.mMeekFront);
+    }
+
+    let ptStateDir = TorLauncherUtil.getTorFile("tordatadir", false);
+    if (!ptStateDir)
+    {
+      let msg = TorLauncherUtil.getLocalizedString("datadir_missing");
+      return Promise.reject(new Error(msg));
+    }
+    ptStateDir.append("pt_state");  // Match what tor uses.
+
+    let envAdditions = { TOR_PT_MANAGED_TRANSPORT_VER: "1",
+                         TOR_PT_STATE_LOCATION: ptStateDir.path,
+                         TOR_PT_EXIT_ON_STDIN_CLOSE: "1",
+                         TOR_PT_CLIENT_TRANSPORTS: this.kTransport };
+    if (this.mLocalProxyURL)
+      envAdditions.TOR_PT_PROXY = this.mLocalProxyURL;
+
+    TorLauncherLogger.log(3, "starting " + meekClientPath + " in "
+                          + workDir.path);
+    TorLauncherLogger.log(3, "args " + JSON.stringify(args));
+    TorLauncherLogger.log(3, "env additions " + JSON.stringify(envAdditions));
+    let opts = { command: meekClientPath,
+                 arguments: args,
+                 workdir: workDir.path,
+                 environmentAppend: true,
+                 environment: envAdditions,
+                 stderr: "pipe" };
+    return Subprocess.call(opts)
+      .then(aProc =>
+      {
+        this.mMeekClientProcess = aProc;
+        aProc.wait()
+          .then(aExitObj =>
+          {
+            this.mMeekClientProcess = undefined;
+            TorLauncherLogger.log(3, "The meek client exited");
+          });
+
+          this.mState = this.kStateWaitingForVersion;
+          TorLauncherLogger.log(3, "The meek client process has been started");
+          this._startStderrLogger();
+          return this._meekClientHandshake(aProc);
+      });
+  }, // _startMeekClient
+
+  // Returns a promise that is resolved when the PT handshake finishes.
+  _meekClientHandshake: function(aMeekClientProc)
+  {
+    return new Promise((aResolve, aReject) =>
+    {
+      this._startStdoutRead(aResolve, aReject);
+    });
+  },
+
+  _startStdoutRead: function(aResolve, aReject)
+  {
+    if (!this.mMeekClientProcess)
+      throw new Error("No meek client process.");
+
+    let readPromise = this.mMeekClientProcess.stdout.readString();
+    readPromise
+      .then(aStr =>
+      {
+        if (!aStr || (aStr.length == 0))
+        {
+          let err = "The meek client exited unexpectedly during the pluggable transport handshake.";
+          TorLauncherLogger.log(3, err);
+          throw new Error(err);
+        }
+
+        TorLauncherLogger.log(2, "meek client stdout: " + aStr);
+        if (!this.mMeekClientStdoutBuffer)
+          this.mMeekClientStdoutBuffer = aStr;
+        else
+          this.mMeekClientStdoutBuffer += aStr;
+
+        if (this._processStdoutLines())
+        {
+          aResolve();
+        }
+        else
+        {
+          // The PT handshake has not finished yet. Read more data.
+          this._startStdoutRead(aResolve, aReject);
+        }
+      })
+      .catch(aErr =>
+      {
+        aReject(this.mUserCanceled ? Cr.NS_ERROR_ABORT : aErr);
+      });
+  }, // _startStdoutRead
+
+  _startStderrLogger: function()
+  {
+    if (!this.mMeekClientProcess)
+      return;
+
+    let readPromise = this.mMeekClientProcess.stderr.readString();
+    readPromise
+      .then(aStr =>
+      {
+        if (aStr)
+        {
+          TorLauncherLogger.log(5, "meek client stderr: " + aStr);
+          this._startStderrLogger();
+        }
+      });
+  }, // _startStderrLogger
+
+  // May throw. Returns true when the PT handshake is complete.
+  // Conforms to the parent process role of the PT protocol.
+  // See: https://gitweb.torproject.org/torspec.git/tree/pt-spec.txt
+  _processStdoutLines: function()
+  {
+    if (!this.mMeekClientStdoutBuffer)
+      throw new Error("The stdout buffer is missing.");
+
+    let idx = this.mMeekClientStdoutBuffer.indexOf('\n');
+    while (idx >= 0)
+    {
+      let line = this.mMeekClientStdoutBuffer.substring(0, idx);
+      let tokens = line.split(' ');
+      this.mMeekClientStdoutBuffer =
+                            this.mMeekClientStdoutBuffer.substring(idx + 1);
+      idx = this.mMeekClientStdoutBuffer.indexOf('\n');
+
+      // Per the PT specification, unknown keywords are ignored.
+      let keyword = tokens[0];
+      let errMsg;
+      switch (this.mState) {
+        case this.kStateWaitingForVersion:
+          if (keyword == "VERSION")
+          {
+            if (this.mLocalProxyURL)
+              this.mState = this.kStateWaitingForProxyDone;
+            else
+              this.mState = this.kStateWaitingForCMethod;
+          }
+          else if (keyword == "VERSION-ERROR")
+          {
+            throw new Error("Unsupported pluggable transport version.");
+          }
+          break;
+        case this.kStateWaitingForProxyDone:
+          if ((keyword == "ENV-ERROR") || (keyword == "PROXY-ERROR"))
+            throw new Error(line);
+
+          if ((keyword == "PROXY") &&
+              (tokens.length > 1) && (tokens[1] == "DONE"))
+          {
+            this.mState = this.kStateWaitingForCMethod;
+          }
+          break;
+        case this.kStateWaitingForCMethod:
+          if (keyword == "ENV-ERROR")
+            throw new Error(line);
+
+          if (keyword == "CMETHOD")
+          {
+            if (tokens.length != 4)
+            {
+              errMsg = "Invalid CMETHOD response (too few parameters).";
+            }
+            else if (tokens[1] != this.kTransport)
+            {
+              errMsg = "Unexpected transport " + tokens[1]
+                       + " in CMETHOD response.";
+            }
+            else
+            {
+              let proxyType = tokens[2];
+              if (proxyType == "socks5")
+              {
+                this.mMeekClientProxyType = "socks";
+              }
+              else if ((proxyType == "socks4a") || (proxyType == "socks4"))
+              {
+                this.mMeekClientProxyType = "socks4";
+              }
+              else
+              {
+                errMsg = "Unexpected proxy type " + proxyType +
+                         " in CMETHOD response.";
+                break;
+              }
+              let addrPort = tokens[3];
+              let colonIdx = addrPort.indexOf(':');
+              if (colonIdx < 1)
+              {
+                errMsg = "Missing port in CMETHOD response.";
+              }
+              else
+              {
+                this.mMeekClientIP = addrPort.substring(0, colonIdx);
+                this.mMeekClientPort =
+                                parseInt(addrPort.substring(colonIdx + 1));
+              }
+            }
+          }
+          else if (keyword == "CMETHOD-ERROR")
+          {
+            if (tokens.length < 3)
+            {
+              errMsg = "Invalid CMETHOD-ERROR response (too few parameters).";
+            }
+            else
+            {
+              errMsg = tokens[1] + " not available: "
+                       + tokens.slice(2).join(' ');
+            }
+          }
+          else if ((keyword == "CMETHODS") && (tokens.length > 1) &&
+                   (tokens[1] == "DONE"))
+          {
+            this.mState = this.kStateInitialized;
+          }
+          break;
+      }
+
+      if (errMsg)
+        throw new Error(errMsg);
+    }
+
+    if (this.mState == this.kStateInitialized)
+    {
+      TorLauncherLogger.log(2, "meek client proxy type: "
+                            + this.mMeekClientProxyType);
+      TorLauncherLogger.log(2, "meek client proxy IP: "
+                            + this.mMeekClientIP);
+      TorLauncherLogger.log(2, "meek client proxy port: "
+                            + this.mMeekClientPort);
+    }
+
+    return (this.mState == this.kStateInitialized);
+  }, // _processStdoutLines
+
+  // Returns a promise.
+  // Based on meek/firefox/components/main.js
+  _sendMoatRequest: function(aRequestObj, aIsCheck)
+  {
+    let proxyPS = Cc["@mozilla.org/network/protocol-proxy-service;1"]
+                  .getService(Ci.nsIProtocolProxyService);
+    let flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+    let noTimeout = 0xFFFFFFFF; // UINT32_MAX
+    let proxyInfo = proxyPS.newProxyInfo(this.mMeekClientProxyType,
+                              this.mMeekClientIP, this.mMeekClientPort,
+                              flags, noTimeout, undefined);
+    let uriStr = TorLauncherUtil.getCharPref(this.kPrefMoatService);
+    if (!uriStr)
+    {
+      return Promise.reject(
+                  new Error("Missing value for " + this.kPrefMoatService));
+    }
+
+    uriStr += (aIsCheck) ? this.kMoatCheckURLPath : this.kMoatFetchURLPath;
+    let uri = Services.io.newURI(uriStr);
+
+    // There does not seem to be a way to directly create an nsILoadInfo from
+    // JavaScript, so we create a throw away non-proxied channel to get one.
+    let loadInfo = Services.io.newChannelFromURI2(uri, undefined,
+                        Services.scriptSecurityManager.getSystemPrincipal(),
+                        undefined,
+                        Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                        Ci.nsIContentPolicy.TYPE_OTHER).loadInfo;
+    let httpHandler = Services.io.getProtocolHandler("http")
+                                 .QueryInterface(Ci.nsIHttpProtocolHandler);
+    let ch = httpHandler.newProxiedChannel2(uri, proxyInfo, 0, undefined,
+                                 loadInfo).QueryInterface(Ci.nsIHttpChannel);
+
+    // Remove unwanted HTTP headers and set request parameters.
+    let headers = [];
+    ch.visitRequestHeaders({visitHeader: function(aKey, aValue) {
+        headers.push(aKey); }});
+    headers.forEach(aKey =>
+      {
+        if (aKey !== "Host")
+          ch.setRequestHeader(aKey, "", false);
+      });
+
+    // BridgeDB expects to receive an X-Forwarded-For header. If we are
+    // not using domain fronting (e.g., in a test setup), include a fake
+    // header value.
+    if (!this.mMeekFront)
+      ch.setRequestHeader("X-Forwarded-For", "1.2.3.4", false);
+
+    // Arrange for the POST data to be sent.
+    let requestData = JSON.stringify(aRequestObj);
+    let inStream = Cc["@mozilla.org/io/string-input-stream;1"]
+                   .createInstance(Ci.nsIStringInputStream);
+    inStream.setData(requestData, requestData.length);
+    let upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
+    upChannel.setUploadStream(inStream, this.kMoatContentType,
+                              requestData.length);
+    ch.requestMethod = "POST";
+
+    return new Promise((aResolve, aReject) =>
+      {
+        this.mMoatResponseListener =
+            new _MoatResponseListener(this, ch, aIsCheck, aResolve, aReject);
+        TorLauncherLogger.log(1, "Moat JSON request: " + requestData);
+        ch.asyncOpen(this.mMoatResponseListener, ch);
+      });
+  } // _sendMoatRequest
+};
+
+
+// _MoatResponseListener is an HTTP stream listener that knows how to
+// process Moat /fetch and /check responses.
+function _MoatResponseListener(aRequestor, aChannel, aIsCheck,
+                               aResolve, aReject)
+{
+  this.mRequestor = aRequestor;
+  this.mChannel = aChannel;
+  this.mIsCheck = aIsCheck;
+  this.mResolveCallback = aResolve;
+  this.mRejectCallback = aReject;
+}
+
+
+_MoatResponseListener.prototype =
+{
+  mRequestor: undefined,
+  mChannel: undefined,
+  mIsCheck: false,
+  mResolveCallback: undefined,
+  mRejectCallback: undefined,
+  mResponseLength: 0,
+  mResponseBody: undefined,
+
+  onStartRequest: function(aRequest, aContext)
+  {
+    this.mResponseLength = 0;
+    this.mResponseBody = "";
+  },
+
+  onStopRequest: function(aRequest, aContext, aStatus)
+  {
+    this.mChannel = undefined;
+
+    if (!Components.isSuccessCode(aStatus))
+    {
+      this.mRejectCallback(new TorLauncherBridgeDB.error(aStatus,
+                        TorLauncherUtil.getLocalizedStringForError(aStatus)));
+      return;
+    }
+
+    let statusCode, msg;
+    try
+    {
+      statusCode = aContext.responseStatus;
+      if (aContext.responseStatusText)
+        msg = statusCode + " " + aContext.responseStatusText;
+    }
+    catch (e) {}
+
+    TorLauncherLogger.log(3, "Moat response HTTP status: " + statusCode);
+    if (statusCode != 200)
+    {
+      this.mRejectCallback(new TorLauncherBridgeDB.error(statusCode, msg));
+      return;
+    }
+
+    TorLauncherLogger.log(1, "Moat JSON response: " + this.mResponseBody);
+
+    try
+    {
+      // Parse the response. We allow response.data to be an array or object.
+      let response = JSON.parse(this.mResponseBody);
+      if (response.data && Array.isArray(response.data))
+        response.data = response.data[0];
+
+      let errCode = 400;
+      let errStr;
+      if (!response.data)
+      {
+        if (response.errors && Array.isArray(response.errors))
+        {
+          errCode = response.errors[0].code;
+          errStr = response.errors[0].detail;
+          if (this.mIsCheck && (errCode == 404))
+            errStr = TorLauncherUtil.getLocalizedString("no_bridges_available");
+        }
+        else
+        {
+          errStr = "missing data in Moat response";
+        }
+      }
+      else if (response.data.version !== this.mRequestor.kMoatVersion)
+      {
+        errStr = "unexpected version";
+      }
+
+      if (errStr)
+        this.mRejectCallback(new TorLauncherBridgeDB.error(errCode, errStr));
+      else if (!this.mIsCheck)
+        this._parseFetchResponse(response);
+      else
+        this._parseCheckResponse(response);
+    }
+    catch(e)
+    {
+      TorLauncherLogger.log(3, "received invalid JSON: " + e);
+      this.mRejectCallback(e);
+    }
+  }, // onStopRequest
+
+  onDataAvailable: function(aRequest, aContext, aStream, aSrcOffset, aLength)
+  {
+    TorLauncherLogger.log(2, "Moat onDataAvailable: " + aLength + " bytes");
+    if ((this.mResponseLength + aLength) > this.mRequestor.kMaxResponseLength)
+    {
+      aRequest.cancel(Cr.NS_ERROR_FAILURE);
+      this.mChannel = undefined;
+      this.mRejectCallback(new TorLauncherBridgeDB.error(500,
+                                                  "Moat response too large"));
+      return;
+    }
+
+    this.mResponseLength += aLength;
+    let scriptableStream =  Cc["@mozilla.org/scriptableinputstream;1"]
+                             .createInstance(Ci.nsIScriptableInputStream);
+    scriptableStream.init(aStream);
+    this.mResponseBody += scriptableStream.read(aLength);
+  },
+
+  cancelMoatRequest: function()
+  {
+    let didCancel = false;
+    let rv = Cr.NS_ERROR_ABORT;
+    if (this.mChannel)
+    {
+      this.mChannel.cancel(rv);
+      this.mChannel = undefined;
+      didCancel = true;
+    }
+
+    this.mRejectCallback(rv);
+    return didCancel;
+  },
+
+  _parseFetchResponse: function(aResponse)
+  {
+    /*
+     * Expected response if successful:
+     * {
+     *   "data": {
+     *     "id": "1",
+     *     "type": "moat-challenge",
+     *     "version": "0.1.0",
+     *     "transport": TRANSPORT,
+     *     "image": CAPTCHA,
+     *     "challenge": CHALLENGE
+     *    }
+     * }
+     *
+     * If there is no overlap between the type of bridge we requested and
+     * the transports which BridgeDB supports, the response is the same except
+     * the transport property will contain an array of supported transports:
+     *     ...
+     *     "transport": [ "TRANSPORT", "TRANSPORT", ... ],
+     *     ...
+     */
+
+    // We do not check aResponse.id because it may vary.
+    let errStr;
+    if (aResponse.data.type !== this.mRequestor.kMoatFetchResponseType)
+      errStr = "unexpected response type";
+    else if (!aResponse.data.transport)
+      errStr = "missing transport";
+    else if (!aResponse.data.challenge)
+      errStr = "missing challenge";
+    else if (!aResponse.data.image)
+      errStr = "missing CAPTCHA image";
+
+    if (errStr)
+    {
+      this.mRejectCallback(new TorLauncherBridgeDB.error(500, errStr));
+    }
+    else
+    {
+      let imageURI = "data:image/jpeg;base64,"
+                     + encodeURIComponent(aResponse.data.image);
+      // If there was no overlap between the bridge type we requested and what
+      // BridgeDB has, we use the first type that BridgeDB can provide.
+      let t = aResponse.data.transport;
+      if (Array.isArray(t))
+        t = t[0];
+      this.mResolveCallback({ captchaImage: imageURI,
+                              transport: t,
+                              challenge: aResponse.data.challenge });
+    }
+  }, // _parseFetchResponse
+
+  _parseCheckResponse: function(aResponse)
+  {
+    /*
+     * Expected response if successful:
+     * {
+     *   "data": {
+     *     "id": "3",
+     *     "type": "moat-bridges",
+     *     "version": "0.1.0",
+     *     "bridges": [ "BRIDGE_LINE", ... ],
+     *     "qrcode": "QRCODE"
+     *   }
+     * }
+     */
+
+    // We do not check aResponse.id because it may vary.
+    // To be robust, we treat a zero-length bridge array the same as the 404
+    // error (no bridges available), which is handled inside onStopRequest().
+    let errStr;
+    if (aResponse.data.type !== this.mRequestor.kMoatCheckResponseType)
+      errStr = "unexpected response type";
+    else if (!aResponse.data.bridges || (aResponse.data.bridges.length == 0))
+      errStr = TorLauncherUtil.getLocalizedString("no_bridges_available");
+
+    if (errStr)
+      this.mRejectCallback(new TorLauncherBridgeDB.error(500, errStr));
+    else
+      this.mResolveCallback({ bridges: aResponse.data.bridges });
+  } // _parseCheckResponse
+};
diff --git a/src/modules/tl-util.jsm b/src/modules/tl-util.jsm
index bb84bdf..a79e2bd 100644
--- a/src/modules/tl-util.jsm
+++ b/src/modules/tl-util.jsm
@@ -1,4 +1,4 @@
-// Copyright (c) 2017, The Tor Project, Inc.
+// Copyright (c) 2018, The Tor Project, Inc.
 // See LICENSE for licensing information.
 //
 // vim: set sw=2 sts=2 ts=8 et syntax=javascript:
@@ -12,8 +12,10 @@ let EXPORTED_SYMBOLS = [ "TorLauncherUtil" ];
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
+const Cr = Components.results;
 const kPropBundleURI = "chrome://torlauncher/locale/torlauncher.properties";
 const kPropNamePrefix = "torlauncher.";
+const kPrefBranchDefaultBridge = "extensions.torlauncher.default_bridge.";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TorLauncherLogger",
@@ -161,6 +163,24 @@ let TorLauncherUtil =  // Public
     return aStringName;
   },
 
+  getLocalizedStringForError: function(aNSResult)
+  {
+    for (let prop in Cr)
+    {
+      if (Cr[prop] == aNSResult)
+      {
+        let key = "nsresult." + prop;
+        let rv = this.getLocalizedString(key);
+        if (rv !== key)
+          return rv;
+
+        return prop;  // As a fallback, return the NS_ERROR... name.
+      }
+    }
+
+    return undefined;
+  },
+
   getLocalizedBootstrapStatus: function(aStatusObj, aKeyword)
   {
     if (!aStatusObj || !aKeyword)
@@ -276,6 +296,13 @@ let TorLauncherUtil =  // Public
     } catch (e) {}
   },
 
+  getPrefBranch: function(aBranchName)
+  {
+    return Cc["@mozilla.org/preferences-service;1"]
+             .getService(Ci.nsIPrefService)
+             .getBranch(aBranchName);
+  },
+
   // Currently, this returns a random permutation of an array, bridgeArray.
   // Later, we might want to change this function to weight based on the
   // bridges' bandwidths.
@@ -361,9 +388,7 @@ let TorLauncherUtil =  // Public
   {
     try
     {
-      var prefBranch = Cc["@mozilla.org/preferences-service;1"]
-                           .getService(Ci.nsIPrefService)
-                           .getBranch("extensions.torlauncher.default_bridge.");
+      var prefBranch = this.getPrefBranch(kPrefBranchDefaultBridge);
       var childPrefs = prefBranch.getChildList("", []);
       var typeArray = [];
       for (var i = 0; i < childPrefs.length; ++i)
@@ -390,9 +415,7 @@ let TorLauncherUtil =  // Public
 
     try
     {
-      var prefBranch = Cc["@mozilla.org/preferences-service;1"]
-                           .getService(Ci.nsIPrefService)
-                           .getBranch("extensions.torlauncher.default_bridge.");
+      var prefBranch = this.getPrefBranch(kPrefBranchDefaultBridge);
       var childPrefs = prefBranch.getChildList("", []);
       var bridgeArray = [];
       // The pref service seems to return the values in reverse order, so
@@ -430,11 +453,13 @@ let TorLauncherUtil =  // Public
 
     let isRelativePath = false;
     let isUserData = (aTorFileType != "tor") &&
+                     (aTorFileType != "pt-startup-dir") &&
                      (aTorFileType != "torrc-defaults");
     let isControlIPC = ("control_ipc" == aTorFileType);
     let isSOCKSIPC = ("socks_ipc" == aTorFileType);
     let isIPC = isControlIPC || isSOCKSIPC;
     let checkIPCPathLen = true;
+    let useAppDir = false;
 
     const kControlIPCFileName = "control.socket";
     const kSOCKSIPCFileName = "socks.socket";
@@ -523,6 +548,8 @@ let TorLauncherUtil =  // Public
         {
           if ("tor" == aTorFileType)
             path = "TorBrowser\\Tor\\tor.exe";
+          else if ("pt-startup-dir" == aTorFileType)
+            useAppDir = true;
           else if ("torrc-defaults" == aTorFileType)
             path = "TorBrowser\\Tor\\torrc-defaults";
           else if ("torrc" == aTorFileType)
@@ -534,6 +561,8 @@ let TorLauncherUtil =  // Public
         {
           if ("tor" == aTorFileType)
             path = "Contents/Resources/TorBrowser/Tor/tor";
+          else if ("pt-startup-dir" == aTorFileType)
+            path = "Contents/MacOS/Tor";
           else if ("torrc-defaults" == aTorFileType)
             path = "Contents/Resources/TorBrowser/Tor/torrc-defaults";
           else if ("torrc" == aTorFileType)
@@ -547,6 +576,8 @@ let TorLauncherUtil =  // Public
         {
           if ("tor" == aTorFileType)
             path = "TorBrowser/Tor/tor";
+          else if ("pt-startup-dir" == aTorFileType)
+            useAppDir = true;
           else if ("torrc-defaults" == aTorFileType)
             path = "TorBrowser/Tor/torrc-defaults";
           else if ("torrc" == aTorFileType)
@@ -562,6 +593,8 @@ let TorLauncherUtil =  // Public
         // This block is used for the non-TorBrowser-Data/ case.
         if ("tor" == aTorFileType)
           path = "Tor\\tor.exe";
+        else if ("pt-startup-dir" == aTorFileType)
+          useAppDir = true;
         else if ("torrc-defaults" == aTorFileType)
           path = "Data\\Tor\\torrc-defaults";
         else if ("torrc" == aTorFileType)
@@ -574,6 +607,8 @@ let TorLauncherUtil =  // Public
         // This block is also used for the non-TorBrowser-Data/ case.
         if ("tor" == aTorFileType)
           path = "Tor/tor";
+        else if ("pt-startup-dir" == aTorFileType)
+          useAppDir = true;
         else if ("torrc-defaults" == aTorFileType)
           path = "Data/Tor/torrc-defaults";
         else if ("torrc" == aTorFileType)
@@ -584,13 +619,17 @@ let TorLauncherUtil =  // Public
           path = "Data/Tor/" + ipcFileName;
       }
 
-      if (!path)
+      if (!path && !useAppDir)
         return null;
     }
 
     try
     {
-      if (path)
+      if (useAppDir)
+      {
+        torFile = TLUtilInternal._appDir.clone();
+      }
+      else if (path)
       {
         if (isRelativePath)
         {



More information about the tor-commits mailing list