Commit a1389f29 authored by Richard Pospesel's avatar Richard Pospesel Committed by Matthew Finkel
Browse files

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
 - The Networking Settings in General preferences has been removed
parent 4e79114f
......@@ -57,6 +57,7 @@ DIRS += [
"syncedtabs",
"uitour",
"urlbar",
"torpreferences",
"translation",
]
......
......@@ -665,59 +665,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"
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-https,
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-sharing.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>
......@@ -373,15 +373,6 @@ var gMainPane = {
});
this.updatePerformanceSettingsBox({ duringChangeEvent: false });
this.displayUseSystemLocale();
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();
......@@ -515,11 +506,6 @@ var gMainPane = {
"change",
gMainPane.updateHardwareAcceleration.bind(gMainPane)
);
setEventListener(
"connectionSettings",
"command",
gMainPane.showConnections
);
setEventListener(
"browserContainersCheckbox",
"command",
......
......@@ -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 */
"use strict";
......@@ -136,6 +137,14 @@ function init_all() {
register_module("paneSync", gSyncPane);
}
register_module("paneSearchResults", gSearchResultsPane);
if (gTorPane.enabled) {
document.getElementById("category-tor").hidden = false;
register_module("paneTor", gTorPane);
} else {
// Remove the pane from the DOM so it doesn't get incorrectly included in search results.
document.getElementById("template-paneTor").remove();
}
gSearchResultsPane.init();
gMainPane.preInit();
......
......@@ -13,6 +13,7 @@
<?xml-stylesheet href="chrome://browser/skin/preferences/containers.css"?>
<?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?>
<?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?>
<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
<!DOCTYPE html [
<!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd">
......@@ -155,6 +156,9 @@
<image class="category-icon"/>
<label class="category-name" flex="1" data-l10n-id="pane-experimental-title"></label>
</richlistitem>
#include ../torpreferences/content/torCategory.inc.xhtml
</richlistbox>
<spacer flex="1"/>
......@@ -215,6 +219,7 @@
#include containers.inc.xhtml
#include sync.inc.xhtml
#include experimental.inc.xhtml
#include ../torpreferences/content/torPane.xhtml
</vbox>
</vbox>
</vbox>
......
......@@ -80,6 +80,7 @@ XPCOMUtils.defineLazyGetter(this, "AlertsServiceDND", function() {
}
});
// TODO: module import via ChromeUtils.defineModuleGetter
XPCOMUtils.defineLazyScriptGetter(
this,
["SecurityLevelPreferences"],
......
"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 a string 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' or '/r/n' delimited bridge string, which we split and trim
// each bridge string can also optionally have 'bridge' at the beginning ie:
// bridge $(type) $(address):$(port) $(certificate)
// we strip out the 'bridge' prefix here
let parseBridgeStrings = function(aBridgeStrings) {
// replace carriage returns ('\r') with new lines ('\n')
aBridgeStrings = aBridgeStrings.replace(/\r/g, "\n");
// then replace contiguous new lines ('\n') with a single one
aBridgeStrings = aBridgeStrings.replace(/[\n]+/g, "\n");
// split on the newline and for each bridge string: trim, remove starting 'bridge' string
// finally discard entries that are empty strings; empty strings could occur if we receive
// a new line containing only whitespace
let splitStrings = aBridgeStrings.split("\n");
return splitStrings.map(val => val.trim().replace(/^bridge\s+/i, ""))
.filter(bridgeString => bridgeString != "");
};
// 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()));
};
"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._submitButton = null;
this._dialogDescription = null;
this._captchaImage = null;
this._captchaEntryTextbox = 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",
captchaImage: "image#torPreferences-requestBridge-captchaImage",
captchaEntryTextbox: "input#torPreferences-requestBridge-captchaTextbox",
refreshCaptchaButton:
"button#torPreferences-requestBridge-refreshCaptchaButton",
incorrectCaptchaHbox:
"hbox#torPreferences-requestBridge-incorrectCaptchaHbox",
incorrectCaptchaLabel:
"label#torPreferences-requestBridge-incorrectCaptchaError",
};
}
_populateXUL(dialog) {
const selectors = RequestBridgeDialog.selectors;
this._dialog = dialog;
const dialogWin = dialog.parentElement;
dialogWin.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.cancelDialog();
}
});
this._submitButton = this._dialog.getButton(selectors.submitButton);
this._submitButton.setAttribute("label", TorStrings.settings.submitCaptcha);
this._submitButton.disabled = true;
this._dialog.addEventListener("dialogaccept", e => {
e.preventDefault();
this.onSubmitCaptcha();
});
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;
// disable submit if entry textbox is empty
this._captchaEntryTextbox.oninput = () => {
this._submitButton.disabled = this._captchaEntryTextbox.value == "";
};
this._captchaRefreshButton = this._dialog.querySelector(
selectors.refreshCaptchaButton
);
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 was successful, but use cancelDialog() to close, since
// we intercept the `dialogaccept` event.
this._dialog.cancelDialog();
})
.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.xhtml",
{
features: "resizable=yes",
closingCallback: () => {
this.close();
aCloseCallback(this._bridges);
}
},
this,
);
}
}
<?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"?>
<window type="child"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml">
<dialog id="torPreferences-requestBridge-dialog"
buttons="accept,cancel">
<!-- ok, so &#8203; 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">&#8203;</description>
<!-- init to transparent 400x125 png -->
<image id="torPreferences-requestBridge-captchaImage" flex="1"/>
<hbox id="torPreferences-requestBridge-inputHbox">
<html:input id="torPreferences-requestBridge-captchaTextbox" type="text" style="-moz-box-flex: 1;"/>
<button id="torPreferences-requestBridge-refreshCaptchaButton"
image="chrome://browser/skin/reload.svg"
oncommand="requestBridgeDialog.onRefreshCaptcha();"/>
</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>
</window>
\ No newline at end of file
"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();