Verified Commit c62ccbe5 authored by Richard Pospesel's avatar Richard Pospesel
Browse files

Bug 40597: Implement TorSettings module

- migrated in-page settings read/write implementation from about:preferences#tor
  to the TorSettings module
- TorSettings initially loads settings from the tor daemon, and saves them to
  firefox prefs
- TorSettings notifies observers when a setting has changed; currently only
  QuickStart notification is implemented for parity with previous preference
  notify logic in about:torconnect and about:preferences#tor
- about:preferences#tor, and about:torconnect now read and write settings
  thorugh the TorSettings module
- all tor settings live in the torbrowser.settings.* preference branch
- removed unused pref modify permission for about:torconnect content page from
  AsyncPrefs.jsm
parent 94a32bb8
......@@ -7,10 +7,9 @@ const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import(
"resource:///modules/TorConnect.jsm"
);
const TorLauncherPrefs = Object.freeze({
quickstart: "extensions.torlauncher.quickstart",
});
const { TorSettings, TorSettingsTopics, TorSettingsData } = ChromeUtils.import(
"resource:///modules/TorSettings.jsm"
);
/*
This object is basically a marshalling interface between the TorConnect module
......@@ -31,7 +30,7 @@ class TorConnectParent extends JSWindowActorParent {
BootstrapProgress: TorConnect.bootstrapProgress,
BootstrapStatus: TorConnect.bootstrapStatus,
ShowCopyLog: TorConnect.logHasWarningOrError,
QuickStartEnabled: Services.prefs.getBoolPref(TorLauncherPrefs.quickstart, false),
QuickStartEnabled: TorSettings.quickstart.enabled,
};
// JSWindowActiveParent derived objects cannot observe directly, so create a member
......@@ -78,9 +77,12 @@ class TorConnectParent extends JSWindowActorParent {
// TODO: handle
break;
}
case "nsPref:changed": {
if (aData === TorLauncherPrefs.quickstart) {
self.state.QuickStartEnabled = Services.prefs.getBoolPref(TorLauncherPrefs.quickstart);
case TorSettingsTopics.SettingChanged:{
if (aData === TorSettingsData.QuickStartEnabled) {
self.state.QuickStartEnabled = obj.value;
} else {
// this isn't a setting torconnect cares about
return;
}
break;
}
......@@ -98,7 +100,7 @@ class TorConnectParent extends JSWindowActorParent {
const topic = TorConnectTopics[key];
Services.obs.addObserver(this.torConnectObserver, topic);
}
Services.prefs.addObserver(TorLauncherPrefs.quickstart, this.torConnectObserver);
Services.obs.addObserver(this.torConnectObserver, TorSettingsTopics.SettingChanged);
}
willDestroy() {
......@@ -107,13 +109,14 @@ class TorConnectParent extends JSWindowActorParent {
const topic = TorConnectTopics[key];
Services.obs.removeObserver(this.torConnectObserver, topic);
}
Services.prefs.removeObserver(TorLauncherPrefs.quickstart, this.torConnectObserver);
Services.obs.removeObserver(this.torConnectObserver, TorSettingsTopics.SettingChanged);
}
receiveMessage(message) {
switch (message.name) {
case "torconnect:set-quickstart":
Services.prefs.setBoolPref(TorLauncherPrefs.quickstart, message.data);
TorSettings.quickstart.enabled = message.data;
TorSettings.saveToPrefs().applySettings();
break;
case "torconnect:open-tor-preferences":
TorConnect.openTorPreferences();
......
"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 = [
"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;
}
"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;
}
......@@ -2,6 +2,10 @@
/* global Services */
const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource, TorBuiltinBridgeTypes, TorProxyType } = ChromeUtils.import(
"resource:///modules/TorSettings.jsm"
);
const { TorProtocolService } = ChromeUtils.import(
"resource:///modules/TorProtocolService.jsm"
);
......@@ -10,35 +14,6 @@ const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import(
"resource:///modules/TorConnect.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"
);
......@@ -53,14 +28,6 @@ ChromeUtils.defineModuleGetter(
"resource:///modules/TorStrings.jsm"
);
const { parsePort, parseBridgeStrings, parsePortList } = ChromeUtils.import(
"chrome://browser/content/torpreferences/parseFunctions.jsm"
);
const TorLauncherPrefs = {
quickstart: "extensions.torlauncher.quickstart",
}
/*
Tor Pane
......@@ -261,10 +228,11 @@ const gTorPane = (function() {
);
this._enableQuickstartCheckbox.addEventListener("command", e => {
const checked = this._enableQuickstartCheckbox.checked;
Services.prefs.setBoolPref(TorLauncherPrefs.quickstart, checked);
TorSettings.quickstart.enabled = checked;
TorSettings.saveToPrefs().applySettings();
});