[tbb-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 tbb-commits
mailing list