commit bd4082f2f1db8a1f1c135d6d1689242f7c659a19 Author: Richard Pospesel richard@torproject.org Date: Mon Sep 16 15:25:39 2019 -0700
Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#tor
This patch adds a new about:preferences#tor page which allows modifying bridge, proxy, and firewall settings from within Tor Browser. All of the functionality present in tor-launcher's Network Configuration panel is present:
- Setting built-in bridges - Requesting bridges from BridgeDB via moat - Using user-provided bridges - Configuring SOCKS4, SOCKS5, and HTTP/HTTPS proxies - Setting firewall ports - Viewing and Copying Tor's logs
In addition the following changes have been made:
- The Networking Settings in General preferences has been removed - TorStrings has been removed from the SecurityLevel component and moved into a common shared module also used by about:preferences#tor --- browser/components/moz.build | 1 + browser/components/preferences/in-content/main.js | 15 - browser/components/preferences/in-content/main.xul | 56 -- .../preferences/in-content/preferences.js | 2 + .../preferences/in-content/preferences.xul | 5 + .../components/preferences/in-content/privacy.js | 1 + .../securitylevel/content/securityLevel.js | 155 +--- .../torpreferences/content/parseFunctions.jsm | 76 ++ .../torpreferences/content/requestBridgeDialog.jsm | 220 ++++++ .../torpreferences/content/requestBridgeDialog.xul | 35 + .../torpreferences/content/torBridgeSettings.jsm | 325 +++++++++ .../torpreferences/content/torCategory.inc.xul | 8 + .../torpreferences/content/torFirewallSettings.jsm | 72 ++ .../torpreferences/content/torLogDialog.jsm | 65 ++ .../torpreferences/content/torLogDialog.xul | 22 + .../components/torpreferences/content/torPane.js | 802 +++++++++++++++++++++ .../components/torpreferences/content/torPane.xul | 119 +++ .../torpreferences/content/torPreferences.css | 63 ++ .../torpreferences/content/torPreferencesIcon.svg | 5 + .../torpreferences/content/torProxySettings.jsm | 245 +++++++ browser/components/torpreferences/jar.mn | 14 + browser/components/torpreferences/moz.build | 1 + browser/modules/BridgeDB.jsm | 110 +++ browser/modules/TorProtocolService.jsm | 203 ++++++ browser/modules/TorStrings.jsm | 326 +++++++++ browser/modules/moz.build | 3 + 26 files changed, 2755 insertions(+), 194 deletions(-)
diff --git a/browser/components/moz.build b/browser/components/moz.build index 111794a7532c..c0c9629cac65 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -56,6 +56,7 @@ DIRS += [ 'syncedtabs', 'uitour', 'urlbar', + 'torpreferences', 'translation', ]
diff --git a/browser/components/preferences/in-content/main.js b/browser/components/preferences/in-content/main.js index 845ef2f61e30..98c73c5ac119 100644 --- a/browser/components/preferences/in-content/main.js +++ b/browser/components/preferences/in-content/main.js @@ -365,16 +365,6 @@ var gMainPane = { }); this.updatePerformanceSettingsBox({ duringChangeEvent: false });
- let connectionSettingsLink = document.getElementById( - "connectionSettingsLearnMore" - ); - let connectionSettingsUrl = - Services.urlFormatter.formatURLPref("app.support.baseURL") + - "prefs-connection-settings"; - connectionSettingsLink.setAttribute("href", connectionSettingsUrl); - this.updateProxySettingsUI(); - initializeProxyUI(gMainPane); - if (Services.prefs.getBoolPref("intl.multilingual.enabled")) { gMainPane.initBrowserLocale(); } @@ -464,11 +454,6 @@ var gMainPane = { gMainPane.updateHardwareAcceleration.bind(gMainPane) ); setEventListener( - "connectionSettings", - "command", - gMainPane.showConnections - ); - setEventListener( "browserContainersCheckbox", "command", gMainPane.checkBrowserContainers diff --git a/browser/components/preferences/in-content/main.xul b/browser/components/preferences/in-content/main.xul index 83d64f26a62d..85a219ff783b 100644 --- a/browser/components/preferences/in-content/main.xul +++ b/browser/components/preferences/in-content/main.xul @@ -669,60 +669,4 @@ <label id="cfrFeaturesLearnMore" class="learnMore" data-l10n-id="browsing-cfr-recommendations-learn-more" is="text-link"/> </hbox> </groupbox> - -<hbox id="networkProxyCategory" - class="subcategory" - hidden="true" - data-category="paneGeneral"> - <html:h1 data-l10n-id="network-settings-title"/> -</hbox> - -<!-- Network Settings--> -<groupbox id="connectionGroup" data-category="paneGeneral" hidden="true"> - <label class="search-header" hidden="true"><html:h2 data-l10n-id="network-settings-title"/></label> - - <hbox align="center"> - <hbox align="center" flex="1"> - <description id="connectionSettingsDescription" control="connectionSettings"/> - <spacer width="5"/> - <label id="connectionSettingsLearnMore" class="learnMore" is="text-link" - data-l10n-id="network-proxy-connection-learn-more"> - </label> - <separator orient="vertical"/> - </hbox> - - <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> - <hbox> - <button id="connectionSettings" - is="highlightable-button" - class="accessory-button" - icon="network" - data-l10n-id="network-proxy-connection-settings" - searchkeywords="doh trr" - search-l10n-ids=" - connection-window.title, - connection-proxy-option-no.label, - connection-proxy-option-auto.label, - connection-proxy-option-system.label, - connection-proxy-option-manual.label, - connection-proxy-http, - connection-proxy-ssl, - connection-proxy-ftp, - connection-proxy-http-port, - connection-proxy-socks, - connection-proxy-socks4, - connection-proxy-socks5, - connection-proxy-noproxy, - connection-proxy-noproxy-desc, - connection-proxy-http-share.label, - connection-proxy-autotype.label, - connection-proxy-reload.label, - connection-proxy-autologin.label, - connection-proxy-socks-remote-dns.label, - connection-dns-over-https.label, - connection-dns-over-https-url-custom.label, - " /> - </hbox> - </hbox> -</groupbox> </html:template> diff --git a/browser/components/preferences/in-content/preferences.js b/browser/components/preferences/in-content/preferences.js index d2851f20d2ca..b5b5f87af459 100644 --- a/browser/components/preferences/in-content/preferences.js +++ b/browser/components/preferences/in-content/preferences.js @@ -13,6 +13,7 @@ /* import-globals-from findInPage.js */ /* import-globals-from ../../../base/content/utilityOverlay.js */ /* import-globals-from ../../../../toolkit/content/preferencesBindings.js */ +/* import-globals-from ../../torpreferences/content/torPane.js */ /* global MozXULElement */
"use strict"; @@ -92,6 +93,7 @@ function init_all() { document.getElementById("template-paneSync").remove(); } register_module("paneSearchResults", gSearchResultsPane); + register_module("paneTor", gTorPane); gSearchResultsPane.init(); gMainPane.preInit();
diff --git a/browser/components/preferences/in-content/preferences.xul b/browser/components/preferences/in-content/preferences.xul index 3b07e4596907..7a01443ab048 100644 --- a/browser/components/preferences/in-content/preferences.xul +++ b/browser/components/preferences/in-content/preferences.xul @@ -16,6 +16,7 @@ <?xml-stylesheet href="chrome://browser/skin/preferences/in-content/containers.css"?> <?xml-stylesheet href="chrome://browser/skin/preferences/in-content/privacy.css"?> <?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?> +<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
<!DOCTYPE page [ <!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd"> @@ -138,6 +139,9 @@ <image class="category-icon"/> <label class="category-name" flex="1" data-l10n-id="pane-sync-title2"></label> </richlistitem> + +#include ../../torpreferences/content/torCategory.inc.xul + </richlistbox>
<spacer flex="1"/> @@ -195,6 +199,7 @@ #include privacy.xul #include containers.xul #include sync.xul +#include ../../torpreferences/content/torPane.xul </vbox> </vbox> </vbox> diff --git a/browser/components/preferences/in-content/privacy.js b/browser/components/preferences/in-content/privacy.js index e9112a2c467e..297d07fadf1f 100644 --- a/browser/components/preferences/in-content/privacy.js +++ b/browser/components/preferences/in-content/privacy.js @@ -62,6 +62,7 @@ XPCOMUtils.defineLazyGetter(this, "AlertsServiceDND", function() { } });
+// TODO: module import via ChromeUtils.defineModuleGetter XPCOMUtils.defineLazyScriptGetter( this, ["SecurityLevelPreferences"], diff --git a/browser/components/securitylevel/content/securityLevel.js b/browser/components/securitylevel/content/securityLevel.js index 9965046a7d15..7f307c5df43a 100644 --- a/browser/components/securitylevel/content/securityLevel.js +++ b/browser/components/securitylevel/content/securityLevel.js @@ -8,102 +8,11 @@ XPCOMUtils.defineLazyModuleGetters(this, { PanelMultiView: "resource:///modules/PanelMultiView.jsm", });
-XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser"]); -XPCOMUtils.defineLazyGetter(this, "domParser", () => { - const parser = new DOMParser(); - parser.forceEnableDTD(); - return parser; -}); - -/* - Security Level Strings - - Strings loaded from torbutton, but en-US defaults provided in case torbutton addon not enabled -*/ -XPCOMUtils.defineLazyGetter(this, "SecurityLevelStrings", function() { - // copied from testing/marionette/l10n.js - let localizeEntity = function(urls, id) { - // Build a string which contains all possible entity locations - let locations = []; - urls.forEach((url, index) => { - locations.push(`<!ENTITY % dtd_${index} SYSTEM "${url}">%dtd_${index};`); - }); - - // Use the DOM parser to resolve the entity and extract its real value - let header = `<?xml version="1.0"?><!DOCTYPE elem [${locations.join("")}]>`; - let elem = `<elem id="elementID">&${id};</elem>`; - let doc = domParser.parseFromString(header + elem, "text/xml"); - let element = doc.querySelector("elem[id='elementID']"); - - if (element === null) { - throw new Error(`Entity with id='${id}' hasn't been found`); - } - - return element.textContent; - }; - - let getString = function(key, fallback) { - try { - return localizeEntity( - ['chrome://torbutton/locale/torbutton.dtd'], - `torbutton.prefs.sec_${key}` - ); - } catch (e) { } - return fallback; - }; - - // read localized strings from torbutton; but use hard-coded en-US strings as fallbacks in case of error - let retval = { - securityLevel : getString("caption", "Security Level"), - customWarning : getString("custom_warning", "Custom"), - overview : getString("overview", "Disable certain web features that can be used to attack your security and anonymity."), - standard : { - level : getString("standard_label", "Standard"), - tooltip : getString("standard_tooltip", "Security Level : Standard"), - summary : getString("standard_description", "All Tor Browser and website features are enabled."), - }, - safer : { - level : getString("safer_label", "Safer"), - tooltip : getString("safer_tooltip", "Security Level : Safer"), - summary : getString("safer_description", "Disables website features that are often dangerous, causing some sites to lose functionality."), - description1 : getString("js_on_https_sites_only", "JavaScript is disabled on non-HTTPS sites."), - description2 : getString("limit_typography", "Some fonts and math symbols are disabled."), - description3 : getString("click_to_play_media", "Audio and video (HTML5 media), and WebGL are click-to-play."), - }, - safest : { - level : getString("safest_label", "Safest"), - tooltip : getString("safest_tooltip", "Security Level : Safest"), - summary : getString("safest_description", "Only allows website features required for static sites and basic services. These changes affect images, media, and scripts."), - description1 : getString("js_disabled", "JavaScript is disabled by default on all sites."), - description2 : getString("limit_graphics_and_typography", "Some fonts, icons, math symbols, and images are disabled."), - description3 : getString("click_to_play_media", "Audio and video (HTML5 media), and WebGL are click-to-play."), - }, - custom : { - summary : getString("custom_summary", "Your custom browser preferences have resulted in unusual security settings. For security and privacy reasons, we recommend you choose one of the default security levels."), - }, - learnMore : getString("learn_more_label", "Learn more"), - learnMoreURL : function() { - let locale = ""; - try { - let { getLocale } = - Cu.import("resource://torbutton/modules/utils.js", {}); - locale = getLocale(); - } catch(e) {} - - if (locale == "") { - locale = "en-US"; - } - - return "https://tb-manual.torproject.org/" + locale + "/security-settings/"; - }(), - restoreDefaults : getString("restore_defaults", "Restore Defaults"), - advancedSecuritySettings : getString("advanced_security_settings", "Advanced Security Settings\u2026"), - }; - - - return retval; -}); - +ChromeUtils.defineModuleGetter( + this, + "TorStrings", + "resource:///modules/TorStrings.jsm" +);
/* Security Level Prefs @@ -158,8 +67,8 @@ const SecurityLevelButton = {
_populateXUL : function(securityLevelButton) { if (securityLevelButton != null) { - securityLevelButton.setAttribute("tooltiptext", SecurityLevelStrings.securityLevel); - securityLevelButton.setAttribute("label", SecurityLevelStrings.securityLevel); + securityLevelButton.setAttribute("tooltiptext", TorStrings.securityLevel.securityLevel); + securityLevelButton.setAttribute("label", TorStrings.securityLevel.securityLevel); } },
@@ -171,15 +80,15 @@ const SecurityLevelButton = { switch(securitySlider) { case 4: classList.add("standard"); - securityLevelButton.setAttribute("tooltiptext", SecurityLevelStrings.standard.tooltip); + securityLevelButton.setAttribute("tooltiptext", TorStrings.securityLevel.standard.tooltip); break; case 2: classList.add("safer"); - securityLevelButton.setAttribute("tooltiptext", SecurityLevelStrings.safer.tooltip); + securityLevelButton.setAttribute("tooltiptext", TorStrings.securityLevel.safer.tooltip); break; case 1: classList.add("safest"); - securityLevelButton.setAttribute("tooltiptext", SecurityLevelStrings.safest.tooltip); + securityLevelButton.setAttribute("tooltiptext", TorStrings.securityLevel.safest.tooltip); break; } } @@ -294,12 +203,12 @@ const SecurityLevelPanel = { let buttonRestoreDefaults = panelview.querySelector("#securityLevel-restoreDefaults"); let buttonAdvancedSecuritySettings = panelview.querySelector("#securityLevel-advancedSecuritySettings");
- labelHeader.setAttribute("value", SecurityLevelStrings.securityLevel); - labelCustomWarning.setAttribute("value", SecurityLevelStrings.customWarning); - labelLearnMore.setAttribute("value", SecurityLevelStrings.learnMore); - labelLearnMore.setAttribute("href", SecurityLevelStrings.learnMoreURL); - buttonRestoreDefaults.setAttribute("label", SecurityLevelStrings.restoreDefaults); - buttonAdvancedSecuritySettings.setAttribute("label", SecurityLevelStrings.advancedSecuritySettings); + labelHeader.setAttribute("value", TorStrings.securityLevel.securityLevel); + labelCustomWarning.setAttribute("value", TorStrings.securityLevel.customWarning); + labelLearnMore.setAttribute("value", TorStrings.securityLevel.learnMore); + labelLearnMore.setAttribute("href", TorStrings.securityLevel.learnMoreURL); + buttonRestoreDefaults.setAttribute("label", TorStrings.securityLevel.restoreDefaults); + buttonAdvancedSecuritySettings.setAttribute("label", TorStrings.securityLevel.advancedSecuritySettings);
// rest of the XUL is set based on security prefs this._configUIFromPrefs(); @@ -328,24 +237,24 @@ const SecurityLevelPanel = { switch(securitySlider) { // standard case 4: - labelLevel.setAttribute("value", SecurityLevelStrings.standard.level); - summary.textContent = SecurityLevelStrings.standard.summary; + labelLevel.setAttribute("value", TorStrings.securityLevel.standard.level); + summary.textContent = TorStrings.securityLevel.standard.summary; break; // safer case 2: - labelLevel.setAttribute("value", SecurityLevelStrings.safer.level); - summary.textContent = SecurityLevelStrings.safer.summary; + labelLevel.setAttribute("value", TorStrings.securityLevel.safer.level); + summary.textContent = TorStrings.securityLevel.safer.summary; break; // safest case 1: - labelLevel.setAttribute("value", SecurityLevelStrings.safest.level); - summary.textContent = SecurityLevelStrings.safest.summary; + labelLevel.setAttribute("value", TorStrings.securityLevel.safest.level); + summary.textContent = TorStrings.securityLevel.safest.summary; break; }
// override the summary text with custom warning if (securityCustom) { - summary.textContent = SecurityLevelStrings.custom.summary; + summary.textContent = TorStrings.securityLevel.custom.summary; } },
@@ -425,14 +334,14 @@ const SecurityLevelPreferences = let groupbox = document.getElementById("securityLevel-groupbox");
let labelHeader = groupbox.querySelector("#securityLevel-header"); - labelHeader.setAttribute("value", SecurityLevelStrings.securityLevel); + labelHeader.setAttribute("value", TorStrings.securityLevel.securityLevel);
let spanOverview = groupbox.querySelector("#securityLevel-overview"); - spanOverview.textContent = SecurityLevelStrings.overview; + spanOverview.textContent = TorStrings.securityLevel.overview;
let labelLearnMore = groupbox.querySelector("#securityLevel-learnMore"); - labelLearnMore.setAttribute("value", SecurityLevelStrings.learnMore); - labelLearnMore.setAttribute("href", SecurityLevelStrings.learnMoreURL); + labelLearnMore.setAttribute("value", TorStrings.securityLevel.learnMore); + labelLearnMore.setAttribute("href", TorStrings.securityLevel.learnMoreURL);
let populateRadioElements = function(vboxQuery, stringStruct) { let vbox = groupbox.querySelector(vboxQuery); @@ -441,13 +350,13 @@ const SecurityLevelPreferences = radio.setAttribute("label", stringStruct.level);
let customWarning = vbox.querySelector("#securityLevel-customWarning"); - customWarning.setAttribute("value", SecurityLevelStrings.customWarning); + customWarning.setAttribute("value", TorStrings.securityLevel.customWarning);
let labelSummary = vbox.querySelector("#securityLevel-summary"); labelSummary.textContent = stringStruct.summary;
let labelRestoreDefaults = vbox.querySelector("#securityLevel-restoreDefaults"); - labelRestoreDefaults.setAttribute("value", SecurityLevelStrings.restoreDefaults); + labelRestoreDefaults.setAttribute("value", TorStrings.securityLevel.restoreDefaults);
let description1 = vbox.querySelector("#securityLevel-description1"); if (description1) { @@ -463,9 +372,9 @@ const SecurityLevelPreferences = } };
- populateRadioElements("#securityLevel-vbox-standard", SecurityLevelStrings.standard); - populateRadioElements("#securityLevel-vbox-safer", SecurityLevelStrings.safer); - populateRadioElements("#securityLevel-vbox-safest", SecurityLevelStrings.safest); + populateRadioElements("#securityLevel-vbox-standard", TorStrings.securityLevel.standard); + populateRadioElements("#securityLevel-vbox-safer", TorStrings.securityLevel.safer); + populateRadioElements("#securityLevel-vbox-safest", TorStrings.securityLevel.safest); },
_configUIFromPrefs : function() { diff --git a/browser/components/torpreferences/content/parseFunctions.jsm b/browser/components/torpreferences/content/parseFunctions.jsm new file mode 100644 index 000000000000..a6e6c554ca63 --- /dev/null +++ b/browser/components/torpreferences/content/parseFunctions.jsm @@ -0,0 +1,76 @@ +"use strict"; + +var EXPORTED_SYMBOLS = [ + "parsePort", + "parseAddrPort", + "parseUsernamePassword", + "parseAddrPortList", + "parseBridgeStrings", + "parsePortList", +]; + +// expects a string representation of an integer from 1 to 65535 +let parsePort = function(aPort) { + // ensure port string is a valid positive integer + const validIntRegex = /^[0-9]+$/; + if (!validIntRegex.test(aPort)) { + throw new Error(`Invalid PORT string : '${aPort}'`); + } + + // ensure port value is on valid range + let port = Number.parseInt(aPort); + if (port < 1 || port > 65535) { + throw new Error( + `Invalid PORT value, needs to be on range [1,65535] : '${port}'` + ); + } + + return port; +}; +// expects a string in the format: "ADDRESS:PORT" +let parseAddrPort = function(aAddrColonPort) { + let tokens = aAddrColonPort.split(":"); + if (tokens.length != 2) { + throw new Error(`Invalid ADDRESS:PORT string : '${aAddrColonPort}'`); + } + let address = tokens[0]; + let port = parsePort(tokens[1]); + return [address, port]; +}; + +// expects a string in the format: "USERNAME:PASSWORD" +// split on the first colon and any subsequent go into password +let parseUsernamePassword = function(aUsernameColonPassword) { + let colonIndex = aUsernameColonPassword.indexOf(":"); + if (colonIndex < 0) { + // we don't log the contents of the potentially password containing string + throw new Error("Invalid USERNAME:PASSWORD string"); + } + + let username = aUsernameColonPassword.substring(0, colonIndex); + let password = aUsernameColonPassword.substring(colonIndex + 1); + + return [username, password]; +}; + +// expects tring in the format: ADDRESS:PORT,ADDRESS:PORT,... +// returns array of ports (as ints) +let parseAddrPortList = function(aAddrPortList) { + let addrPorts = aAddrPortList.split(","); + // parse ADDRESS:PORT string and only keep the port (second element in returned array) + let retval = addrPorts.map(addrPort => parseAddrPort(addrPort)[1]); + return retval; +}; + +// expects a '/n' delimited string of bridge string, which we split and trim +let parseBridgeStrings = function(aBridgeStrings) { + let splitStrings = aBridgeStrings.split("\n"); + return splitStrings.map(val => val.trim()); +}; + +// expecting a ',' delimited list of ints with possible white space between +// returns an array of ints +let parsePortList = function(aPortListString) { + let splitStrings = aPortListString.split(","); + return splitStrings.map(val => parsePort(val.trim())); +}; diff --git a/browser/components/torpreferences/content/requestBridgeDialog.jsm b/browser/components/torpreferences/content/requestBridgeDialog.jsm new file mode 100644 index 000000000000..b66272edf880 --- /dev/null +++ b/browser/components/torpreferences/content/requestBridgeDialog.jsm @@ -0,0 +1,220 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["RequestBridgeDialog"]; + +const { BridgeDB } = ChromeUtils.import("resource:///modules/BridgeDB.jsm"); +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); + +class RequestBridgeDialog { + constructor() { + this._dialog = null; + this._submitCommand = null; + this._submitButton = null; + this._dialogDescription = null; + this._captchaImage = null; + this._captchaEntryTextbox = null; + this._captchaRefreshCommand = null; + this._captchaRefreshButton = null; + this._incorrectCaptchaHbox = null; + this._incorrectCaptchaLabel = null; + this._bridges = []; + this._proxyURI = null; + } + + static get selectors() { + return { + submitButton: + "accept" /* not really a selector but a key for dialog's getButton */, + dialogDescription: "description#torPreferences-requestBridge-description", + submitCommand: "command#torPreferences-requestBridge-submitCommand", + captchaImage: "image#torPreferences-requestBridge-captchaImage", + captchaEntryTextbox: + "textbox#torPreferences-requestBridge-captchaTextbox", + refreshCaptchaCommand: + "command#torPreferences-requestBridge-refreshCaptchaCommand", + refreshCaptchaButton: + "button#torPreferences-requestBridge-refreshCaptchaButton", + incorrectCaptchaHbox: + "hbox#torPreferences-requestBridge-incorrectCaptchaHbox", + incorrectCaptchaLabel: + "label#torPreferences-requestBridge-incorrectCaptchaError", + }; + } + + _populateXUL(dialog) { + const selectors = RequestBridgeDialog.selectors; + + this._dialog = dialog; + this._dialog.setAttribute( + "title", + TorStrings.settings.requestBridgeDialogTitle + ); + // user may have opened a Request Bridge dialog in another tab, so update the + // CAPTCHA image or close out the dialog if we have a bridge list + this._dialog.addEventListener("focusin", () => { + const uri = BridgeDB.currentCaptchaImage; + const bridges = BridgeDB.currentBridges; + + // new captcha image + if (uri) { + this._setcaptchaImage(uri); + } else if (bridges) { + this._bridges = bridges; + this._submitButton.disabled = false; + this._dialog.acceptDialog(); + } + }); + + this._submitCommand = this._dialog.querySelector(selectors.submitCommand); + + this._submitButton = this._dialog.getButton(selectors.submitButton); + this._submitButton.setAttribute("label", TorStrings.settings.submitCaptcha); + this._submitButton.setAttribute("command", this._submitCommand.id); + this._submitButton.disabled = true; + + this._dialogDescription = this._dialog.querySelector( + selectors.dialogDescription + ); + this._dialogDescription.textContent = + TorStrings.settings.contactingBridgeDB; + + this._captchaImage = this._dialog.querySelector(selectors.captchaImage); + + // request captcha from bridge db + BridgeDB.requestNewCaptchaImage(this._proxyURI).then(uri => { + this._setcaptchaImage(uri); + }); + + this._captchaEntryTextbox = this._dialog.querySelector( + selectors.captchaEntryTextbox + ); + this._captchaEntryTextbox.setAttribute( + "placeholder", + TorStrings.settings.captchaTextboxPlaceholder + ); + this._captchaEntryTextbox.disabled = true; + this._captchaEntryTextbox.onkeypress = evt => { + const ENTER_KEY = 13; + if (evt.keyCode == ENTER_KEY) { + // logically same as pressing the 'submit' button of the parent dialog + this.onSubmitCaptcha(); + return false; + } + return true; + }; + // disable submit if entry textbox is empty + this._captchaEntryTextbox.oninput = () => { + this._submitButton.disabled = this._captchaEntryTextbox.value == ""; + }; + + this._captchaRefreshCommand = this._dialog.querySelector( + selectors.refreshCaptchaCommand + ); + this._captchaRefreshButton = this._dialog.querySelector( + selectors.refreshCaptchaButton + ); + this._captchaRefreshButton.setAttribute( + "command", + this._captchaRefreshCommand.id + ); + this._captchaRefreshButton.disabled = true; + + this._incorrectCaptchaHbox = this._dialog.querySelector( + selectors.incorrectCaptchaHbox + ); + this._incorrectCaptchaLabel = this._dialog.querySelector( + selectors.incorrectCaptchaLabel + ); + this._incorrectCaptchaLabel.setAttribute( + "value", + TorStrings.settings.incorrectCaptcha + ); + + return true; + } + + _setcaptchaImage(uri) { + if (uri != this._captchaImage.src) { + this._captchaImage.src = uri; + this._dialogDescription.textContent = TorStrings.settings.solveTheCaptcha; + this._setUIDisabled(false); + this._captchaEntryTextbox.focus(); + this._captchaEntryTextbox.select(); + } + } + + _setUIDisabled(disabled) { + this._submitButton.disabled = this._captchaGuessIsEmpty() || disabled; + this._captchaEntryTextbox.disabled = disabled; + this._captchaRefreshButton.disabled = disabled; + } + + _captchaGuessIsEmpty() { + return this._captchaEntryTextbox.value == ""; + } + + init(window, dialog) { + // defer to later until firefox has populated the dialog with all our elements + window.setTimeout(() => { + this._populateXUL(dialog); + }, 0); + } + + close() { + BridgeDB.close(); + } + + /* + Event Handlers + */ + onSubmitCaptcha() { + let captchaText = this._captchaEntryTextbox.value.trim(); + // noop if the field is empty + if (captchaText == "") { + return; + } + + // freeze ui while we make request + this._setUIDisabled(true); + this._incorrectCaptchaHbox.style.visibility = "hidden"; + + BridgeDB.submitCaptchaGuess(captchaText) + .then(aBridges => { + this._bridges = aBridges; + + this._submitButton.disabled = false; + this._dialog.acceptDialog(); + }) + .catch(aError => { + this._bridges = []; + this._setUIDisabled(false); + this._incorrectCaptchaHbox.style.visibility = "visible"; + }); + } + + onRefreshCaptcha() { + this._setUIDisabled(true); + this._captchaImage.src = ""; + this._dialogDescription.textContent = + TorStrings.settings.contactingBridgeDB; + this._captchaEntryTextbox.value = ""; + this._incorrectCaptchaHbox.style.visibility = "hidden"; + + BridgeDB.requestNewCaptchaImage(this._proxyURI).then(uri => { + this._setcaptchaImage(uri); + }); + } + + openDialog(gSubDialog, aProxyURI, aCloseCallback) { + this._proxyURI = aProxyURI; + gSubDialog.open( + "chrome://browser/content/torpreferences/requestBridgeDialog.xul", + "resizable=yes", + this, + () => { + this.close(); + aCloseCallback(this._bridges); + } + ); + } +} diff --git a/browser/components/torpreferences/content/requestBridgeDialog.xul b/browser/components/torpreferences/content/requestBridgeDialog.xul new file mode 100644 index 000000000000..67cae40b9a48 --- /dev/null +++ b/browser/components/torpreferences/content/requestBridgeDialog.xul @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> +<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?> + +<dialog id="torPreferences-requestBridge-dialog" type="child" class="prefwindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Request Bridge" + buttons="cancel,accept" + role="dialog"> + <command id="torPreferences-requestBridge-submitCommand" oncommand="requestBridgeDialog.onSubmitCaptcha();"/> + <!-- ok, so ​ is a zero-width space. We need to have *something* in the innerText so that XUL knows how tall the + description node is so that it can determine how large to make the dialog element's inner draw area. If we have + nothing in the innerText, then it collapse to 0 height, and the contents of the dialog ends up partially hidden >:( --> + <description id="torPreferences-requestBridge-description">​</description> + <!-- init to transparent 400x125 png --> + <image id="torPreferences-requestBridge-captchaImage" flex="1"/> + <hbox id="torPreferences-requestBridge-inputHbox"> + <textbox id="torPreferences-requestBridge-captchaTextbox" flex="1" /> + <command id="torPreferences-requestBridge-refreshCaptchaCommand" oncommand="requestBridgeDialog.onRefreshCaptcha();"/> + <button id="torPreferences-requestBridge-refreshCaptchaButton" image="chrome://browser/skin/reload.svg"/> + </hbox> + <hbox id="torPreferences-requestBridge-incorrectCaptchaHbox" align="center"> + <image id="torPreferences-requestBridge-errorIcon" /> + <label id="torPreferences-requestBridge-incorrectCaptchaError" flex="1"/> + </hbox> + <script type="application/javascript"><![CDATA[ + "use strict"; + + let requestBridgeDialog = window.arguments[0]; + let dialog = document.getElementById("torPreferences-requestBridge-dialog"); + requestBridgeDialog.init(window, dialog); + ]]></script> +</dialog> \ No newline at end of file diff --git a/browser/components/torpreferences/content/torBridgeSettings.jsm b/browser/components/torpreferences/content/torBridgeSettings.jsm new file mode 100644 index 000000000000..ceb61d3ec972 --- /dev/null +++ b/browser/components/torpreferences/content/torBridgeSettings.jsm @@ -0,0 +1,325 @@ +"use strict"; + +var EXPORTED_SYMBOLS = [ + "TorBridgeSource", + "TorBridgeSettings", + "makeTorBridgeSettingsNone", + "makeTorBridgeSettingsBuiltin", + "makeTorBridgeSettingsBridgeDB", + "makeTorBridgeSettingsUserProvided", +]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { TorProtocolService } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); + +const TorBridgeSource = { + NONE: "NONE", + BUILTIN: "BUILTIN", + BRIDGEDB: "BRIDGEDB", + USERPROVIDED: "USERPROVIDED", +}; + +class TorBridgeSettings { + constructor() { + this._bridgeSource = TorBridgeSource.NONE; + this._selectedDefaultBridgeType = null; + this._bridgeStrings = []; + } + + get selectedDefaultBridgeType() { + if (this._bridgeSource == TorBridgeSource.BUILTIN) { + return this._selectedDefaultBridgeType; + } + return undefined; + } + + get bridgeSource() { + return this._bridgeSource; + } + + // for display + get bridgeStrings() { + return this._bridgeStrings.join("\n"); + } + + // raw + get bridgeStringsArray() { + return this._bridgeStrings; + } + + static get defaultBridgeTypes() { + if (TorBridgeSettings._defaultBridgeTypes) { + return TorBridgeSettings._defaultBridgeTypes; + } + + let bridgeListBranch = Services.prefs.getBranch( + TorStrings.preferenceBranches.defaultBridge + ); + let bridgePrefs = bridgeListBranch.getChildList("", {}); + + // an unordered set for shoving bridge types into + let bridgeTypes = new Set(); + // look for keys ending in ".N" and treat string before that as the bridge type + const pattern = /.[0-9]+$/; + for (const key of bridgePrefs) { + const offset = key.search(pattern); + if (offset != -1) { + const bt = key.substring(0, offset); + bridgeTypes.add(bt); + } + } + + // recommended bridge type goes first in the list + let recommendedBridgeType = Services.prefs.getCharPref( + TorStrings.preferenceKeys.recommendedBridgeType, + null + ); + + let retval = []; + if (recommendedBridgeType && bridgeTypes.has(recommendedBridgeType)) { + retval.push(recommendedBridgeType); + } + + for (const bridgeType of bridgeTypes.values()) { + if (bridgeType != recommendedBridgeType) { + retval.push(bridgeType); + } + } + + // cache off + TorBridgeSettings._defaultBridgeTypes = retval; + return retval; + } + + _readDefaultBridges(aBridgeType) { + let bridgeBranch = Services.prefs.getBranch( + TorStrings.preferenceBranches.defaultBridge + ); + let bridgeBranchPrefs = bridgeBranch.getChildList("", {}); + + let retval = []; + + // regex matches against strings ending in ".N" where N is a positive integer + let pattern = /.[0-9]+$/; + for (const key of bridgeBranchPrefs) { + // verify the location of the match is the correct offset required for aBridgeType + // to fit, and that the string begins with aBridgeType + if ( + key.search(pattern) == aBridgeType.length && + key.startsWith(aBridgeType) + ) { + let bridgeStr = bridgeBranch.getCharPref(key); + retval.push(bridgeStr); + } + } + + // fisher-yates shuffle + // shuffle so that Tor Browser users don't all try the built-in bridges in the same order + for (let i = retval.length - 1; i > 0; --i) { + // number n such that 0.0 <= n < 1.0 + const n = Math.random(); + // integer j such that 0 <= j <= i + const j = Math.floor(n * (i + 1)); + + // swap values at indices i and j + const tmp = retval[i]; + retval[i] = retval[j]; + retval[j] = tmp; + } + + return retval; + } + + _readBridgeDBBridges() { + let bridgeBranch = Services.prefs.getBranch( + `${TorStrings.preferenceBranches.bridgeDBBridges}` + ); + let bridgeBranchPrefs = bridgeBranch.getChildList("", {}); + // the child prefs do not come in any particular order so sort the keys + // so the values can be compared to what we get out off torrc + bridgeBranchPrefs.sort(); + + // just assume all of the prefs under the parent point to valid bridge string + let retval = bridgeBranchPrefs.map(key => + bridgeBranch.getCharPref(key).trim() + ); + + return retval; + } + + _readTorrcBridges() { + let bridgeList = TorProtocolService.readStringArraySetting( + TorStrings.configKeys.bridgeList + ); + + let retval = []; + for (const line of bridgeList) { + let trimmedLine = line.trim(); + if (trimmedLine) { + retval.push(trimmedLine); + } + } + + return retval; + } + + // analagous to initBridgeSettings() + readSettings() { + // restore to defaults + this._bridgeSource = TorBridgeSource.NONE; + this._selectedDefaultBridgeType = null; + this._bridgeStrings = []; + + // So the way tor-launcher determines the origin of the configured bridges is a bit + // weird and depends on inferring our scenario based on some firefox prefs and the + // relationship between the saved list of bridges in about:config vs the list saved in torrc + + // first off, if "extensions.torlauncher.default_bridge_type" is set to one of our + // builtin default types (obfs4, meek-azure, snowflake, etc) then we provide the + // bridges in "extensions.torlauncher.default_bridge.*" (filtered by our default_bridge_type) + + // next, we compare the list of bridges saved in torrc to the bridges stored in the + // "extensions.torlauncher.bridgedb_bridge."" branch. If they match *exactly* then we assume + // the bridges were retrieved from BridgeDB and use those. If the torrc list is empty then we know + // we have no bridge settings + + // finally, if none of the previous conditions are not met, it is assumed the bridges stored in + // torrc are user-provided + + // what we should(?) do once we excise tor-launcher entirely is explicitly store an int/enum in + // about:config that tells us which scenario we are in so we don't have to guess + + let defaultBridgeType = Services.prefs.getCharPref( + TorStrings.preferenceKeys.defaultBridgeType, + null + ); + + // check if source is BUILTIN + if (defaultBridgeType) { + this._bridgeStrings = this._readDefaultBridges(defaultBridgeType); + this._bridgeSource = TorBridgeSource.BUILTIN; + this._selectedDefaultBridgeType = defaultBridgeType; + return; + } + + let torrcBridges = this._readTorrcBridges(); + + // no stored bridges means no bridge is in use + if (torrcBridges.length == 0) { + this._bridgeStrings = []; + this._bridgeSource = TorBridgeSource.NONE; + return; + } + + let bridgedbBridges = this._readBridgeDBBridges(); + + // if these two lists are equal then we got our bridges from bridgedb + // ie: same element in identical order + let arraysEqual = (left, right) => { + if (left.length != right.length) { + return false; + } + const length = left.length; + for (let i = 0; i < length; ++i) { + if (left[i] != right[i]) { + return false; + } + } + return true; + }; + + // agreement between prefs and torrc means bridgedb bridges + if (arraysEqual(torrcBridges, bridgedbBridges)) { + this._bridgeStrings = torrcBridges; + this._bridgeSource = TorBridgeSource.BRIDGEDB; + return; + } + + // otherwise they must be user provided + this._bridgeStrings = torrcBridges; + this._bridgeSource = TorBridgeSource.USERPROVIDED; + } + + writeSettings() { + let settingsObject = new Map(); + + // init tor bridge settings to null + settingsObject.set(TorStrings.configKeys.useBridges, null); + settingsObject.set(TorStrings.configKeys.bridgeList, null); + + // clear bridge related firefox prefs + Services.prefs.setCharPref(TorStrings.preferenceKeys.defaultBridgeType, ""); + let bridgeBranch = Services.prefs.getBranch( + `${TorStrings.preferenceBranches.bridgeDBBridges}` + ); + let bridgeBranchPrefs = bridgeBranch.getChildList("", {}); + for (const pref of bridgeBranchPrefs) { + Services.prefs.clearUserPref( + `${TorStrings.preferenceBranches.bridgeDBBridges}${pref}` + ); + } + + switch (this._bridgeSource) { + case TorBridgeSource.BUILTIN: + // set builtin bridge type to use in prefs + Services.prefs.setCharPref( + TorStrings.preferenceKeys.defaultBridgeType, + this._selectedDefaultBridgeType + ); + break; + case TorBridgeSource.BRIDGEDB: + // save bridges off to prefs + for (let i = 0; i < this.bridgeStringsArray.length; ++i) { + Services.prefs.setCharPref( + `${TorStrings.preferenceBranches.bridgeDBBridges}${i}`, + this.bridgeStringsArray[i] + ); + } + break; + } + + // write over our bridge list if bridges are enabled + if (this._bridgeSource != TorBridgeSource.NONE) { + settingsObject.set(TorStrings.configKeys.useBridges, true); + settingsObject.set( + TorStrings.configKeys.bridgeList, + this.bridgeStringsArray + ); + } + TorProtocolService.writeSettings(settingsObject); + } +} + +function makeTorBridgeSettingsNone() { + return new TorBridgeSettings(); +} + +function makeTorBridgeSettingsBuiltin(aBridgeType) { + let retval = new TorBridgeSettings(); + retval._bridgeSource = TorBridgeSource.BUILTIN; + retval._selectedDefaultBridgeType = aBridgeType; + retval._bridgeStrings = retval._readDefaultBridges(aBridgeType); + + return retval; +} + +function makeTorBridgeSettingsBridgeDB(aBridges) { + let retval = new TorBridgeSettings(); + retval._bridgeSource = TorBridgeSource.BRIDGEDB; + retval._selectedDefaultBridgeType = null; + retval._bridgeStrings = aBridges; + + return retval; +} + +function makeTorBridgeSettingsUserProvided(aBridges) { + let retval = new TorBridgeSettings(); + retval._bridgeSource = TorBridgeSource.USERPROVIDED; + retval._selectedDefaultBridgeType = null; + retval._bridgeStrings = aBridges; + + return retval; +} diff --git a/browser/components/torpreferences/content/torCategory.inc.xul b/browser/components/torpreferences/content/torCategory.inc.xul new file mode 100644 index 000000000000..746059358d5f --- /dev/null +++ b/browser/components/torpreferences/content/torCategory.inc.xul @@ -0,0 +1,8 @@ +<richlistitem id="category-tor" + class="category" + value="paneTor" + helpTopic="prefs-tor" + align="center"> + <image class="category-icon"/> + <label id="torPreferences-labelCategory" class="category-name" flex="1" value="Tor"/> +</richlistitem> diff --git a/browser/components/torpreferences/content/torFirewallSettings.jsm b/browser/components/torpreferences/content/torFirewallSettings.jsm new file mode 100644 index 000000000000..e77f18ef2fae --- /dev/null +++ b/browser/components/torpreferences/content/torFirewallSettings.jsm @@ -0,0 +1,72 @@ +"use strict"; + +var EXPORTED_SYMBOLS = [ + "TorFirewallSettings", + "makeTorFirewallSettingsNone", + "makeTorFirewallSettingsCustom", +]; + +const { TorProtocolService } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); +const { parseAddrPortList } = ChromeUtils.import( + "chrome://browser/content/torpreferences/parseFunctions.jsm" +); + +class TorFirewallSettings { + constructor() { + this._allowedPorts = []; + } + + get portsConfigurationString() { + let portStrings = this._allowedPorts.map(port => `*:${port}`); + return portStrings.join(","); + } + + get commaSeparatedListString() { + return this._allowedPorts.join(","); + } + + get hasPorts() { + return this._allowedPorts.length > 0; + } + + readSettings() { + let addressPortList = TorProtocolService.readStringSetting( + TorStrings.configKeys.reachableAddresses + ); + + let allowedPorts = []; + if (addressPortList) { + allowedPorts = parseAddrPortList(addressPortList); + } + this._allowedPorts = allowedPorts; + } + + writeSettings() { + let settingsObject = new Map(); + + // init to null so Tor daemon resets if no ports + settingsObject.set(TorStrings.configKeys.reachableAddresses, null); + + if (this._allowedPorts.length > 0) { + settingsObject.set( + TorStrings.configKeys.reachableAddresses, + this.portsConfigurationString + ); + } + + TorProtocolService.writeSettings(settingsObject); + } +} + +function makeTorFirewallSettingsNone() { + return new TorFirewallSettings(); +} + +function makeTorFirewallSettingsCustom(aPortsList) { + let retval = new TorFirewallSettings(); + retval._allowedPorts = aPortsList; + return retval; +} diff --git a/browser/components/torpreferences/content/torLogDialog.jsm b/browser/components/torpreferences/content/torLogDialog.jsm new file mode 100644 index 000000000000..13a8c42884d2 --- /dev/null +++ b/browser/components/torpreferences/content/torLogDialog.jsm @@ -0,0 +1,65 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["TorLogDialog"]; + +const { TorProtocolService } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); + +class TorLogDialog { + constructor() { + this._dialog = null; + this._logTextarea = null; + this._copyLogButton = null; + } + + static get selectors() { + return { + copyLogButton: "extra1", + logTextarea: "textarea#torPreferences-torDialog-textarea", + }; + } + + _populateXUL(aDialog) { + this._dialog = aDialog; + this._dialog.setAttribute("title", TorStrings.settings.torLogDialogTitle); + + this._logTextarea = this._dialog.querySelector( + TorLogDialog.selectors.logTextarea + ); + + this._copyLogButton = this._dialog.getButton( + TorLogDialog.selectors.copyLogButton + ); + this._copyLogButton.setAttribute("label", TorStrings.settings.copyLog); + this._copyLogButton.addEventListener("command", () => { + this.copyTorLog(); + }); + + this._logTextarea.value = TorProtocolService.getLog(); + } + + init(window, aDialog) { + // defer to later until firefox has populated the dialog with all our elements + window.setTimeout(() => { + this._populateXUL(aDialog); + }, 0); + } + + copyTorLog() { + // Copy tor log messages to the system clipboard. + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + clipboard.copyString(this._logTextarea.value); + } + + openDialog(gSubDialog) { + gSubDialog.open( + "chrome://browser/content/torpreferences/torLogDialog.xul", + "resizable=yes", + this + ); + } +} diff --git a/browser/components/torpreferences/content/torLogDialog.xul b/browser/components/torpreferences/content/torLogDialog.xul new file mode 100644 index 000000000000..ae0f4b294204 --- /dev/null +++ b/browser/components/torpreferences/content/torLogDialog.xul @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> +<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?> + +<dialog id="torPreferences-torLog-dialog" type="child" class="prefwindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + buttons="accept,extra1" + role="dialog"> + <html:textarea + id="torPreferences-torDialog-textarea" + multiline="true" + readonly="true"/> + <script type="application/javascript"><![CDATA[ + "use strict"; + + let torLogDialog = window.arguments[0]; + let dialog = document.getElementById("torPreferences-torLog-dialog"); + torLogDialog.init(window, dialog); + ]]></script> +</dialog> \ No newline at end of file diff --git a/browser/components/torpreferences/content/torPane.js b/browser/components/torpreferences/content/torPane.js new file mode 100644 index 000000000000..08de0613e1d4 --- /dev/null +++ b/browser/components/torpreferences/content/torPane.js @@ -0,0 +1,802 @@ +"use strict"; + +const { TorProtocolService } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); + +const { + TorBridgeSource, + TorBridgeSettings, + makeTorBridgeSettingsNone, + makeTorBridgeSettingsBuiltin, + makeTorBridgeSettingsBridgeDB, + makeTorBridgeSettingsUserProvided, +} = ChromeUtils.import( + "chrome://browser/content/torpreferences/torBridgeSettings.jsm" +); + +const { + TorProxyType, + TorProxySettings, + makeTorProxySettingsNone, + makeTorProxySettingsSocks4, + makeTorProxySettingsSocks5, + makeTorProxySettingsHTTPS, +} = ChromeUtils.import( + "chrome://browser/content/torpreferences/torProxySettings.jsm" +); +const { + TorFirewallSettings, + makeTorFirewallSettingsNone, + makeTorFirewallSettingsCustom, +} = ChromeUtils.import( + "chrome://browser/content/torpreferences/torFirewallSettings.jsm" +); + +const { TorLogDialog } = ChromeUtils.import( + "chrome://browser/content/torpreferences/torLogDialog.jsm" +); + +const { RequestBridgeDialog } = ChromeUtils.import( + "chrome://browser/content/torpreferences/requestBridgeDialog.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "TorStrings", + "resource:///modules/TorStrings.jsm" +); + +const { parsePort, parseBridgeStrings, parsePortList } = ChromeUtils.import( + "chrome://browser/content/torpreferences/parseFunctions.jsm" +); + +/* + Tor Pane + + Code for populating the XUL in about:preferences#tor, handling input events, interfacing with tor-launcher +*/ +const gTorPane = (function() { + /* CSS selectors for all of the Tor Network DOM elements we need to access */ + const selectors = { + category: { + title: "label#torPreferences-labelCategory", + }, + torPreferences: { + header: "h1#torPreferences-header", + description: "span#torPreferences-description", + learnMore: "label#torPreferences-learnMore", + }, + bridges: { + header: "h2#torPreferences-bridges-header", + description: "span#torPreferences-bridges-description", + learnMore: "label#torPreferences-bridges-learnMore", + useBridgeCheckbox: "checkbox#torPreferences-bridges-toggle", + bridgeSelectionRadiogroup: + "radiogroup#torPreferences-bridges-bridgeSelection", + builtinBridgeOption: "radio#torPreferences-bridges-radioBuiltin", + builtinBridgeList: "menulist#torPreferences-bridges-builtinList", + requestBridgeOption: "radio#torPreferences-bridges-radioRequestBridge", + requestBridgeButton: "button#torPreferences-bridges-buttonRequestBridge", + requestBridgeTextarea: + "textarea#torPreferences-bridges-textareaRequestBridge", + provideBridgeOption: "radio#torPreferences-bridges-radioProvideBridge", + provideBridgeDescription: + "description#torPreferences-bridges-descriptionProvideBridge", + provideBridgeTextarea: + "textarea#torPreferences-bridges-textareaProvideBridge", + }, + advanced: { + header: "h2#torPreferences-advanced-header", + description: "span#torPreferences-advanced-description", + learnMore: "label#torPreferences-advanced-learnMore", + useProxyCheckbox: "checkbox#torPreferences-advanced-toggleProxy", + proxyTypeLabel: "label#torPreferences-localProxy-type", + proxyTypeList: "menulist#torPreferences-localProxy-builtinList", + proxyAddressLabel: "label#torPreferences-localProxy-address", + proxyAddressTextbox: "textbox#torPreferences-localProxy-textboxAddress", + proxyPortLabel: "label#torPreferences-localProxy-port", + proxyPortTextbox: "input#torPreferences-localProxy-textboxPort", + proxyUsernameLabel: "label#torPreferences-localProxy-username", + proxyUsernameTextbox: "textbox#torPreferences-localProxy-textboxUsername", + proxyPasswordLabel: "label#torPreferences-localProxy-password", + proxyPasswordTextbox: "textbox#torPreferences-localProxy-textboxPassword", + useFirewallCheckbox: "checkbox#torPreferences-advanced-toggleFirewall", + firewallAllowedPortsLabel: "label#torPreferences-advanced-allowedPorts", + firewallAllowedPortsTextbox: + "textbox#torPreferences-advanced-textboxAllowedPorts", + torLogsLabel: "label#torPreferences-torLogs", + torLogsButton: "button#torPreferences-buttonTorLogs", + }, + }; /* selectors */ + + let retval = { + // cached frequently accessed DOM elements + _useBridgeCheckbox: null, + _bridgeSelectionRadiogroup: null, + _builtinBridgeOption: null, + _builtinBridgeMenulist: null, + _requestBridgeOption: null, + _requestBridgeButton: null, + _requestBridgeTextarea: null, + _provideBridgeOption: null, + _provideBridgeTextarea: null, + _useProxyCheckbox: null, + _proxyTypeLabel: null, + _proxyTypeMenulist: null, + _proxyAddressLabel: null, + _proxyAddressTextbox: null, + _proxyPortLabel: null, + _proxyPortTextbox: null, + _proxyUsernameLabel: null, + _proxyUsernameTextbox: null, + _proxyPasswordLabel: null, + _proxyPasswordTextbox: null, + _useFirewallCheckbox: null, + _allowedPortsLabel: null, + _allowedPortsTextbox: null, + + // tor network settings + _bridgeSettings: null, + _proxySettings: null, + _firewallSettings: null, + + // disables the provided list of elements + _setElementsDisabled(elements, disabled) { + for (let currentElement of elements) { + currentElement.disabled = disabled; + } + }, + + // populate xul with strings and cache the relevant elements + _populateXUL() { + // saves tor settings to disk when navigate away from about:preferences + window.addEventListener("blur", val => { + TorProtocolService.flushSettings(); + }); + + document + .querySelector(selectors.category.title) + .setAttribute("value", TorStrings.settings.categoryTitle); + + let prefpane = document.getElementById("mainPrefPane"); + + // Heading + prefpane.querySelector(selectors.torPreferences.header).innerText = + TorStrings.settings.torPreferencesHeading; + prefpane.querySelector(selectors.torPreferences.description).textContent = + TorStrings.settings.torPreferencesDescription; + { + let learnMore = prefpane.querySelector( + selectors.torPreferences.learnMore + ); + learnMore.setAttribute("value", TorStrings.settings.learnMore); + learnMore.setAttribute( + "href", + TorStrings.settings.learnMoreTorBrowserURL + ); + } + + // Bridge setup + prefpane.querySelector(selectors.bridges.header).innerText = + TorStrings.settings.bridgesHeading; + prefpane.querySelector(selectors.bridges.description).textContent = + TorStrings.settings.bridgesDescription; + { + let learnMore = prefpane.querySelector(selectors.bridges.learnMore); + learnMore.setAttribute("value", TorStrings.settings.learnMore); + learnMore.setAttribute("href", TorStrings.settings.learnMoreBridgesURL); + } + + this._useBridgeCheckbox = prefpane.querySelector( + selectors.bridges.useBridgeCheckbox + ); + this._useBridgeCheckbox.setAttribute( + "label", + TorStrings.settings.useBridge + ); + this._bridgeSelectionRadiogroup = prefpane.querySelector( + selectors.bridges.bridgeSelectionRadiogroup + ); + this._bridgeSelectionRadiogroup.value = TorBridgeSource.BUILTIN; + + // Builtin bridges + this._builtinBridgeOption = prefpane.querySelector( + selectors.bridges.builtinBridgeOption + ); + this._builtinBridgeOption.setAttribute( + "label", + TorStrings.settings.selectBridge + ); + this._builtinBridgeOption.setAttribute("value", TorBridgeSource.BUILTIN); + this._builtinBridgeMenulist = prefpane.querySelector( + selectors.bridges.builtinBridgeList + ); + + // Request bridge + this._requestBridgeOption = prefpane.querySelector( + selectors.bridges.requestBridgeOption + ); + this._requestBridgeOption.setAttribute( + "label", + TorStrings.settings.requestBridgeFromTorProject + ); + this._requestBridgeOption.setAttribute("value", TorBridgeSource.BRIDGEDB); + this._requestBridgeButton = prefpane.querySelector( + selectors.bridges.requestBridgeButton + ); + this._requestBridgeButton.setAttribute( + "label", + TorStrings.settings.requestNewBridge + ); + this._requestBridgeTextarea = prefpane.querySelector( + selectors.bridges.requestBridgeTextarea + ); + + // Provide a bridge + this._provideBridgeOption = prefpane.querySelector( + selectors.bridges.provideBridgeOption + ); + this._provideBridgeOption.setAttribute( + "label", + TorStrings.settings.provideBridge + ); + this._provideBridgeOption.setAttribute( + "value", + TorBridgeSource.USERPROVIDED + ); + prefpane.querySelector( + selectors.bridges.provideBridgeDescription + ).textContent = TorStrings.settings.provideBridgeDirections; + this._provideBridgeTextarea = prefpane.querySelector( + selectors.bridges.provideBridgeTextarea + ); + this._provideBridgeTextarea.setAttribute( + "placeholder", + TorStrings.settings.provideBridgePlaceholder + ); + + // Advanced setup + prefpane.querySelector(selectors.advanced.header).innerText = + TorStrings.settings.advancedHeading; + prefpane.querySelector(selectors.advanced.description).textContent = + TorStrings.settings.advancedDescription; + { + let learnMore = prefpane.querySelector(selectors.advanced.learnMore); + learnMore.setAttribute("value", TorStrings.settings.learnMore); + learnMore.setAttribute( + "href", + TorStrings.settings.learnMoreNetworkSettingsURL + ); + } + + // Local Proxy + this._useProxyCheckbox = prefpane.querySelector( + selectors.advanced.useProxyCheckbox + ); + this._useProxyCheckbox.setAttribute( + "label", + TorStrings.settings.useLocalProxy + ); + this._proxyTypeLabel = prefpane.querySelector( + selectors.advanced.proxyTypeLabel + ); + this._proxyTypeLabel.setAttribute("value", TorStrings.settings.proxyType); + + let mockProxies = [ + { + value: TorProxyType.SOCKS4, + label: TorStrings.settings.proxyTypeSOCKS4, + }, + { + value: TorProxyType.SOCKS5, + label: TorStrings.settings.proxyTypeSOCKS5, + }, + { value: TorProxyType.HTTPS, label: TorStrings.settings.proxyTypeHTTP }, + ]; + this._proxyTypeMenulist = prefpane.querySelector( + selectors.advanced.proxyTypeList + ); + for (let currentProxy of mockProxies) { + let menuEntry = document.createElement("menuitem"); + menuEntry.setAttribute("value", currentProxy.value); + menuEntry.setAttribute("label", currentProxy.label); + this._proxyTypeMenulist.querySelector("menupopup").append(menuEntry); + } + + this._proxyAddressLabel = prefpane.querySelector( + selectors.advanced.proxyAddressLabel + ); + this._proxyAddressLabel.setAttribute( + "value", + TorStrings.settings.proxyAddress + ); + this._proxyAddressTextbox = prefpane.querySelector( + selectors.advanced.proxyAddressTextbox + ); + this._proxyAddressTextbox.setAttribute( + "placeholder", + TorStrings.settings.proxyAddressPlaceholder + ); + this._proxyPortLabel = prefpane.querySelector( + selectors.advanced.proxyPortLabel + ); + this._proxyPortLabel.setAttribute("value", TorStrings.settings.proxyPort); + this._proxyPortTextbox = prefpane.querySelector( + selectors.advanced.proxyPortTextbox + ); + this._proxyUsernameLabel = prefpane.querySelector( + selectors.advanced.proxyUsernameLabel + ); + this._proxyUsernameLabel.setAttribute( + "value", + TorStrings.settings.proxyUsername + ); + this._proxyUsernameTextbox = prefpane.querySelector( + selectors.advanced.proxyUsernameTextbox + ); + this._proxyUsernameTextbox.setAttribute( + "placeholder", + TorStrings.settings.proxyUsernamePasswordPlaceholder + ); + this._proxyPasswordLabel = prefpane.querySelector( + selectors.advanced.proxyPasswordLabel + ); + this._proxyPasswordLabel.setAttribute( + "value", + TorStrings.settings.proxyPassword + ); + this._proxyPasswordTextbox = prefpane.querySelector( + selectors.advanced.proxyPasswordTextbox + ); + this._proxyPasswordTextbox.setAttribute( + "placeholder", + TorStrings.settings.proxyUsernamePasswordPlaceholder + ); + + // Local firewall + this._useFirewallCheckbox = prefpane.querySelector( + selectors.advanced.useFirewallCheckbox + ); + this._useFirewallCheckbox.setAttribute( + "label", + TorStrings.settings.useFirewall + ); + this._allowedPortsLabel = prefpane.querySelector( + selectors.advanced.firewallAllowedPortsLabel + ); + this._allowedPortsLabel.setAttribute( + "value", + TorStrings.settings.allowedPorts + ); + this._allowedPortsTextbox = prefpane.querySelector( + selectors.advanced.firewallAllowedPortsTextbox + ); + this._allowedPortsTextbox.setAttribute( + "placeholder", + TorStrings.settings.allowedPortsPlaceholder + ); + + // Tor logs + prefpane + .querySelector(selectors.advanced.torLogsLabel) + .setAttribute("value", TorStrings.settings.showTorDaemonLogs); + prefpane + .querySelector(selectors.advanced.torLogsButton) + .setAttribute("label", TorStrings.settings.showLogs); + + // Disable all relevant elements by default + this._setElementsDisabled( + [ + this._builtinBridgeOption, + this._builtinBridgeMenulist, + this._requestBridgeOption, + this._requestBridgeButton, + this._requestBridgeTextarea, + this._provideBridgeOption, + this._provideBridgeTextarea, + this._proxyTypeLabel, + this._proxyTypeMenulist, + this._proxyAddressLabel, + this._proxyAddressTextbox, + this._proxyPortLabel, + this._proxyPortTextbox, + this._proxyUsernameLabel, + this._proxyUsernameTextbox, + this._proxyPasswordLabel, + this._proxyPasswordTextbox, + this._allowedPortsLabel, + this._allowedPortsTextbox, + ], + true + ); + + // load bridge settings + let torBridgeSettings = new TorBridgeSettings(); + torBridgeSettings.readSettings(); + + // populate the bridge list + for (let currentBridge of TorBridgeSettings.defaultBridgeTypes) { + let menuEntry = document.createElement("menuitem"); + menuEntry.setAttribute("value", currentBridge); + menuEntry.setAttribute("label", currentBridge); + this._builtinBridgeMenulist + .querySelector("menupopup") + .append(menuEntry); + } + + this.onSelectBridgeOption(torBridgeSettings.bridgeSource); + this.onToggleBridge( + torBridgeSettings.bridgeSource != TorBridgeSource.NONE + ); + switch (torBridgeSettings.bridgeSource) { + case TorBridgeSource.NONE: + break; + case TorBridgeSource.BUILTIN: + this._builtinBridgeMenulist.value = + torBridgeSettings.selectedDefaultBridgeType; + break; + case TorBridgeSource.BRIDGEDB: + this._requestBridgeTextarea.value = torBridgeSettings.bridgeStrings; + break; + case TorBridgeSource.USERPROVIDED: + this._provideBridgeTextarea.value = torBridgeSettings.bridgeStrings; + break; + } + + this._bridgeSettings = torBridgeSettings; + + // load proxy settings + let torProxySettings = new TorProxySettings(); + torProxySettings.readSettings(); + + if (torProxySettings.type != TorProxyType.NONE) { + this.onToggleProxy(true); + this.onSelectProxyType(torProxySettings.type); + this._proxyAddressTextbox.value = torProxySettings.address; + this._proxyPortTextbox.value = torProxySettings.port; + this._proxyUsernameTextbox.value = torProxySettings.username; + this._proxyPasswordTextbox.value = torProxySettings.password; + } + + this._proxySettings = torProxySettings; + + // load firewall settings + let torFirewallSettings = new TorFirewallSettings(); + torFirewallSettings.readSettings(); + + if (torFirewallSettings.hasPorts) { + this.onToggleFirewall(true); + this._allowedPortsTextbox.value = + torFirewallSettings.commaSeparatedListString; + } + + this._firewallSettings = torFirewallSettings; + }, + + init() { + this._populateXUL(); + }, + + // + // Callbacks + // + + // callback when using bridges toggled + onToggleBridge(enabled) { + this._useBridgeCheckbox.checked = enabled; + let disabled = !enabled; + + // first disable all the bridge related elements + this._setElementsDisabled( + [ + this._builtinBridgeOption, + this._builtinBridgeMenulist, + this._requestBridgeOption, + this._requestBridgeButton, + this._requestBridgeTextarea, + this._provideBridgeOption, + this._provideBridgeTextarea, + ], + disabled + ); + + // and selectively re-enable based on the radiogroup's current value + if (enabled) { + this.onSelectBridgeOption(this._bridgeSelectionRadiogroup.value); + } else { + this.onSelectBridgeOption(TorBridgeSource.NONE); + } + return this; + }, + + // callback when a bridge option is selected + onSelectBridgeOption(source) { + // disable all of the bridge elements under radio buttons + this._setElementsDisabled( + [ + this._builtinBridgeMenulist, + this._requestBridgeButton, + this._requestBridgeTextarea, + this._provideBridgeTextarea, + ], + true + ); + + if (source != TorBridgeSource.NONE) { + this._bridgeSelectionRadiogroup.value = source; + } + + switch (source) { + case TorBridgeSource.BUILTIN: { + this._setElementsDisabled([this._builtinBridgeMenulist], false); + break; + } + case TorBridgeSource.BRIDGEDB: { + this._setElementsDisabled( + [this._requestBridgeButton, this._requestBridgeTextarea], + false + ); + break; + } + case TorBridgeSource.USERPROVIDED: { + this._setElementsDisabled([this._provideBridgeTextarea], false); + break; + } + } + return this; + }, + + // called when the request brige button is activated + onRequestBridge() { + let requestBridgeDialog = new RequestBridgeDialog(); + requestBridgeDialog.openDialog( + gSubDialog, + this._proxySettings.proxyURI, + aBridges => { + if (aBridges.length > 0) { + let bridgeSettings = makeTorBridgeSettingsBridgeDB(aBridges); + bridgeSettings.writeSettings(); + this._bridgeSettings = bridgeSettings; + + this._requestBridgeTextarea.value = bridgeSettings.bridgeStrings; + } + } + ); + return this; + }, + + // pushes bridge settings from UI to tor + onUpdateBridgeSettings() { + let bridgeSettings = null; + + let source = this._useBridgeCheckbox.checked + ? this._bridgeSelectionRadiogroup.value + : TorBridgeSource.NONE; + switch (source) { + case TorBridgeSource.NONE: { + bridgeSettings = makeTorBridgeSettingsNone(); + break; + } + case TorBridgeSource.BUILTIN: { + // if there is a built-in bridge already selected, use that + let bridgeType = this._builtinBridgeMenulist.value; + if (bridgeType) { + bridgeSettings = makeTorBridgeSettingsBuiltin(bridgeType); + } else { + bridgeSettings = makeTorBridgeSettingsNone(); + } + break; + } + case TorBridgeSource.BRIDGEDB: { + // if there are bridgedb bridges saved in the text area, use them + let bridgeStrings = this._requestBridgeTextarea.value; + if (bridgeStrings) { + let bridgeStringList = parseBridgeStrings(bridgeStrings); + bridgeSettings = makeTorBridgeSettingsBridgeDB(bridgeStringList); + } else { + bridgeSettings = makeTorBridgeSettingsNone(); + } + break; + } + case TorBridgeSource.USERPROVIDED: { + // if bridges already exist in the text area, use them + let bridgeStrings = this._provideBridgeTextarea.value; + if (bridgeStrings) { + let bridgeStringList = parseBridgeStrings(bridgeStrings); + bridgeSettings = makeTorBridgeSettingsUserProvided( + bridgeStringList + ); + } else { + bridgeSettings = makeTorBridgeSettingsNone(); + } + break; + } + } + bridgeSettings.writeSettings(); + this._bridgeSettings = bridgeSettings; + return this; + }, + + // callback when proxy is toggled + onToggleProxy(enabled) { + this._useProxyCheckbox.checked = enabled; + let disabled = !enabled; + + this._setElementsDisabled( + [ + this._proxyTypeLabel, + this._proxyTypeMenulist, + this._proxyAddressLabel, + this._proxyAddressTextbox, + this._proxyPortLabel, + this._proxyPortTextbox, + this._proxyUsernameLabel, + this._proxyUsernameTextbox, + this._proxyPasswordLabel, + this._proxyPasswordTextbox, + ], + disabled + ); + this.onSelectProxyType(this._proxyTypeMenulist.value); + return this; + }, + + // callback when proxy type is changed + onSelectProxyType(value) { + if (value == "") { + value = TorProxyType.NONE; + } + this._proxyTypeMenulist.value = value; + switch (value) { + case TorProxyType.NONE: { + this._setElementsDisabled( + [ + this._proxyAddressLabel, + this._proxyAddressTextbox, + this._proxyPortLabel, + this._proxyPortTextbox, + this._proxyUsernameLabel, + this._proxyUsernameTextbox, + this._proxyPasswordLabel, + this._proxyPasswordTextbox, + ], + true + ); // DISABLE + + this._proxyAddressTextbox.value = ""; + this._proxyPortTextbox.value = ""; + this._proxyUsernameTextbox.value = ""; + this._proxyPasswordTextbox.value = ""; + break; + } + case TorProxyType.SOCKS4: { + this._setElementsDisabled( + [ + this._proxyAddressLabel, + this._proxyAddressTextbox, + this._proxyPortLabel, + this._proxyPortTextbox, + ], + false + ); // ENABLE + this._setElementsDisabled( + [ + this._proxyUsernameLabel, + this._proxyUsernameTextbox, + this._proxyPasswordLabel, + this._proxyPasswordTextbox, + ], + true + ); // DISABLE + + this._proxyUsernameTextbox.value = ""; + this._proxyPasswordTextbox.value = ""; + break; + } + case TorProxyType.SOCKS5: + case TorProxyType.HTTPS: { + this._setElementsDisabled( + [ + this._proxyAddressLabel, + this._proxyAddressTextbox, + this._proxyPortLabel, + this._proxyPortTextbox, + this._proxyUsernameLabel, + this._proxyUsernameTextbox, + this._proxyPasswordLabel, + this._proxyPasswordTextbox, + ], + false + ); // ENABLE + break; + } + } + return this; + }, + + // pushes proxy settings from UI to tor + onUpdateProxySettings() { + const proxyType = this._useProxyCheckbox.checked + ? this._proxyTypeMenulist.value + : TorProxyType.NONE; + const addressString = this._proxyAddressTextbox.value; + const portString = this._proxyPortTextbox.value; + const usernameString = this._proxyUsernameTextbox.value; + const passwordString = this._proxyPasswordTextbox.value; + + let proxySettings = null; + + switch (proxyType) { + case TorProxyType.NONE: + proxySettings = makeTorProxySettingsNone(); + break; + case TorProxyType.SOCKS4: + proxySettings = makeTorProxySettingsSocks4( + addressString, + parsePort(portString) + ); + break; + case TorProxyType.SOCKS5: + proxySettings = makeTorProxySettingsSocks5( + addressString, + parsePort(portString), + usernameString, + passwordString + ); + break; + case TorProxyType.HTTPS: + proxySettings = makeTorProxySettingsHTTPS( + addressString, + parsePort(portString), + usernameString, + passwordString + ); + break; + } + + proxySettings.writeSettings(); + this._proxySettings = proxySettings; + return this; + }, + + // callback when firewall proxy is toggled + onToggleFirewall(enabled) { + this._useFirewallCheckbox.checked = enabled; + let disabled = !enabled; + + this._setElementsDisabled( + [this._allowedPortsLabel, this._allowedPortsTextbox], + disabled + ); + + return this; + }, + + // pushes firewall settings from UI to tor + onUpdateFirewallSettings() { + let portListString = this._useFirewallCheckbox.checked + ? this._allowedPortsTextbox.value + : ""; + let firewallSettings = null; + + if (portListString) { + firewallSettings = makeTorFirewallSettingsCustom( + parsePortList(portListString) + ); + } else { + firewallSettings = makeTorFirewallSettingsNone(); + } + + firewallSettings.writeSettings(); + this._firewallSettings = firewallSettings; + return this; + }, + + onViewTorLogs() { + let torLogDialog = new TorLogDialog(); + torLogDialog.openDialog(gSubDialog); + }, + }; + return retval; +})(); /* gTorPane */ diff --git a/browser/components/torpreferences/content/torPane.xul b/browser/components/torpreferences/content/torPane.xul new file mode 100644 index 000000000000..298c148bcbf2 --- /dev/null +++ b/browser/components/torpreferences/content/torPane.xul @@ -0,0 +1,119 @@ +<!-- Tor panel --> + +<script type="application/javascript" + src="chrome://browser/content/torpreferences/torPane.js"/> + +<hbox id="torPreferencesCategory" + class="subcategory" + hidden="true" + data-category="paneTor"> + <html:h1 id="torPreferences-header"/> +</hbox> + +<groupbox data-category="paneTor" > + <description flex="1"> + <html:span id="torPreferences-description" class="tail-with-learn-more"/> + <label id="torPreferences-learnMore" class="learnMore text-link" is="text-link"/> + </description> +</groupbox> + +<!-- Bridges --> +<groupbox id="torPreferences-bridges-group" data-category="paneTor" > + <html:h2 id="torPreferences-bridges-header"/> + <description flex="1"> + <html:span id="torPreferences-bridges-description" class="tail-with-learn-more"/> + <label id="torPreferences-bridges-learnMore" class="learnMore text-link" is="text-link"/> + </description> + <checkbox id="torPreferences-bridges-toggle" oncommand="gTorPane.onToggleBridge(this.checked).onUpdateBridgeSettings();"/> + <radiogroup id="torPreferences-bridges-bridgeSelection" + oncommand="gTorPane.onSelectBridgeOption(this.value).onUpdateBridgeSettings();"> + <hbox class="indent"> + <radio id="torPreferences-bridges-radioBuiltin"/> + <spacer flex="1"/> + <menulist id="torPreferences-bridges-builtinList" class="torMarginFix" oncommand="gTorPane.onUpdateBridgeSettings();"> + <menupopup/> + </menulist> + </hbox> + <vbox class="indent"> + <hbox> + <radio id="torPreferences-bridges-radioRequestBridge"/> + <space flex="1"/> + <button id="torPreferences-bridges-buttonRequestBridge" class="torMarginFix" oncommand="gTorPane.onRequestBridge();"/> + </hbox> + <html:textarea + id="torPreferences-bridges-textareaRequestBridge" + class="indent torMarginFix" + multiline="true" + rows="3" + readonly="true"/> + </vbox> + <hbox class="indent" flex="1"> + <vbox flex="1"> + <radio id="torPreferences-bridges-radioProvideBridge"/> + <description id="torPreferences-bridges-descriptionProvideBridge" class="indent"/> + <html:textarea + id="torPreferences-bridges-textareaProvideBridge" + class="indent torMarginFix" + multiline="true" + rows="3" + onblur="gTorPane.onUpdateBridgeSettings();"/> + </vbox> + </hbox> + </radiogroup> +</groupbox> + +<!-- Advanced --> +<groupbox id="torPreferences-advanced-group" data-category="paneTor"> + <html:h2 id="torPreferences-advanced-header"/> + <description flex="1"> + <html:span id="torPreferences-advanced-description" class="tail-with-learn-more"/> + <label id="torPreferences-advanced-learnMore" class="learnMore text-link" is="text-link"/> + </description> + <grid flex="1"> + <columns> + <column flex="0"/> + <column flex="1"/> + </columns> + <rows> + <!-- Local Proxy --> + <checkbox id="torPreferences-advanced-toggleProxy" oncommand="gTorPane.onToggleProxy(this.checked).onUpdateProxySettings();"/> + <row class="indent" align="center"> + <label id="torPreferences-localProxy-type"/> + <hbox> + <spacer flex="1"/> + <menulist id="torPreferences-localProxy-builtinList" class="torMarginFix" oncommand="gTorPane.onSelectProxyType(this.value).onUpdateProxySettings();"> + <menupopup/> + </menulist> + </hbox> + </row> + <row class="indent" align="center"> + <label id="torPreferences-localProxy-address"/> + <hbox align="center"> + <textbox id="torPreferences-localProxy-textboxAddress" class="torMarginFix" flex="4" onblur="gTorPane.onUpdateProxySettings();"/> + <label id="torPreferences-localProxy-port"/> + <!-- proxy-port-input class style pulled from preferences.css and used in the vanilla proxy setup menu --> + <html:input id="torPreferences-localProxy-textboxPort" class="proxy-port-input torMarginFix" hidespinbuttons="true" type="number" min="0" max="65535" maxlength="5" onblur="gTorPane.onUpdateProxySettings();"/> + </hbox> + </row> + <row class="indent" align="center"> + <label id="torPreferences-localProxy-username"/> + <hbox align="center"> + <textbox id="torPreferences-localProxy-textboxUsername" class="torMarginFix" flex="1" onblur="gTorPane.onUpdateProxySettings();"/> + <label id="torPreferences-localProxy-password"/> + <textbox id="torPreferences-localProxy-textboxPassword" class="torMarginFix" type="password" flex="1" onblur="gTorPane.onUpdateProxySettings();"/> + </hbox> + </row> + <!-- Firewall --> + <checkbox id="torPreferences-advanced-toggleFirewall" oncommand="gTorPane.onToggleFirewall(this.checked).onUpdateFirewallSettings();"/> + <row class="indent" align="center"> + <label id="torPreferences-advanced-allowedPorts"/> + <textbox id="torPreferences-advanced-textboxAllowedPorts" class="torMarginFix" value="80,443" onblur="gTorPane.onUpdateFirewallSettings();"/> + </row> + </rows> + </grid> + <hbox id="torPreferences-torDaemon-hbox" align="center"> + <label id="torPreferences-torLogs"/> + <spacer flex="1"/> + <button id="torPreferences-buttonTorLogs" class="torMarginFix" oncommand="gTorPane.onViewTorLogs();"/> + </hbox> +</groupbox> diff --git a/browser/components/torpreferences/content/torPreferences.css b/browser/components/torpreferences/content/torPreferences.css new file mode 100644 index 000000000000..19c7421cd647 --- /dev/null +++ b/browser/components/torpreferences/content/torPreferences.css @@ -0,0 +1,63 @@ +#category-tor > .category-icon { + list-style-image: url("chrome://browser/content/torpreferences/torPreferencesIcon.svg"); +} + +hbox#torPreferences-torDaemon-hbox { + margin-top: 20px; +} + +description#torPreferences-requestBridge-description { + /*margin-bottom: 1em;*/ + min-height: 2em; +} + +image#torPreferences-requestBridge-captchaImage { + margin: 1em; + min-height: 125px; +} + +button#torPreferences-requestBridge-refreshCaptchaButton { + min-width: initial; +} + +dialog#torPreferences-requestBridge-dialog > hbox +{ + margin-bottom: 1em; +} + +/* + Various elements that really should be lining up don't because they have inconsistent margins +*/ +.torMarginFix +{ + margin-left : 4px; + margin-right : 4px; +} + +/* + This hbox is hidden by css here by default so that the + xul dialog allocates enough screen space for the error message + element, otherwise it gets cut off since dialog's overflow is hidden +*/ +hbox#torPreferences-requestBridge-incorrectCaptchaHbox { + visibility: hidden; +} + +image#torPreferences-requestBridge-errorIcon { + list-style-image: url("chrome://browser/skin/warning.svg"); +} + +groupbox#torPreferences-bridges-group textarea { + white-space: pre; + overflow: auto; +} + +textarea#torPreferences-torDialog-textarea { + -moz-box-flex: 1; + font-family: monospace; + font-size: 0.8em; + white-space: pre; + overflow: auto; + /* 10 lines */ + min-height: 20em; +} \ No newline at end of file diff --git a/browser/components/torpreferences/content/torPreferencesIcon.svg b/browser/components/torpreferences/content/torPreferencesIcon.svg new file mode 100644 index 000000000000..d7895f1107c5 --- /dev/null +++ b/browser/components/torpreferences/content/torPreferencesIcon.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g fill="context-fill" fill-opacity="context-fill-opacity" fill-rule="nonzero"> + <path d="M12.0246161,21.8174863 L12.0246161,20.3628098 C16.6324777,20.3495038 20.3634751,16.6108555 20.3634751,11.9996673 C20.3634751,7.38881189 16.6324777,3.65016355 12.0246161,3.63685757 L12.0246161,2.18218107 C17.4358264,2.1958197 21.8178189,6.58546322 21.8178189,11.9996673 C21.8178189,17.4142042 17.4358264,21.8041803 12.0246161,21.8174863 L12.0246161,21.8174863 Z M12.0246161,16.7259522 C14.623607,16.7123136 16.7272828,14.6023175 16.7272828,11.9996673 C16.7272828,9.39734991 14.623607,7.28735377 12.0246161,7.27371516 L12.0246161,5.81937131 C15.4272884,5.8326773 18.1819593,8.59400123 18.1819593,11.9996673 C18.1819593,15.4056661 15.4272884,18.1669901 12.0246161,18.1802961 L12.0246161,16.7259522 Z M12.0246161,9.45556355 C13.4187503,9.46886953 14.5454344,10.6022066 14.5454344,11.9996673 C14.5454344,13.3974608 13.4187503,14.5307978 12.0246161,14.5441038 L12.0246161,9.45556355 Z M0,11.9996673 C0,18.6273771 5.37229031,24 12,24 C18.6273771,24 24,18.6273771 24,11.9996673 C24,5.37229031 18.6273771,0 12,0 C5.37229031,0 0,5.37229031 0,11.9996673 Z"/> + </g> +</svg> \ No newline at end of file diff --git a/browser/components/torpreferences/content/torProxySettings.jsm b/browser/components/torpreferences/content/torProxySettings.jsm new file mode 100644 index 000000000000..98bb5e8d5cbf --- /dev/null +++ b/browser/components/torpreferences/content/torProxySettings.jsm @@ -0,0 +1,245 @@ +"use strict"; + +var EXPORTED_SYMBOLS = [ + "TorProxyType", + "TorProxySettings", + "makeTorProxySettingsNone", + "makeTorProxySettingsSocks4", + "makeTorProxySettingsSocks5", + "makeTorProxySettingsHTTPS", +]; + +const { TorProtocolService } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); +const { parseAddrPort, parseUsernamePassword } = ChromeUtils.import( + "chrome://browser/content/torpreferences/parseFunctions.jsm" +); + +const TorProxyType = { + NONE: "NONE", + SOCKS4: "SOCKS4", + SOCKS5: "SOCKS5", + HTTPS: "HTTPS", +}; + +class TorProxySettings { + constructor() { + this._proxyType = TorProxyType.NONE; + this._proxyAddress = undefined; + this._proxyPort = undefined; + this._proxyUsername = undefined; + this._proxyPassword = undefined; + } + + get type() { + return this._proxyType; + } + get address() { + return this._proxyAddress; + } + get port() { + return this._proxyPort; + } + get username() { + return this._proxyUsername; + } + get password() { + return this._proxyPassword; + } + get proxyURI() { + switch (this._proxyType) { + case TorProxyType.SOCKS4: + return `socks4a://${this._proxyAddress}:${this._proxyPort}`; + case TorProxyType.SOCKS5: + if (this._proxyUsername) { + return `socks5://${this._proxyUsername}:${this._proxyPassword}@${ + this._proxyAddress + }:${this._proxyPort}`; + } + return `socks5://${this._proxyAddress}:${this._proxyPort}`; + case TorProxyType.HTTPS: + if (this._proxyUsername) { + return `http://$%7Bthis._proxyUsername%7D:$%7Bthis._proxyPassword%7D@$%7B + this._proxyAddress + }:${this._proxyPort}`; + } + return `http://$%7Bthis._proxyAddress%7D:$%7Bthis._proxyPort%7D%60; + } + return undefined; + } + + // attempts to read proxy settings from Tor daemon + readSettings() { + // SOCKS4 + { + let addressPort = TorProtocolService.readStringSetting( + TorStrings.configKeys.socks4Proxy + ); + if (addressPort) { + // address+port + let [proxyAddress, proxyPort] = parseAddrPort(addressPort); + + this._proxyType = TorProxyType.SOCKS4; + this._proxyAddress = proxyAddress; + this._proxyPort = proxyPort; + this._proxyUsername = ""; + this._proxyPassword = ""; + + return; + } + } + + // SOCKS5 + { + let addressPort = TorProtocolService.readStringSetting( + TorStrings.configKeys.socks5Proxy + ); + + if (addressPort) { + // address+port + let [proxyAddress, proxyPort] = parseAddrPort(addressPort); + // username + let proxyUsername = TorProtocolService.readStringSetting( + TorStrings.configKeys.socks5ProxyUsername + ); + // password + let proxyPassword = TorProtocolService.readStringSetting( + TorStrings.configKeys.socks5ProxyPassword + ); + + this._proxyType = TorProxyType.SOCKS5; + this._proxyAddress = proxyAddress; + this._proxyPort = proxyPort; + this._proxyUsername = proxyUsername; + this._proxyPassword = proxyPassword; + + return; + } + } + + // HTTP + { + let addressPort = TorProtocolService.readStringSetting( + TorStrings.configKeys.httpsProxy + ); + + if (addressPort) { + // address+port + let [proxyAddress, proxyPort] = parseAddrPort(addressPort); + + // username:password + let proxyAuthenticator = TorProtocolService.readStringSetting( + TorStrings.configKeys.httpsProxyAuthenticator + ); + + let [proxyUsername, proxyPassword] = ["", ""]; + if (proxyAuthenticator) { + [proxyUsername, proxyPassword] = parseUsernamePassword( + proxyAuthenticator + ); + } + + this._proxyType = TorProxyType.HTTPS; + this._proxyAddress = proxyAddress; + this._proxyPort = proxyPort; + this._proxyUsername = proxyUsername; + this._proxyPassword = proxyPassword; + } + } + // no proxy settings + } /* TorProxySettings::ReadFromTor() */ + + // attempts to write proxy settings to Tor daemon + // throws on error + writeSettings() { + let settingsObject = new Map(); + + // init proxy related settings to null so Tor daemon resets them + settingsObject.set(TorStrings.configKeys.socks4Proxy, null); + settingsObject.set(TorStrings.configKeys.socks5Proxy, null); + settingsObject.set(TorStrings.configKeys.socks5ProxyUsername, null); + settingsObject.set(TorStrings.configKeys.socks5ProxyPassword, null); + settingsObject.set(TorStrings.configKeys.httpsProxy, null); + settingsObject.set(TorStrings.configKeys.httpsProxyAuthenticator, null); + + switch (this._proxyType) { + case TorProxyType.SOCKS4: + settingsObject.set( + TorStrings.configKeys.socks4Proxy, + `${this._proxyAddress}:${this._proxyPort}` + ); + break; + case TorProxyType.SOCKS5: + settingsObject.set( + TorStrings.configKeys.socks5Proxy, + `${this._proxyAddress}:${this._proxyPort}` + ); + settingsObject.set( + TorStrings.configKeys.socks5ProxyUsername, + this._proxyUsername + ); + settingsObject.set( + TorStrings.configKeys.socks5ProxyPassword, + this._proxyPassword + ); + break; + case TorProxyType.HTTPS: + settingsObject.set( + TorStrings.configKeys.httpsProxy, + `${this._proxyAddress}:${this._proxyPort}` + ); + settingsObject.set( + TorStrings.configKeys.httpsProxyAuthenticator, + `${this._proxyUsername}:${this._proxyPassword}` + ); + break; + } + + TorProtocolService.writeSettings(settingsObject); + } /* TorProxySettings::WriteToTor() */ +} + +// factory methods for our various supported proxies +function makeTorProxySettingsNone() { + return new TorProxySettings(); +} + +function makeTorProxySettingsSocks4(aProxyAddress, aProxyPort) { + let retval = new TorProxySettings(); + retval._proxyType = TorProxyType.SOCKS4; + retval._proxyAddress = aProxyAddress; + retval._proxyPort = aProxyPort; + return retval; +} + +function makeTorProxySettingsSocks5( + aProxyAddress, + aProxyPort, + aProxyUsername, + aProxyPassword +) { + let retval = new TorProxySettings(); + retval._proxyType = TorProxyType.SOCKS5; + retval._proxyAddress = aProxyAddress; + retval._proxyPort = aProxyPort; + retval._proxyUsername = aProxyUsername; + retval._proxyPassword = aProxyPassword; + return retval; +} + +function makeTorProxySettingsHTTPS( + aProxyAddress, + aProxyPort, + aProxyUsername, + aProxyPassword +) { + let retval = new TorProxySettings(); + retval._proxyType = TorProxyType.HTTPS; + retval._proxyAddress = aProxyAddress; + retval._proxyPort = aProxyPort; + retval._proxyUsername = aProxyUsername; + retval._proxyPassword = aProxyPassword; + return retval; +} diff --git a/browser/components/torpreferences/jar.mn b/browser/components/torpreferences/jar.mn new file mode 100644 index 000000000000..44920322fe4f --- /dev/null +++ b/browser/components/torpreferences/jar.mn @@ -0,0 +1,14 @@ +browser.jar: + content/browser/torpreferences/parseFunctions.jsm (content/parseFunctions.jsm) + content/browser/torpreferences/requestBridgeDialog.xul (content/requestBridgeDialog.xul) + content/browser/torpreferences/requestBridgeDialog.jsm (content/requestBridgeDialog.jsm) + content/browser/torpreferences/torBridgeSettings.jsm (content/torBridgeSettings.jsm) + content/browser/torpreferences/torCategory.inc.xul (content/torCategory.inc.xul) + content/browser/torpreferences/torFirewallSettings.jsm (content/torFirewallSettings.jsm) + content/browser/torpreferences/torLogDialog.jsm (content/torLogDialog.jsm) + content/browser/torpreferences/torLogDialog.xul (content/torLogDialog.xul) + content/browser/torpreferences/torPane.js (content/torPane.js) + content/browser/torpreferences/torPane.xul (content/torPane.xul) + content/browser/torpreferences/torPreferences.css (content/torPreferences.css) + content/browser/torpreferences/torPreferencesIcon.svg (content/torPreferencesIcon.svg) + content/browser/torpreferences/torProxySettings.jsm (content/torProxySettings.jsm) diff --git a/browser/components/torpreferences/moz.build b/browser/components/torpreferences/moz.build new file mode 100644 index 000000000000..7e103239c8d6 --- /dev/null +++ b/browser/components/torpreferences/moz.build @@ -0,0 +1 @@ +JAR_MANIFESTS += ['jar.mn'] diff --git a/browser/modules/BridgeDB.jsm b/browser/modules/BridgeDB.jsm new file mode 100644 index 000000000000..16bf02e6c688 --- /dev/null +++ b/browser/modules/BridgeDB.jsm @@ -0,0 +1,110 @@ +"use strict;"; + +var EXPORTED_SYMBOLS = ["BridgeDB"]; + +const { TorLauncherBridgeDB } = ChromeUtils.import( + "resource://torlauncher/modules/tl-bridgedb.jsm" +); +const { TorProtocolService } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); + +var BridgeDB = { + _moatRequestor: null, + _currentCaptchaInfo: null, + _bridges: null, + + get currentCaptchaImage() { + if (this._currentCaptchaInfo) { + return this._currentCaptchaInfo.captchaImage; + } + return null; + }, + + get currentBridges() { + return this._bridges; + }, + + submitCaptchaGuess(aCaptchaSolution) { + if (this._moatRequestor && this._currentCaptchaInfo) { + return this._moatRequestor + .finishFetch( + this._currentCaptchaInfo.transport, + this._currentCaptchaInfo.challenge, + aCaptchaSolution + ) + .then(aBridgeInfo => { + this._moatRequestor.close(); + this._moatRequestor = null; + this._currentCaptchaInfo = null; + this._bridges = aBridgeInfo.bridges; + // array of bridge strings + return this._bridges; + }); + } + + return new Promise((aResponse, aReject) => { + aReject(new Error("Invalid _moatRequestor or _currentCaptchaInfo")); + }); + }, + + requestNewCaptchaImage(aProxyURI) { + // close and clear out existing state on captcha request + this.close(); + + let transportPlugins = TorProtocolService.readStringArraySetting( + TorStrings.configKeys.clientTransportPlugin + ); + + let meekClientPath; + let meekTransport; // We support both "meek" and "meek_lite". + let meekClientArgs; + // TODO: shouldn't this early out once meek settings are found? + for (const line of transportPlugins) { + // Parse each ClientTransportPlugin line and look for the meek or + // meek_lite transport. This code works a lot like the Tor daemon's + // parse_transport_line() function. + let tokens = line.split(" "); + if (tokens.length > 2 && tokens[1] == "exec") { + let transportArray = tokens[0].split(",").map(aStr => aStr.trim()); + let transport = transportArray.find( + aTransport => aTransport === "meek" + ); + if (!transport) { + transport = transportArray.find( + aTransport => aTransport === "meek_lite" + ); + } + if (transport) { + meekTransport = transport; + meekClientPath = tokens[2]; + meekClientArgs = tokens.slice(3); + } + } + } + + this._moatRequestor = TorLauncherBridgeDB.createMoatRequestor(); + + return this._moatRequestor + .init(aProxyURI, meekTransport, meekClientPath, meekClientArgs) + .then(() => { + // TODO: get this from TorLauncherUtil + let bridgeType = "obfs4"; + return this._moatRequestor.fetchBridges([bridgeType]); + }) + .then(aCaptchaInfo => { + // cache off the current captcha info as the challenge is needed for response + this._currentCaptchaInfo = aCaptchaInfo; + return aCaptchaInfo.captchaImage; + }); + }, + + close() { + if (this._moatRequestor) { + this._moatRequestor.close(); + this._moatRequestor = null; + } + this._currentCaptchaInfo = null; + }, +}; diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm new file mode 100644 index 000000000000..682e7be9de1a --- /dev/null +++ b/browser/modules/TorProtocolService.jsm @@ -0,0 +1,203 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["TorProtocolService"]; + +var TorProtocolService = { + _tlps: Cc["@torproject.org/torlauncher-protocol-service;1"].getService( + Ci.nsISupports + ).wrappedJSObject, + // maintain a map of tor settings set by Tor Browser so that we don't + // repeatedly set the same key/values over and over + // this map contains string keys to primitive or array values + _settingsCache: new Map(), + + _typeof(aValue) { + switch (typeof aValue) { + case "boolean": + return "boolean"; + case "string": + return "string"; + case "object": + if (aValue == null) { + return "null"; + } else if (Array.isArray(aValue)) { + return "array"; + } + return "object"; + } + return "unknown"; + }, + + _assertValidSettingKey(aSetting) { + // ensure the 'key' is a string + if (typeof aSetting != "string") { + throw new Error( + `Expected setting of type string but received ${typeof aSetting}` + ); + } + }, + + _assertValidSetting(aSetting, aValue) { + this._assertValidSettingKey(aSetting); + + const valueType = this._typeof(aValue); + switch (valueType) { + case "boolean": + case "string": + case "null": + return; + case "array": + for (const element of aValue) { + if (typeof element != "string") { + throw new Error( + `Setting '${aSetting}' array contains value of invalid type '${typeof element}'` + ); + } + } + return; + default: + throw new Error( + `Invalid object type received for setting '${aSetting}'` + ); + } + }, + + // takes a Map containing tor settings + // throws on error + writeSettings(aSettingsObj) { + // only write settings that have changed + let newSettings = new Map(); + for (const [setting, value] of aSettingsObj) { + let saveSetting = false; + + // make sure we have valid data here + this._assertValidSetting(setting, value); + + if (!this._settingsCache.has(setting)) { + // no cached setting, so write + saveSetting = true; + } else { + const cachedValue = this._settingsCache.get(setting); + if (value != cachedValue) { + // compare arrays member-wise + if (Array.isArray(value) && Array.isArray(cachedValue)) { + if (value.length != cachedValue.length) { + saveSetting = true; + } else { + const arrayLength = value.length; + for (let i = 0; i < arrayLength; ++i) { + if (value[i] != cachedValue[i]) { + saveSetting = true; + break; + } + } + } + } else { + // some other different values + saveSetting = true; + } + } + } + + if (saveSetting) { + newSettings.set(setting, value); + } + } + + // only write if new setting to save + if (newSettings.size > 0) { + // convert settingsObject map to js object for torlauncher-protocol-service + let settingsObject = {}; + for (const [setting, value] of newSettings) { + // console.log(`${setting} : ${value}`); + settingsObject[setting] = value; + } + + let errorObject = {}; + if (!this._tlps.TorSetConfWithReply(settingsObject, errorObject)) { + throw new Error(errorObject.details); + } + + // save settings to cache after successfully writing to Tor + for (const [setting, value] of newSettings) { + this._settingsCache.set(setting, value); + } + } + }, + + _readSetting(aSetting) { + this._assertValidSettingKey(aSetting); + let reply = this._tlps.TorGetConf(aSetting); + if (this._tlps.TorCommandSucceeded(reply)) { + return reply.lineArray; + } + throw new Error(reply.lineArray.join("\n")); + }, + + _readBoolSetting(aSetting) { + let lineArray = this._readSetting(aSetting); + if (lineArray.length != 1) { + throw new Error( + `Expected an array with length 1 but received array of length ${ + lineArray.length + }` + ); + } + + let retval = lineArray[0]; + switch (retval) { + case "0": + return false; + case "1": + return true; + default: + throw new Error(`Expected boolean (1 or 0) but received '${retval}'`); + } + }, + + _readStringSetting(aSetting) { + let lineArray = this._readSetting(aSetting); + if (lineArray.length != 1) { + throw new Error( + `Expected an array with length 1 but received array of length ${ + lineArray.length + }` + ); + } + return lineArray[0]; + }, + + _readStringArraySetting(aSetting) { + let lineArray = this._readSetting(aSetting); + return lineArray; + }, + + readBoolSetting(aSetting) { + let value = this._readBoolSetting(aSetting); + this._settingsCache.set(aSetting, value); + return value; + }, + + readStringSetting(aSetting) { + let value = this._readStringSetting(aSetting); + this._settingsCache.set(aSetting, value); + return value; + }, + + readStringArraySetting(aSetting) { + let value = this._readStringArraySetting(aSetting); + this._settingsCache.set(aSetting, value); + return value; + }, + + // writes current tor settings to disk + flushSettings() { + this._tlps.TorSendCommand("SAVECONF"); + }, + + getLog() { + let countObj = { value: 0 }; + let torLog = this._tlps.TorGetLog(countObj); + return torLog; + }, +}; diff --git a/browser/modules/TorStrings.jsm b/browser/modules/TorStrings.jsm new file mode 100644 index 000000000000..c7bd6f4236ae --- /dev/null +++ b/browser/modules/TorStrings.jsm @@ -0,0 +1,326 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["TorStrings"]; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { getLocale } = ChromeUtils.import( + "resource://torbutton/modules/utils.js" +); + +XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser"]); +XPCOMUtils.defineLazyGetter(this, "domParser", () => { + const parser = new DOMParser(); + parser.forceEnableDTD(); + return parser; +}); + +/* + Tor String Bundle + + Strings loaded from torbutton/tor-launcher, but provide a fallback in case they aren't available +*/ +class TorStringBundle { + constructor(aBundleURLs, aPrefix) { + let locations = []; + for (const [index, url] of aBundleURLs.entries()) { + locations.push(`<!ENTITY % dtd_${index} SYSTEM "${url}">%dtd_${index};`); + } + this._locations = locations; + this._prefix = aPrefix; + } + + // copied from testing/marionette/l10n.js + localizeEntity(urls, id) { + // Use the DOM parser to resolve the entity and extract its real value + let header = `<?xml version="1.0"?><!DOCTYPE elem [${this._locations.join( + "" + )}]>`; + let elem = `<elem id="elementID">&${id};</elem>`; + let doc = domParser.parseFromString(header + elem, "text/xml"); + let element = doc.querySelector("elem[id='elementID']"); + + if (element === null) { + throw new Error(`Entity with id='${id}' hasn't been found`); + } + + return element.textContent; + } + + getString(key, fallback) { + if (key) { + try { + return this.localizeEntity(this._bundleURLs, `${this._prefix}${key}`); + } catch (e) {} + } + + // on failure, assign the fallback if it exists + if (fallback) { + return fallback; + } + // otherwise return string key + return `$(${key})`; + } +} + +/* + Security Level Strings +*/ +var TorStrings = { + /* + Tor Browser Security Level Strings + */ + securityLevel: (function() { + let tsb = new TorStringBundle( + ["chrome://torbutton/locale/torbutton.dtd"], + "torbutton.prefs.sec_" + ); + let getString = function(key, fallback) { + return tsb.getString(key, fallback); + }; + + // read localized strings from torbutton; but use hard-coded en-US strings as fallbacks in case of error + let retval = { + securityLevel: getString("caption", "Security Level"), + customWarning: getString("custom_warning", "Custom"), + overview: getString( + "overview", + "Disable certain web features that can be used to attack your security and anonymity." + ), + standard: { + level: getString("standard_label", "Standard"), + tooltip: getString("standard_tooltip", "Security Level : Standard"), + summary: getString( + "standard_description", + "All Tor Browser and website features are enabled." + ), + }, + safer: { + level: getString("safer_label", "Safer"), + tooltip: getString("safer_tooltip", "Security Level : Safer"), + summary: getString( + "safer_description", + "Disables website features that are often dangerous, causing some sites to lose functionality." + ), + description1: getString( + "js_on_https_sites_only", + "JavaScript is disabled on non-HTTPS sites." + ), + description2: getString( + "limit_typography", + "Some fonts and math symbols are disabled." + ), + description3: getString( + "click_to_play_media", + "Audio and video (HTML5 media), and WebGL are click-to-play." + ), + }, + safest: { + level: getString("safest_label", "Safest"), + tooltip: getString("safest_tooltip", "Security Level : Safest"), + summary: getString( + "safest_description", + "Only allows website features required for static sites and basic services. These changes affect images, media, and scripts." + ), + description1: getString( + "js_disabled", + "JavaScript is disabled by default on all sites." + ), + description2: getString( + "limit_graphics_and_typography", + "Some fonts, icons, math symbols, and images are disabled." + ), + description3: getString( + "click_to_play_media", + "Audio and video (HTML5 media), and WebGL are click-to-play." + ), + }, + custom: { + summary: getString( + "custom_summary", + "Your custom browser preferences have resulted in unusual security settings. For security and privacy reasons, we recommend you choose one of the default security levels." + ), + }, + learnMore: getString("learn_more_label", "Learn more"), + learnMoreURL: `https://tb-manual.torproject.org/$%7BgetLocale()%7D/security-settings/%60, + restoreDefaults: getString("restore_defaults", "Restore Defaults"), + advancedSecuritySettings: getString( + "advanced_security_settings", + "Advanced Security Settings\u2026" + ), + }; + return retval; + })() /* Security Level Strings */, + + /* + Tor about:preferences#tor Strings + */ + settings: (function() { + let tsb = new TorStringBundle( + ["chrome://torlauncher/locale/network-settings.dtd"], + "" + ); + let getString = function(key, fallback) { + return tsb.getString(key, fallback); + }; + + let retval = { + categoryTitle: getString("torPreferences.categoryTitle", "Tor"), + torPreferencesHeading: getString( + "torPreferences.torSettings", + "Tor Settings" + ), + torPreferencesDescription: getString( + "torPreferences.torSettingsDescription", + "Tor Browser routes your traffic over the Tor Network, run by thousands of volunteers around the world." + ), + learnMore: getString("torPreferences.learnMore", "Learn More"), + bridgesHeading: getString("torPreferences.bridges", "Bridges"), + bridgesDescription: getString( + "torPreferences.bridgesDescription", + "Bridges help you access the Tor Network in places where Tor is blocked. Depending on where you are, one bridge may work better than another." + ), + useBridge: getString("torPreferences.useBridge", "Use a bridge"), + selectBridge: getString( + "torsettings.useBridges.default", + "Select a bridge" + ), + requestBridgeFromTorProject: getString( + "torsettings.useBridges.bridgeDB", + "Request a bridge from torproject.org" + ), + requestNewBridge: getString( + "torPreferences.requestNewBridge", + "Request a New Bridge\u2026" + ), + provideBridge: getString( + "torPreferences.provideBridge", + "Provide a bridge" + ), + provideBridgeDirections: getString( + "torsettings.useBridges.label", + "Enter bridge information from a trusted source." + ), + provideBridgePlaceholder: getString( + "torsettings.useBridges.placeholder", + "type address:port (one per line)" + ), + advancedHeading: getString("torPreferences.advanced", "Advanced"), + advancedDescription: getString( + "torPreferences.advancedDescription", + "Configure how Tor Browser connects to the internet." + ), + useLocalProxy: getString("torPreferences.useProxy", "Use a local proxy"), + proxyType: getString("torsettings.useProxy.type", "Proxy Type"), + proxyTypeSOCKS4: getString("torsettings.useProxy.type.socks4", "SOCKS4"), + proxyTypeSOCKS5: getString("torsettings.useProxy.type.socks5", "SOCKS5"), + proxyTypeHTTP: getString("torsettings.useProxy.type.http", "HTTP/HTTPS"), + proxyAddress: getString("torsettings.useProxy.address", "Address"), + proxyAddressPlaceholder: getString( + "torsettings.useProxy.address.placeholder", + "IP address or hostname" + ), + proxyPort: getString("torsettings.useProxy.port", "Port"), + proxyUsername: getString("torsettings.useProxy.username", "Username"), + proxyPassword: getString("torsettings.useProxy.password", "Password"), + proxyUsernamePasswordPlaceholder: getString( + "torsettings.optional", + "Optional" + ), + useFirewall: getString( + "torsettings.firewall.checkbox", + "This computer goes through a firewall that only allows connections to certain ports" + ), + allowedPorts: getString( + "torsettings.firewall.allowedPorts", + "Allowed Ports" + ), + allowedPortsPlaceholder: getString( + "torPreferences.firewallPortsPlaceholder", + "Comma-seperated values" + ), + requestBridgeDialogTitle: getString( + "torPreferences.requestBridgeDialogTitle", + "Request Bridge" + ), + submitCaptcha: getString( + "torsettings.useBridges.captchaSubmit", + "Submit" + ), + contactingBridgeDB: getString( + "torPreferences.requestBridgeDialogWaitPrompt", + "Contacting BridgeDB. Please Wait." + ), + solveTheCaptcha: getString( + "torPreferences.requestBridgeDialogSolvePrompt", + "Solve the CAPTCHA to request a bridge." + ), + captchaTextboxPlaceholder: getString( + "torsettings.useBridges.captchaSolution.placeholder", + "Enter the characters from the image" + ), + incorrectCaptcha: getString( + "torPreferences.requestBridgeErrorBadSolution", + "The solution is not correct. Please try again." + ), + showTorDaemonLogs: getString( + "torPreferences.viewTorLogs", + "View the Tor logs." + ), + showLogs: getString("torPreferences.viewLogs", "View Logs\u2026"), + torLogDialogTitle: getString( + "torPreferences.torLogsDialogTitle", + "Tor Logs" + ), + copyLog: getString("torsettings.copyLog", "Copy Tor Log to Clipboard"), + + learnMoreTorBrowserURL: `https://tb-manual.torproject.org/$%7BgetLocale()%7D/about/%60, + learnMoreBridgesURL: `https://tb-manual.torproject.org/$%7BgetLocale()%7D/bridges/%60, + learnMoreNetworkSettingsURL: `about:blank`, + }; + + return retval; + })() /* Tor Network Settings Strings */, + + /* + Tor Deamon Configuration Key Strings + */ + + // TODO: proper camel case + configKeys: { + /* Bridge Conf Settings */ + useBridges: "UseBridges", + bridgeList: "Bridge", + /* Proxy Conf Strings */ + socks4Proxy: "Socks4Proxy", + socks5Proxy: "Socks5Proxy", + socks5ProxyUsername: "Socks5ProxyUsername", + socks5ProxyPassword: "Socks5ProxyPassword", + httpsProxy: "HTTPSProxy", + httpsProxyAuthenticator: "HTTPSProxyAuthenticator", + /* Firewall Conf Strings */ + reachableAddresses: "ReachableAddresses", + + /* BridgeDB Strings */ + clientTransportPlugin: "ClientTransportPlugin", + }, + + /* + about:config preference keys + */ + + preferenceKeys: { + defaultBridgeType: "extensions.torlauncher.default_bridge_type", + recommendedBridgeType: + "extensions.torlauncher.default_bridge_recommended_type", + }, + + /* + about:config preference branches + */ + preferenceBranches: { + defaultBridge: "extensions.torlauncher.default_bridge.", + bridgeDBBridges: "extensions.torlauncher.bridgedb_bridge.", + }, +}; diff --git a/browser/modules/moz.build b/browser/modules/moz.build index 304bd34d2ff2..43f1524f4553 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -134,6 +134,7 @@ XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] EXTRA_JS_MODULES += [ 'AboutNewTab.jsm', 'AsyncTabSwitcher.jsm', + 'BridgeDB.jsm', 'BrowserUsageTelemetry.jsm', 'BrowserWindowTracker.jsm', 'ContentClick.jsm', @@ -165,6 +166,8 @@ EXTRA_JS_MODULES += [ 'TabsList.jsm', 'TabUnloader.jsm', 'ThemeVariableMap.jsm', + 'TorProtocolService.jsm', + 'TorStrings.jsm', 'TransientPrefs.jsm', 'webrtcUI.jsm', 'ZoomUI.jsm',