commit e921bb15681ac54c9e937b564d31a2a6ec2ceb33 Author: Kathy Brade brade@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@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) {