[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [tor-browser/tor-browser-91.6.0esr-11.5-1] Bug 40597: Implement TorSettings module
commit 7088c2f9336b30f74d4b99e0f846f6aac78ebbe4
Author: Richard Pospesel <richard@xxxxxxxxxxxxxx>
Date: Fri Aug 6 16:39:03 2021 +0200
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
Bug 40645: Migrate Moat APIs to Moat.jsm module
---
browser/modules/BridgeDB.jsm | 61 ++
browser/modules/Moat.jsm | 730 +++++++++++++++++++++
browser/modules/TorConnect.jsm | 643 ++++++++++++++++++
browser/modules/TorProtocolService.jsm | 484 ++++++++++++++
browser/modules/TorSettings.jsm | 693 +++++++++++++++++++
browser/modules/moz.build | 4 +
.../processsingleton/MainProcessSingleton.jsm | 5 +
7 files changed, 2620 insertions(+)
diff --git a/browser/modules/BridgeDB.jsm b/browser/modules/BridgeDB.jsm
new file mode 100644
index 000000000000..50665710ebf4
--- /dev/null
+++ b/browser/modules/BridgeDB.jsm
@@ -0,0 +1,61 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["BridgeDB"];
+
+const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm");
+
+var BridgeDB = {
+ _moatRPC: null,
+ _challenge: null,
+ _image: null,
+ _bridges: null,
+
+ get currentCaptchaImage() {
+ return this._image;
+ },
+
+ get currentBridges() {
+ return this._bridges;
+ },
+
+ async submitCaptchaGuess(solution) {
+ if (!this._moatRPC) {
+ this._moatRPC = new MoatRPC();
+ await this._moatRPC.init();
+ }
+
+ const response = await this._moatRPC.check(
+ "obfs4",
+ this._challenge,
+ solution,
+ false
+ );
+ this._bridges = response?.bridges;
+ return this._bridges;
+ },
+
+ async requestNewCaptchaImage() {
+ try {
+ if (!this._moatRPC) {
+ this._moatRPC = new MoatRPC();
+ await this._moatRPC.init();
+ }
+
+ const response = await this._moatRPC.fetch(["obfs4"]);
+ this._challenge = response.challenge;
+ this._image =
+ "data:image/jpeg;base64," + encodeURIComponent(response.image);
+ } catch (err) {
+ console.log(`error : ${err}`);
+ }
+ return this._image;
+ },
+
+ close() {
+ this._moatRPC?.uninit();
+ this._moatRPC = null;
+ this._challenge = null;
+ this._image = null;
+ this._bridges = null;
+ },
+};
diff --git a/browser/modules/Moat.jsm b/browser/modules/Moat.jsm
new file mode 100644
index 000000000000..d02075e4412f
--- /dev/null
+++ b/browser/modules/Moat.jsm
@@ -0,0 +1,730 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["MoatRPC"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { Subprocess } = ChromeUtils.import(
+ "resource://gre/modules/Subprocess.jsm"
+);
+
+const { TorLauncherUtil } = ChromeUtils.import(
+ "resource://torlauncher/modules/tl-util.jsm"
+);
+
+const { TorProtocolService } = ChromeUtils.import(
+ "resource:///modules/TorProtocolService.jsm"
+);
+
+const { TorSettings, TorBridgeSource, TorProxyType } = ChromeUtils.import(
+ "resource:///modules/TorSettings.jsm"
+);
+
+const TorLauncherPrefs = Object.freeze({
+ bridgedb_front: "extensions.torlauncher.bridgedb_front",
+ bridgedb_reflector: "extensions.torlauncher.bridgedb_reflector",
+ moat_service: "extensions.torlauncher.moat_service",
+});
+
+// Config keys used to query tor daemon properties
+const TorConfigKeys = Object.freeze({
+ clientTransportPlugin: "ClientTransportPlugin",
+});
+
+//
+// Launches and controls the PT process lifetime
+//
+class MeekTransport {
+ constructor() {
+ this._inited = false;
+ this._meekClientProcess = null;
+ this._meekProxyType = null;
+ this._meekProxyAddress = null;
+ this._meekProxyPort = 0;
+ this._meekProxyUsername = null;
+ this._meekProxyPassword = null;
+ }
+
+ // launches the meekprocess
+ async init() {
+ // ensure we haven't already init'd
+ if (this._inited) {
+ throw new Error("MeekTransport: Already initialized");
+ }
+
+ // cleanup function for killing orphaned pt process
+ let onException = () => {};
+ try {
+ // figure out which pluggable transport to use
+ const supportedTransports = ["meek", "meek_lite"];
+ let transportPlugins = await TorProtocolService.readStringArraySetting(
+ TorConfigKeys.clientTransportPlugin
+ );
+
+ let { meekTransport, meekClientPath, meekClientArgs } = (() => {
+ for (const line of transportPlugins) {
+ 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 =>
+ supportedTransports.includes(aTransport)
+ );
+
+ if (transport != undefined) {
+ return {
+ meekTransport: transport,
+ meekClientPath: tokens[2],
+ meekClientArgs: tokens.slice(3),
+ };
+ }
+ }
+ }
+
+ return {
+ meekTransport: null,
+ meekClientPath: null,
+ meekClientArgs: null,
+ };
+ })();
+
+ // Convert meek client path to absolute path if necessary
+ let meekWorkDir = await TorLauncherUtil.getTorFile(
+ "pt-startup-dir",
+ false
+ );
+ let re = TorLauncherUtil.isWindows ? /^[A-Za-z]:\\/ : /^\//;
+ if (!re.test(meekClientPath)) {
+ let meekPath = meekWorkDir.clone();
+ meekPath.appendRelativePath(meekClientPath);
+ meekClientPath = meekPath.path;
+ }
+
+ // Construct the per-connection arguments.
+ let meekClientEscapedArgs = "";
+ const meekReflector = Services.prefs.getStringPref(
+ TorLauncherPrefs.bridgedb_reflector
+ );
+
+ // Escape aValue per section 3.5 of the PT specification:
+ // First the "<Key>=<Value>" formatted arguments MUST be escaped,
+ // such that all backslash, equal sign, and semicolon characters
+ // are escaped with a backslash.
+ let escapeArgValue = aValue => {
+ if (!aValue) {
+ return "";
+ }
+
+ let rv = aValue.replace(/\\/g, "\\\\");
+ rv = rv.replace(/=/g, "\\=");
+ rv = rv.replace(/;/g, "\\;");
+ return rv;
+ };
+
+ if (meekReflector) {
+ meekClientEscapedArgs += "url=";
+ meekClientEscapedArgs += escapeArgValue(meekReflector);
+ }
+ const meekFront = Services.prefs.getStringPref(
+ TorLauncherPrefs.bridgedb_front
+ );
+ if (meekFront) {
+ if (meekClientEscapedArgs.length) {
+ meekClientEscapedArgs += ";";
+ }
+ meekClientEscapedArgs += "front=";
+ meekClientEscapedArgs += escapeArgValue(meekFront);
+ }
+
+ // Setup env and start meek process
+ let ptStateDir = TorLauncherUtil.getTorFile("tordatadir", false);
+ let meekHelperProfileDir = TorLauncherUtil.getTorFile(
+ "pt-profiles-dir",
+ true
+ );
+ ptStateDir.append("pt_state"); // Match what tor uses.
+ meekHelperProfileDir.appendRelativePath("profile.moat-http-helper");
+
+ let envAdditions = {
+ TOR_PT_MANAGED_TRANSPORT_VER: "1",
+ TOR_PT_STATE_LOCATION: ptStateDir.path,
+ TOR_PT_EXIT_ON_STDIN_CLOSE: "1",
+ TOR_PT_CLIENT_TRANSPORTS: meekTransport,
+ TOR_BROWSER_MEEK_PROFILE: meekHelperProfileDir.path,
+ };
+ if (TorSettings.proxy.enabled) {
+ envAdditions.TOR_PT_PROXY = TorSettings.proxy.uri;
+ }
+
+ let opts = {
+ command: meekClientPath,
+ arguments: meekClientArgs,
+ workdir: meekWorkDir.path,
+ environmentAppend: true,
+ environment: envAdditions,
+ stderr: "pipe",
+ };
+
+ // Launch meek client
+ let meekClientProcess = await Subprocess.call(opts);
+ // kill our process if exception is thrown
+ onException = () => {
+ meekClientProcess.kill();
+ };
+
+ // Callback chain for reading stderr
+ let stderrLogger = async () => {
+ if (this._meekClientProcess) {
+ let errString = await this._meekClientProcess.stderr.readString();
+ console.log(`MeekTransport: stderr => ${errString}`);
+ await stderrLogger();
+ }
+ };
+ stderrLogger();
+
+ // Read pt's stdout until terminal (CMETHODS DONE) is reached
+ // returns array of lines for parsing
+ let getInitLines = async (stdout = "") => {
+ let string = await meekClientProcess.stdout.readString();
+ stdout += string;
+
+ // look for the final message
+ const CMETHODS_DONE = "CMETHODS DONE";
+ let endIndex = stdout.lastIndexOf(CMETHODS_DONE);
+ if (endIndex != -1) {
+ endIndex += CMETHODS_DONE.length;
+ return stdout.substr(0, endIndex).split("\n");
+ }
+ return getInitLines(stdout);
+ };
+
+ // read our lines from pt's stdout
+ let meekInitLines = await getInitLines();
+ // tokenize our pt lines
+ let meekInitTokens = meekInitLines.map(line => {
+ let tokens = line.split(" ");
+ return {
+ keyword: tokens[0],
+ args: tokens.slice(1),
+ };
+ });
+
+ let meekProxyType = null;
+ let meekProxyAddr = null;
+ let meekProxyPort = 0;
+
+ // parse our pt tokens
+ for (const { keyword, args } of meekInitTokens) {
+ const argsJoined = args.join(" ");
+ let keywordError = false;
+ switch (keyword) {
+ case "VERSION": {
+ if (args.length != 1 || args[0] !== "1") {
+ keywordError = true;
+ }
+ break;
+ }
+ case "PROXY": {
+ if (args.length != 1 || args[0] !== "DONE") {
+ keywordError = true;
+ }
+ break;
+ }
+ case "CMETHOD": {
+ if (args.length != 3) {
+ keywordError = true;
+ break;
+ }
+ const transport = args[0];
+ const proxyType = args[1];
+ const addrPortString = args[2];
+ const addrPort = addrPortString.split(":");
+
+ if (transport !== meekTransport) {
+ throw new Error(
+ `MeekTransport: Expected ${meekTransport} but found ${transport}`
+ );
+ }
+ if (!["socks4", "socks4a", "socks5"].includes(proxyType)) {
+ throw new Error(
+ `MeekTransport: Invalid proxy type => ${proxyType}`
+ );
+ }
+ if (addrPort.length != 2) {
+ throw new Error(
+ `MeekTransport: Invalid proxy address => ${addrPortString}`
+ );
+ }
+ const addr = addrPort[0];
+ const port = parseInt(addrPort[1]);
+ if (port < 1 || port > 65535) {
+ throw new Error(`MeekTransport: Invalid proxy port => ${port}`);
+ }
+
+ // convert proxy type to strings used by protocol-proxy-servce
+ meekProxyType = proxyType === "socks5" ? "socks" : "socks4";
+ meekProxyAddr = addr;
+ meekProxyPort = port;
+
+ break;
+ }
+ // terminal
+ case "CMETHODS": {
+ if (args.length != 1 || args[0] !== "DONE") {
+ keywordError = true;
+ }
+ break;
+ }
+ // errors (all fall through):
+ case "VERSION-ERROR":
+ case "ENV-ERROR":
+ case "PROXY-ERROR":
+ case "CMETHOD-ERROR":
+ throw new Error(`MeekTransport: ${keyword} => '${argsJoined}'`);
+ }
+ if (keywordError) {
+ throw new Error(
+ `MeekTransport: Invalid ${keyword} keyword args => '${argsJoined}'`
+ );
+ }
+ }
+
+ this._meekClientProcess = meekClientProcess;
+ // register callback to cleanup on process exit
+ this._meekClientProcess.wait().then(exitObj => {
+ this._meekClientProcess = null;
+ this.uninit();
+ });
+
+ this._meekProxyType = meekProxyType;
+ this._meekProxyAddress = meekProxyAddr;
+ this._meekProxyPort = meekProxyPort;
+
+ // socks5
+ if (meekProxyType === "socks") {
+ if (meekClientEscapedArgs.length <= 255) {
+ this._meekProxyUsername = meekClientEscapedArgs;
+ this._meekProxyPassword = "\x00";
+ } else {
+ this._meekProxyUsername = meekClientEscapedArgs.substring(0, 255);
+ this._meekProxyPassword = meekClientEscapedArgs.substring(255);
+ }
+ // socks4
+ } else {
+ this._meekProxyUsername = meekClientEscapedArgs;
+ this._meekProxyPassword = undefined;
+ }
+
+ this._inited = true;
+ } catch (ex) {
+ onException();
+ throw ex;
+ }
+ }
+
+ async uninit() {
+ this._inited = false;
+
+ await this._meekClientProcess?.kill();
+ this._meekClientProcess = null;
+ this._meekProxyType = null;
+ this._meekProxyAddress = null;
+ this._meekProxyPort = 0;
+ this._meekProxyUsername = null;
+ this._meekProxyPassword = null;
+ }
+}
+
+//
+// Callback object with a cached promise for the returned Moat data
+//
+class MoatResponseListener {
+ constructor() {
+ this._response = "";
+ // we need this promise here because await nsIHttpChannel::asyncOpen does
+ // not return only once the request is complete, it seems to return
+ // after it begins, so we have to get the result from this listener object.
+ // This promise is only resolved once onStopRequest is called
+ this._responsePromise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ }
+
+ // callers wait on this for final response
+ response() {
+ return this._responsePromise;
+ }
+
+ // noop
+ onStartRequest(request) {}
+
+ // resolve or reject our Promise
+ onStopRequest(request, status) {
+ try {
+ if (!Components.isSuccessCode(status)) {
+ const errorMessage = TorLauncherUtil.getLocalizedStringForError(status);
+ this._reject(new Error(errorMessage));
+ }
+ if (request.responseStatus != 200) {
+ this._reject(new Error(request.responseStatusText));
+ }
+ } catch (err) {
+ this._reject(err);
+ }
+ this._resolve(this._response);
+ }
+
+ // read response data
+ onDataAvailable(request, stream, offset, length) {
+ const scriptableStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ scriptableStream.init(stream);
+ this._response += scriptableStream.read(length);
+ }
+}
+
+// constructs the json objects and sends the request over moat
+class MoatRPC {
+ constructor() {
+ this._meekTransport = null;
+ this._inited = false;
+ }
+
+ async init() {
+ if (this._inited) {
+ throw new Error("MoatRPC: Already initialized");
+ }
+
+ let meekTransport = new MeekTransport();
+ await meekTransport.init();
+ this._meekTransport = meekTransport;
+ this._inited = true;
+ }
+
+ async uninit() {
+ await this._meekTransport?.uninit();
+ this._meekTransport = null;
+ this._inited = false;
+ }
+
+ async _makeRequest(procedure, args) {
+ if (!this._inited) {
+ throw new Error("MoatRPC: Not initialized");
+ }
+
+ const proxyType = this._meekTransport._meekProxyType;
+ const proxyAddress = this._meekTransport._meekProxyAddress;
+ const proxyPort = this._meekTransport._meekProxyPort;
+ const proxyUsername = this._meekTransport._meekProxyUsername;
+ const proxyPassword = this._meekTransport._meekProxyPassword;
+
+ const proxyPS = Cc[
+ "@mozilla.org/network/protocol-proxy-service;1"
+ ].getService(Ci.nsIProtocolProxyService);
+ const flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+ const noTimeout = 0xffffffff; // UINT32_MAX
+ const proxyInfo = proxyPS.newProxyInfoWithAuth(
+ proxyType,
+ proxyAddress,
+ proxyPort,
+ proxyUsername,
+ proxyPassword,
+ undefined,
+ undefined,
+ flags,
+ noTimeout,
+ undefined
+ );
+
+ const procedureURIString = `${Services.prefs.getStringPref(
+ TorLauncherPrefs.moat_service
+ )}/${procedure}`;
+
+ const procedureURI = Services.io.newURI(procedureURIString);
+ // There does not seem to be a way to directly create an nsILoadInfo from
+ // JavaScript, so we create a throw away non-proxied channel to get one.
+ const secFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
+ const loadInfo = Services.io.newChannelFromURI(
+ procedureURI,
+ undefined,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ undefined,
+ secFlags,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ ).loadInfo;
+
+ const httpHandler = Services.io
+ .getProtocolHandler("http")
+ .QueryInterface(Ci.nsIHttpProtocolHandler);
+ const ch = httpHandler
+ .newProxiedChannel(procedureURI, proxyInfo, 0, undefined, loadInfo)
+ .QueryInterface(Ci.nsIHttpChannel);
+
+ // remove all headers except for 'Host"
+ const headers = [];
+ ch.visitRequestHeaders({
+ visitHeader: (key, val) => {
+ if (key !== "Host") {
+ headers.push(key);
+ }
+ },
+ });
+ headers.forEach(key => ch.setRequestHeader(key, "", false));
+
+ // Arrange for the POST data to be sent.
+ const argsJson = JSON.stringify(args);
+
+ const inStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ inStream.setData(argsJson, argsJson.length);
+ const upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
+ const contentType = "application/vnd.api+json";
+ upChannel.setUploadStream(inStream, contentType, argsJson.length);
+ ch.requestMethod = "POST";
+
+ // Make request
+ const listener = new MoatResponseListener();
+ await ch.asyncOpen(listener, ch);
+
+ // wait for response
+ const responseJSON = await listener.response();
+
+ // parse that JSON
+ return JSON.parse(responseJSON);
+ }
+
+ //
+ // Moat APIs
+ //
+
+ // Receive a CAPTCHA challenge, takes the following parameters:
+ // - transports: array of transport strings available to us eg: ["obfs4", "meek"]
+ //
+ // returns an object with the following fields:
+ // - transport: a transport string the moat server decides it will send you selected
+ // from the list of provided transports
+ // - image: a base64 encoded jpeg with the captcha to complete
+ // - challenge: a nonce/cookie string associated with this request
+ async fetch(transports) {
+
+ if (
+ // ensure this is an array
+ Array.isArray(transports) &&
+ // ensure array has values
+ !!transports.length &&
+ // ensure each value in the array is a string
+ transports.reduce((acc, cur) => acc && typeof cur === "string", true)
+ ) {
+ const args = {
+ data: [
+ {
+ version: "0.1.0",
+ type: "client-transports",
+ supported: transports,
+ },
+ ],
+ };
+ const response = await this._makeRequest("fetch", args);
+ if ("errors" in response) {
+ const code = response.errors[0].code;
+ const detail = response.errors[0].detail;
+ throw new Error(`MoatRPC: ${detail} (${code})`);
+ }
+
+ const transport = response.data[0].transport;
+ const image = response.data[0].image;
+ const challenge = response.data[0].challenge;
+
+ return { transport, image, challenge };
+ }
+ throw new Error("MoatRPC: fetch() expects a non-empty array of strings");
+ }
+
+ // Submit an answer for a CAPTCHA challenge and get back bridges, takes the following
+ // parameters:
+ // - transport: the transport string associated with a previous fetch request
+ // - challenge: the nonce string associated with the fetch request
+ // - solution: solution to the CAPTCHA associated with the fetch request
+ // - qrcode: true|false whether we want to get back a qrcode containing the bridge strings
+ //
+ // returns an object with the following fields:
+ // - bridges: an array of bridge line strings
+ // - qrcode: base64 encoded jpeg of bridges if requested, otherwise null
+ // if the provided solution is incorrect, returns an empty object
+ async check(transport, challenge, solution, qrcode) {
+ const args = {
+ data: [
+ {
+ id: "2",
+ version: "0.1.0",
+ type: "moat-solution",
+ transport,
+ challenge,
+ solution,
+ qrcode: qrcode ? "true" : "false",
+ },
+ ],
+ };
+ const response = await this._makeRequest("check", args);
+ if ("errors" in response) {
+ const code = response.errors[0].code;
+ const detail = response.errors[0].detail;
+ if (code == 419 && detail === "The CAPTCHA solution was incorrect.") {
+ return {};
+ }
+
+ throw new Error(`MoatRPC: ${detail} (${code})`);
+ }
+
+ const bridges = response.data[0].bridges;
+ const qrcodeImg = qrcode ? response.data[0].qrcode : null;
+
+ return { bridges, qrcode: qrcodeImg };
+ }
+
+ // Convert received settings object to format used by TorSettings module
+ // In the event of error, just return null
+ _fixupSettings(settings) {
+ try {
+ let retval = TorSettings.defaultSettings()
+ if ("bridges" in settings) {
+ retval.bridges.enabled = true;
+ switch(settings.bridges.source) {
+ case "builtin":
+ retval.bridges.source = TorBridgeSource.BuiltIn;
+ retval.bridges.builtin_type = settings.bridges.type;
+ // Tor Browser will periodically update the built-in bridge strings list using the
+ // circumvention_builtin() function, so we can ignore the bridge strings we have received here;
+ // BridgeDB only returns a subset of the available built-in bridges through the circumvention_settings()
+ // function which is fine for our 3rd parties, but we're better off ignoring them in Tor Browser, otherwise
+ // we get in a weird situation of needing to update our built-in bridges in a piece-meal fashion which
+ // seems over-complicated/error-prone
+ break;
+ case "bridgedb":
+ retval.bridges.source = TorBridgeSource.BridgeDB;
+ if (settings.bridges.bridge_strings) {
+ retval.bridges.bridge_strings = settings.bridges.bridge_strings;
+ } else {
+ throw new Error("MoatRPC::_fixupSettings(): Received no bridge-strings for BridgeDB bridge source");
+ }
+ break;
+ default:
+ throw new Error(`MoatRPC::_fixupSettings(): Unexpected bridge source '${settings.bridges.source}'`);
+ }
+ }
+ if ("proxy" in settings) {
+ // TODO: populate proxy settings
+ }
+ if ("firewall" in settings) {
+ // TODO: populate firewall settings
+ }
+ return retval;
+ } catch(ex) {
+ console.log(ex.message);
+ return null;
+ }
+ }
+
+ // Converts a list of settings objects received from BridgeDB to a list of settings objects
+ // understood by the TorSettings module
+ // In the event of error, returns and empty list
+ _fixupSettingsList(settingsList) {
+ try {
+ let retval = [];
+ for (let settings of settingsList) {
+ settings = this._fixupSettings(settings);
+ if (settings != null) {
+ retval.push(settings);
+ }
+ }
+ return retval;
+ } catch (ex) {
+ console.log(ex.message);
+ return [];
+ }
+ }
+
+ // Request tor settings for the user optionally based on their location (derived
+ // from their IP), takes the following parameters:
+ // - transports: optional, an array of transports available to the client; if empty (or not
+ // given) returns settings using all working transports known to the server
+ // - country: optional, an ISO 3166-1 alpha-2 country code to request settings for;
+ // if not provided the country is determined by the user's IP address
+ //
+ // returns an array of settings objects in roughly the same format as the _settings
+ // object on the TorSettings module.
+ // - If the server cannot determine the user's country (and no country code is provided),
+ // then null is returned
+ // - If the country has no associated settings, an empty array is returned
+ async circumvention_settings(transports, country) {
+ const args = {
+ transports: transports ? transports : [],
+ country: country,
+ };
+ const response = await this._makeRequest("circumvention/settings", args);
+ if ("errors" in response) {
+ const code = response.errors[0].code;
+ const detail = response.errors[0].detail;
+ if (code == 406) {
+ console.log("MoatRPC::circumvention_settings(): Cannot automatically determine user's country-code");
+ // cannot determine user's country
+ return null;
+ }
+
+ throw new Error(`MoatRPC: ${detail} (${code})`);
+ } else if ("settings" in response) {
+ return this._fixupSettingsList(response.settings);
+ }
+
+ return [];
+ }
+
+ // Request a copy of the censorship circumvention map (as if cirumvention_settings were
+ // queried for all country codes)
+ //
+ // returns a map whose key is an ISO 3166-1 alpha-2 country code and whose
+ // values are arrays of settings objects
+ async circumvention_map() {
+ const args = { };
+ const response = await this._makeRequest("circumvention/map", args);
+ if ("errors" in response) {
+ const code = response.errors[0].code;
+ const detail = response.errors[0].detail;
+ throw new Error(`MoatRPC: ${detail} (${code})`);
+ }
+
+ let map = new Map();
+ for (const [country, config] of Object.entries(response)) {
+ map.set(country, this._fixupSettingsList(config.settings));
+ }
+
+ return map;
+ }
+
+ // Request a copy of the builtin bridges, takes the following parameters:
+ // - transports: optional, an array of transports we would like the latest bridge strings
+ // for; if empty (or not given) returns all of them
+ //
+ // returns a map whose keys are pluggable transport types and whose values are arrays of
+ // bridge strings for that type
+ async circumvention_builtin(transports) {
+ const args = {
+ transports: transports ? transports : [],
+ };
+ const response = await this._makeRequest("circumvention/builtin", args);
+ if ("errors" in response) {
+ const code = response.errors[0].code;
+ const detail = response.errors[0].detail;
+ throw new Error(`MoatRPC: ${detail} (${code})`);
+ }
+
+ let map = new Map();
+ for (const [transport, bridge_strings] of Object.entries(response)) {
+ map.set(transport, bridge_strings);
+ }
+
+ return map;
+ }
+}
diff --git a/browser/modules/TorConnect.jsm b/browser/modules/TorConnect.jsm
new file mode 100644
index 000000000000..c7ab480e2be0
--- /dev/null
+++ b/browser/modules/TorConnect.jsm
@@ -0,0 +1,643 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorConnect", "TorConnectTopics", "TorConnectState"];
+
+const { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+);
+
+const { BrowserWindowTracker } = ChromeUtils.import(
+ "resource:///modules/BrowserWindowTracker.jsm"
+);
+
+const { TorProtocolService, TorProcessStatus, TorTopics, TorBootstrapRequest } = ChromeUtils.import(
+ "resource:///modules/TorProtocolService.jsm"
+);
+
+const { TorLauncherUtil } = ChromeUtils.import(
+ "resource://torlauncher/modules/tl-util.jsm"
+);
+
+const { TorSettings, TorSettingsTopics, TorBridgeSource, TorBuiltinBridgeTypes, TorProxyType } = ChromeUtils.import(
+ "resource:///modules/TorSettings.jsm"
+);
+
+const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm");
+
+/* Browser observer topis */
+const BrowserTopics = Object.freeze({
+ ProfileAfterChange: "profile-after-change",
+});
+
+/* Relevant prefs used by tor-launcher */
+const TorLauncherPrefs = Object.freeze({
+ prompt_at_startup: "extensions.torlauncher.prompt_at_startup",
+});
+
+const TorConnectState = Object.freeze({
+ /* Our initial state */
+ Initial: "Initial",
+ /* In-between initial boot and bootstrapping, users can change tor network settings during this state */
+ Configuring: "Configuring",
+ /* Tor is attempting to bootstrap with settings from censorship-circumvention db */
+ AutoBootstrapping: "AutoBootstrapping",
+ /* Tor is bootstrapping */
+ Bootstrapping: "Bootstrapping",
+ /* Passthrough state back to Configuring */
+ Error: "Error",
+ /* Final state, after successful bootstrap */
+ Bootstrapped: "Bootstrapped",
+ /* If we are using System tor or the legacy Tor-Launcher */
+ Disabled: "Disabled",
+});
+
+/*
+ TorConnect State Transitions
+
+ â??â??â??â??â??â??â??â??â??â??â?? â??â??â??â??â??â??â??â??â??â??
+ â?? â?¼ â?¼ â??
+ â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â??
+ â??â??â?¼â??â??â??â??â??â?? â?? Error â?? â??â??â??â??â?? â??
+ â?? â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â?? â??
+ â?? â?? â?² â?? â??
+ â?? â?? â?? â?? â??
+ â?? â?? â?? â?? â??
+ â?? â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â??â??â??â??â??â??â??â??â??â??â??â?? â?? â??
+ â?? â?? â??â??â??â??â?? â?? Initial â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?¶ â?? Disabled â?? â?? â??
+ â?? â?? â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â??â??â??â??â??â??â??â??â??â??â??â?? â?? â??
+ â?? â?? â?? â?? â?? â??
+ â?? â?? â?? â?? beginBootstrap() â?? â??
+ â?? â?? â?? â?¼ â?? â??
+ â?? â?? â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â?? â??
+ â?? â?? â?? â?? Bootstrapping â?? â??â??â??â??â?? â??
+ â?? â?? â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â??
+ â?? â?? â?? â?? â?² â?? â??
+ â?? â?? â?? â?? cancelBootstrap() â?? beginBootstrap() â??â??â??â??â??â?? â??
+ â?? â?? â?? â?¼ â?? â?? â??
+ â?? â?? â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â?? â??
+ â?? â?? â??â??â??â??â?¶ â?? â?? â??â?¼â??â??â??â??â??
+ â?? â?? â?? â?? â??
+ â?? â?? â?? â?? â??
+ â?? â?? â?? Configuring â?? â??
+ â?? â?? â?? â?? â??
+ â?? â?? â?? â?? â??
+ â??â??â?¼â??â??â??â??â??â?¶ â?? â?? â??
+ â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â??
+ â?? â?? â?² â??
+ â?? â?? beginAutoBootstrap() â?? cancelAutoBootstrap() â??
+ â?? â?¼ â?? â??
+ â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â?? â??
+ â??â??â??â??â??â??â?? â?? AutoBootstrapping â?? â??â?? â??
+ â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â??
+ â?? â??
+ â?? â??
+ â?¼ â??
+ â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â?? â??
+ â?? Bootstrapped â?? â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??
+ â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??â??
+*/
+
+/* Maps allowed state transitions
+ TorConnectStateTransitions[state] maps to an array of allowed states to transition to
+ This is just an encoding of the above transition diagram that we verify at runtime
+*/
+const TorConnectStateTransitions =
+ Object.freeze(new Map([
+ [TorConnectState.Initial,
+ [TorConnectState.Disabled,
+ TorConnectState.Bootstrapping,
+ TorConnectState.Configuring,
+ TorConnectState.Error]],
+ [TorConnectState.Configuring,
+ [TorConnectState.AutoBootstrapping,
+ TorConnectState.Bootstrapping,
+ TorConnectState.Error]],
+ [TorConnectState.AutoBootstrapping,
+ [TorConnectState.Configuring,
+ TorConnectState.Bootstrapped,
+ TorConnectState.Error]],
+ [TorConnectState.Bootstrapping,
+ [TorConnectState.Configuring,
+ TorConnectState.Bootstrapped,
+ TorConnectState.Error]],
+ [TorConnectState.Error,
+ [TorConnectState.Configuring]],
+ // terminal states
+ [TorConnectState.Bootstrapped, []],
+ [TorConnectState.Disabled, []],
+ ]));
+
+/* Topics Notified by the TorConnect module */
+const TorConnectTopics = Object.freeze({
+ StateChange: "torconnect:state-change",
+ BootstrapProgress: "torconnect:bootstrap-progress",
+ BootstrapComplete: "torconnect:bootstrap-complete",
+ BootstrapError: "torconnect:bootstrap-error",
+});
+
+// The StateCallback is a wrapper around an async function which executes during
+// the lifetime of a TorConnect State. A system is also provided to allow this
+// ongoing function to early-out via a per StateCallback on_transition callback
+// which may be called externally when we need to early-out and move on to another
+// state (for example, from Bootstrapping to Configuring in the event the user
+// cancels a bootstrap attempt)
+class StateCallback {
+
+ constructor(state, callback) {
+ this._state = state;
+ this._callback = callback;
+ this._init();
+ }
+
+ _init() {
+ // this context object is bound to the callback each time transition is
+ // attempted via begin()
+ this._context = {
+ // This callback may be overwritten in the _callback for each state
+ // States may have various pieces of work which need to occur
+ // before they can be exited (eg resource cleanup)
+ // See the _stateCallbacks map for examples
+ on_transition: (nextState) => {},
+
+ // flag used to determine if a StateCallback should early-out
+ // its work
+ _transitioning: false,
+
+ // may be called within the StateCallback to determine if exit is possible
+ get transitioning() {
+ return this._transitioning;
+ }
+ };
+ }
+
+ async begin(...args) {
+ console.log(`TorConnect: Entering ${this._state} state`);
+ this._init();
+ try {
+ // this Promise will block until this StateCallback has completed its work
+ await Promise.resolve(this._callback.call(this._context, ...args));
+ console.log(`TorConnect: Exited ${this._state} state`);
+
+ // handled state transition
+ Services.obs.notifyObservers({state: this._nextState}, TorConnectTopics.StateChange);
+ TorConnect._callback(this._nextState).begin(...this._nextStateArgs);
+ } catch (obj) {
+ TorConnect._changeState(TorConnectState.Error, obj?.message, obj?.details);
+ }
+ }
+
+ transition(nextState, ...args) {
+ this._nextState = nextState;
+ this._nextStateArgs = [...args];
+
+ // calls the on_transition callback to resolve any async work or do per-state cleanup
+ // this call to on_transition should resolve the async work currentlying going on in this.begin()
+ this._context.on_transition(nextState);
+ this._context._transitioning = true;
+ }
+}
+
+const TorConnect = (() => {
+ let retval = {
+
+ _state: TorConnectState.Initial,
+ _bootstrapProgress: 0,
+ _bootstrapStatus: null,
+ _errorMessage: null,
+ _errorDetails: null,
+ _logHasWarningOrError: false,
+ _transitionPromise: null,
+
+ /* These functions represent ongoing work associated with one of our states
+ Some of these functions are mostly empty, apart from defining an
+ on_transition function used to resolve their Promise */
+ _stateCallbacks: Object.freeze(new Map([
+ /* Initial is never transitioned to */
+ [TorConnectState.Initial, new StateCallback(TorConnectState.Initial, async function() {
+ // The initial state doesn't actually do anything, so here is a skeleton for other
+ // states which do perform work
+ await new Promise(async (resolve, reject) => {
+ // This function is provided to signal to the callback that it is complete.
+ // It is called as a result of _changeState and at the very least must
+ // resolve the root Promise object within the StateCallback function
+ // The on_transition callback may also perform necessary cleanup work
+ this.on_transition = (nextState) => {
+ resolve();
+ };
+
+ try {
+ // each state may have a sequence of async work to do
+ let asyncWork = async () => {};
+ await asyncWork();
+
+ // after each block we may check for an opportunity to early-out
+ if (this.transitioning) {
+ return;
+ }
+
+ // repeat the above pattern as necessary
+ } catch(err) {
+ // any thrown exceptions here will trigger a transition to the Error state
+ TorConnect._changeState(TorConnectState.Error, err?.message, err?.details);
+ }
+ });
+ })],
+ /* Configuring */
+ [TorConnectState.Configuring, new StateCallback(TorConnectState.Configuring, async function() {
+ await new Promise(async (resolve, reject) => {
+ this.on_transition = (nextState) => {
+ resolve();
+ };
+ });
+ })],
+ /* Bootstrapping */
+ [TorConnectState.Bootstrapping, new StateCallback(TorConnectState.Bootstrapping, async function() {
+ // wait until bootstrap completes or we get an error
+ await new Promise(async (resolve, reject) => {
+ const tbr = new TorBootstrapRequest();
+ this.on_transition = async (nextState) => {
+ if (nextState === TorConnectState.Configuring) {
+ // stop bootstrap process if user cancelled
+ await tbr.cancel();
+ }
+ resolve();
+ };
+
+ tbr.onbootstrapstatus = (progress, status) => {
+ TorConnect._updateBootstrapStatus(progress, status);
+ };
+ tbr.onbootstrapcomplete = () => {
+ TorConnect._changeState(TorConnectState.Bootstrapped);
+ };
+ tbr.onbootstraperror = (message, details) => {
+ TorConnect._changeState(TorConnectState.Error, message, details);
+ };
+
+ tbr.bootstrap();
+ });
+ })],
+ /* AutoBootstrapping */
+ [TorConnectState.AutoBootstrapping, new StateCallback(TorConnectState.AutoBootstrapping, async function(countryCode) {
+ await new Promise(async (resolve, reject) => {
+ this.on_transition = (nextState) => {
+ resolve();
+ };
+
+ // lookup user's potential censorship circumvention settings from Moat service
+ try {
+ this.mrpc = new MoatRPC();
+ await this.mrpc.init();
+
+ this.settings = await this.mrpc.circumvention_settings([...TorBuiltinBridgeTypes, "vanilla"], countryCode);
+
+ if (this.transitioning) return;
+
+ if (this.settings === null) {
+ // unable to determine country
+ TorConnect._changeState(TorConnectState.Error, "Unable to determine user country", "DETAILS_STRING");
+ return;
+ } else if (this.settings.length === 0) {
+ // no settings available for country
+ TorConnect._changeState(TorConnectState.Error, "No settings available for your location", "DETAILS_STRING");
+ return;
+ }
+ } catch (err) {
+ TorConnect._changeState(TorConnectState.Error, err?.message, err?.details);
+ return;
+ } finally {
+ // important to uninit MoatRPC object or else the pt process will live as long as tor-browser
+ this.mrpc?.uninit();
+ }
+
+ // apply each of our settings and try to bootstrap with each
+ try {
+ this.originalSettings = TorSettings.getSettings();
+
+ let index = 0;
+ for (let currentSetting of this.settings) {
+ // let us early out if user cancels
+ if (this.transitioning) return;
+
+ console.log(`TorConnect: Attempting Bootstrap with configuration ${++index}/${this.settings.length}`);
+
+ TorSettings.setSettings(currentSetting);
+ await TorSettings.applySettings();
+
+ // build out our bootstrap request
+ const tbr = new TorBootstrapRequest();
+ tbr.onbootstrapstatus = (progress, status) => {
+ TorConnect._updateBootstrapStatus(progress, status);
+ };
+ tbr.onbootstraperror = (message, details) => {
+ console.log(`TorConnect: Auto-Bootstrap error => ${message}; ${details}`);
+ };
+
+ // update transition callback for user cancel
+ this.on_transition = async (nextState) => {
+ if (nextState === TorConnectState.Configuring) {
+ await tbr.cancel();
+ }
+ resolve();
+ };
+
+ // begin bootstrap
+ if (await tbr.bootstrap()) {
+ // persist the current settings to preferences
+ TorSettings.saveToPrefs();
+ TorConnect._changeState(TorConnectState.Bootstrapped);
+ return;
+ }
+ }
+ // bootstrapped failed for all potential settings, so reset daemon to use original
+ TorSettings.setSettings(this.originalSettings);
+ await TorSettings.applySettings();
+ TorSettings.saveToPrefs();
+
+ // only explicitly change state here if something else has not transitioned us
+ if (!this.transitioning) {
+ TorConnect._changeState(TorConnectState.Error, "AutoBootstrapping failed", "DETAILS_STRING");
+ }
+ return;
+ } catch (err) {
+ // restore original settings in case of error
+ try {
+ TorSettings.setSettings(this.originalSettings);
+ await TorSettings.applySettings();
+ } catch(err) {
+ console.log(`TorConnect: Failed to restore original settings => ${err}`);
+ }
+ TorConnect._changeState(TorConnectState.Error, err?.message, err?.details);
+ return;
+ }
+ });
+ })],
+ /* Bootstrapped */
+ [TorConnectState.Bootstrapped, new StateCallback(TorConnectState.Bootstrapped, async function() {
+ await new Promise((resolve, reject) => {
+ // on_transition not defined because no way to leave Bootstrapped state
+ // notify observers of bootstrap completion
+ Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete);
+ });
+ })],
+ /* Error */
+ [TorConnectState.Error, new StateCallback(TorConnectState.Error, async function(errorMessage, errorDetails) {
+ await new Promise((resolve, reject) => {
+ this.on_transition = async(nextState) => {
+ resolve();
+ };
+
+ TorConnect._errorMessage = errorMessage;
+ TorConnect._errorDetails = errorDetails;
+
+ Services.obs.notifyObservers({message: errorMessage, details: errorDetails}, TorConnectTopics.BootstrapError);
+
+ TorConnect._changeState(TorConnectState.Configuring);
+ });
+ })],
+ /* Disabled */
+ [TorConnectState.Disabled, new StateCallback(TorConnectState.Disabled, async function() {
+ await new Promise((resolve, reject) => {
+ // no-op, on_transition not defined because no way to leave Disabled state
+ });
+ })],
+ ])),
+
+ _callback: function(state) {
+ return this._stateCallbacks.get(state);
+ },
+
+ _changeState: function(newState, ...args) {
+ const prevState = this._state;
+
+ // ensure this is a valid state transition
+ if (!TorConnectStateTransitions.get(prevState)?.includes(newState)) {
+ throw Error(`TorConnect: Attempted invalid state transition from ${prevState} to ${newState}`);
+ }
+
+ console.log(`TorConnect: Try transitioning from ${prevState} to ${newState}`);
+
+ // set our new state first so that state transitions can themselves trigger
+ // a state transition
+ this._state = newState;
+
+ // call our state function and forward any args
+ this._callback(prevState).transition(newState, ...args);
+ },
+
+ _updateBootstrapStatus: function(progress, status) {
+ this._bootstrapProgress= progress;
+ this._bootstrapStatus = status;
+
+ console.log(`TorConnect: Bootstrapping ${this._bootstrapProgress}% complete (${this._bootstrapStatus})`);
+ Services.obs.notifyObservers({
+ progress: TorConnect._bootstrapProgress,
+ status: TorConnect._bootstrapStatus,
+ hasWarnings: TorConnect._logHasWarningOrError
+ }, TorConnectTopics.BootstrapProgress);
+ },
+
+ // init should be called on app-startup in MainProcessingSingleton.jsm
+ init: function() {
+ console.log("TorConnect: init()");
+
+ // delay remaining init until after profile-after-change
+ Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange);
+
+ this._callback(TorConnectState.Initial).begin();
+ },
+
+ observe: async function(subject, topic, data) {
+ console.log(`TorConnect: Observed ${topic}`);
+
+ switch(topic) {
+
+ /* Determine which state to move to from Initial */
+ case BrowserTopics.ProfileAfterChange: {
+ if (TorLauncherUtil.useLegacyLauncher || !TorProtocolService.ownsTorDaemon) {
+ // Disabled
+ this._changeState(TorConnectState.Disabled);
+ } else {
+ let observeTopic = (topic) => {
+ Services.obs.addObserver(this, topic);
+ console.log(`TorConnect: Observing topic '${topic}'`);
+ };
+
+ // register the Tor topics we always care about
+ observeTopic(TorTopics.ProcessExited);
+ observeTopic(TorTopics.LogHasWarnOrErr);
+ observeTopic(TorSettingsTopics.Ready);
+ }
+ Services.obs.removeObserver(this, topic);
+ break;
+ }
+ /* We need to wait until TorSettings have been loaded and applied before we can Quickstart */
+ case TorSettingsTopics.Ready: {
+ if (this.shouldQuickStart) {
+ // Quickstart
+ this._changeState(TorConnectState.Bootstrapping);
+ } else {
+ // Configuring
+ this._changeState(TorConnectState.Configuring);
+ }
+ break;
+ }
+ case TorTopics.LogHasWarnOrErr: {
+ this._logHasWarningOrError = true;
+ break;
+ }
+ default:
+ // ignore
+ break;
+ }
+ },
+
+ /*
+ Various getters
+ */
+
+ get shouldShowTorConnect() {
+ // TorBrowser must control the daemon
+ return (TorProtocolService.ownsTorDaemon &&
+ // and we're not using the legacy launcher
+ !TorLauncherUtil.useLegacyLauncher &&
+ // if we have succesfully bootstraped, then no need to show TorConnect
+ this.state != TorConnectState.Bootstrapped);
+ },
+
+ get shouldQuickStart() {
+ // quickstart must be enabled
+ return TorSettings.quickstart.enabled &&
+ // and the previous bootstrap attempt must have succeeded
+ !Services.prefs.getBoolPref(TorLauncherPrefs.prompt_at_startup, true);
+ },
+
+ get state() {
+ return this._state;
+ },
+
+ get bootstrapProgress() {
+ return this._bootstrapProgress;
+ },
+
+ get bootstrapStatus() {
+ return this._bootstrapStatus;
+ },
+
+ get errorMessage() {
+ return this._errorMessage;
+ },
+
+ get errorDetails() {
+ return this._errorDetails;
+ },
+
+ get logHasWarningOrError() {
+ return this._logHasWarningOrError;
+ },
+
+ /*
+ These functions allow external consumers to tell TorConnect to transition states
+ */
+
+ beginBootstrap: function() {
+ console.log("TorConnect: beginBootstrap()");
+ this._changeState(TorConnectState.Bootstrapping);
+ },
+
+ cancelBootstrap: function() {
+ console.log("TorConnect: cancelBootstrap()");
+ this._changeState(TorConnectState.Configuring);
+ },
+
+ beginAutoBootstrap: function(countryCode) {
+ console.log("TorConnect: beginAutoBootstrap()");
+ this._changeState(TorConnectState.AutoBootstrapping, countryCode);
+ },
+
+ cancelAutoBootstrap: function() {
+ console.log("TorConnect: cancelAutoBootstrap()");
+ this._changeState(TorConnectState.Configuring);
+ },
+
+ /*
+ Further external commands and helper methods
+ */
+ openTorPreferences: function() {
+ const win = BrowserWindowTracker.getTopWindow();
+ win.switchToTabHavingURI("about:preferences#tor", true);
+ },
+
+ openTorConnect: function() {
+ const win = BrowserWindowTracker.getTopWindow();
+ win.switchToTabHavingURI("about:torconnect", true, {ignoreQueryString: true});
+ },
+
+ copyTorLogs: function() {
+ // Copy tor log messages to the system clipboard.
+ const chSvc = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ const countObj = { value: 0 };
+ chSvc.copyString(TorProtocolService.getLog(countObj));
+ const count = countObj.value;
+ return TorLauncherUtil.getFormattedLocalizedString(
+ "copiedNLogMessagesShort",
+ [count],
+ 1
+ );
+ },
+
+ getRedirectURL: function(url) {
+ return `about:torconnect?redirect=${encodeURIComponent(url)}`;
+ },
+
+ // called from browser.js on browser startup, passed in either the user's homepage(s)
+ // or uris passed via command-line; we want to replace them with about:torconnect uris
+ // which redirect after bootstrapping
+ getURIsToLoad: function(uriVariant) {
+ // convert the object we get from browser.js
+ let uriStrings = ((v) => {
+ // an interop array
+ if (v instanceof Ci.nsIArray) {
+ // Transform the nsIArray of nsISupportsString's into a JS Array of
+ // JS strings.
+ return Array.from(
+ v.enumerate(Ci.nsISupportsString),
+ supportStr => supportStr.data
+ );
+ // an interop string
+ } else if (v instanceof Ci.nsISupportsString) {
+ return [v.data];
+ // a js string
+ } else if (typeof v === "string") {
+ return v.split("|");
+ // a js array of js strings
+ } else if (Array.isArray(v) &&
+ v.reduce((allStrings, entry) => {return allStrings && (typeof entry === "string");}, true)) {
+ return v;
+ }
+ // about:tor as safe fallback
+ console.log(`TorConnect: getURIsToLoad() received unknown variant '${JSON.stringify(v)}'`);
+ return ["about:tor"];
+ })(uriVariant);
+
+ // will attempt to convert user-supplied string to a uri, fallback to about:tor if cannot convert
+ // to valid uri object
+ let uriStringToUri = (uriString) => {
+ const fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE;
+ let uri = Services.uriFixup.getFixupURIInfo(uriString, fixupFlags)
+ .preferredURI;
+ return uri ? uri : Services.io.newURI("about:tor");
+ };
+ let uris = uriStrings.map(uriStringToUri);
+
+ // assume we have a valid uri and generate an about:torconnect redirect uri
+ let redirectUrls = uris.map((uri) => this.getRedirectURL(uri.spec));
+
+ console.log(`TorConnect: Will load after bootstrap => [${uris.map((uri) => {return uri.spec;}).join(", ")}]`);
+ return redirectUrls;
+ },
+ };
+ retval.init();
+ return retval;
+})(); /* TorConnect */
diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm
new file mode 100644
index 000000000000..ac6d643691f6
--- /dev/null
+++ b/browser/modules/TorProtocolService.jsm
@@ -0,0 +1,484 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorProtocolService", "TorProcessStatus", "TorTopics", "TorBootstrapRequest"];
+
+const { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+);
+
+const { setTimeout, clearTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+const { TorLauncherUtil } = ChromeUtils.import(
+ "resource://torlauncher/modules/tl-util.jsm"
+);
+
+// see tl-process.js
+const TorProcessStatus = Object.freeze({
+ Unknown: 0,
+ Starting: 1,
+ Running: 2,
+ Exited: 3,
+});
+
+/* tor-launcher observer topics */
+const TorTopics = Object.freeze({
+ BootstrapStatus: "TorBootstrapStatus",
+ BootstrapError: "TorBootstrapError",
+ ProcessExited: "TorProcessExited",
+ LogHasWarnOrErr: "TorLogHasWarnOrErr",
+});
+
+/* Browser observer topis */
+const BrowserTopics = Object.freeze({
+ ProfileAfterChange: "profile-after-change",
+});
+
+var TorProtocolService = {
+ _TorLauncherUtil: function() {
+ let { TorLauncherUtil } = ChromeUtils.import(
+ "resource://torlauncher/modules/tl-util.jsm"
+ );
+ return TorLauncherUtil;
+ }(),
+ _TorLauncherProtocolService: null,
+ _TorProcessService: null,
+
+ // 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(),
+
+ init() {
+ Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange);
+ },
+
+ observe(subject, topic, data) {
+ if (topic === BrowserTopics.ProfileAfterChange) {
+ // we have to delay init'ing this or else the crypto service inits too early without a profile
+ // which breaks the password manager
+ this._TorLauncherProtocolService = Cc["@torproject.org/torlauncher-protocol-service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ this._TorProcessService = Cc["@torproject.org/torlauncher-process-service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject,
+
+ Services.obs.removeObserver(this, topic);
+ }
+ },
+
+ _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
+ async 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) {
+ settingsObject[setting] = value;
+ }
+
+ let errorObject = {};
+ if (! await this._TorLauncherProtocolService.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);
+ }
+ }
+ },
+
+ async _readSetting(aSetting) {
+ this._assertValidSettingKey(aSetting);
+ let reply = await this._TorLauncherProtocolService.TorGetConf(aSetting);
+ if (this._TorLauncherProtocolService.TorCommandSucceeded(reply)) {
+ return reply.lineArray;
+ }
+ throw new Error(reply.lineArray.join("\n"));
+ },
+
+ async _readBoolSetting(aSetting) {
+ let lineArray = await 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}'`);
+ }
+ },
+
+ async _readStringSetting(aSetting) {
+ let lineArray = await 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];
+ },
+
+ async _readStringArraySetting(aSetting) {
+ let lineArray = await this._readSetting(aSetting);
+ return lineArray;
+ },
+
+ async readBoolSetting(aSetting) {
+ let value = await this._readBoolSetting(aSetting);
+ this._settingsCache.set(aSetting, value);
+ return value;
+ },
+
+ async readStringSetting(aSetting) {
+ let value = await this._readStringSetting(aSetting);
+ this._settingsCache.set(aSetting, value);
+ return value;
+ },
+
+ async readStringArraySetting(aSetting) {
+ let value = await this._readStringArraySetting(aSetting);
+ this._settingsCache.set(aSetting, value);
+ return value;
+ },
+
+ // writes current tor settings to disk
+ async flushSettings() {
+ await this.sendCommand("SAVECONF");
+ },
+
+ getLog(countObj) {
+ countObj = countObj || { value: 0 };
+ let torLog = this._TorLauncherProtocolService.TorGetLog(countObj);
+ return torLog;
+ },
+
+ // true if we launched and control tor, false if using system tor
+ get ownsTorDaemon() {
+ return this._TorLauncherUtil.shouldStartAndOwnTor;
+ },
+
+ // Assumes `ownsTorDaemon` is true
+ isNetworkDisabled() {
+ const reply = TorProtocolService._TorLauncherProtocolService.TorGetConfBool(
+ "DisableNetwork",
+ true
+ );
+ if (TorProtocolService._TorLauncherProtocolService.TorCommandSucceeded(reply)) {
+ return reply.retVal;
+ }
+ return true;
+ },
+
+ async enableNetwork() {
+ let settings = {};
+ settings.DisableNetwork = false;
+ let errorObject = {};
+ if (! await this._TorLauncherProtocolService.TorSetConfWithReply(settings, errorObject)) {
+ throw new Error(errorObject.details);
+ }
+ },
+
+ async sendCommand(cmd) {
+ return await this._TorLauncherProtocolService.TorSendCommand(cmd);
+ },
+
+ retrieveBootstrapStatus() {
+ return this._TorLauncherProtocolService.TorRetrieveBootstrapStatus();
+ },
+
+ _GetSaveSettingsErrorMessage(aDetails) {
+ try {
+ return this._TorLauncherUtil.getSaveSettingsErrorMessage(aDetails);
+ } catch (e) {
+ console.log("GetSaveSettingsErrorMessage error", e);
+ return "Unexpected Error";
+ }
+ },
+
+ async setConfWithReply(settings) {
+ let result = false;
+ const error = {};
+ try {
+ result = await this._TorLauncherProtocolService.TorSetConfWithReply(settings, error);
+ } catch (e) {
+ console.log("TorSetConfWithReply error", e);
+ error.details = this._GetSaveSettingsErrorMessage(e.message);
+ }
+ return { result, error };
+ },
+
+ isBootstrapDone() {
+ return this._TorProcessService.mIsBootstrapDone;
+ },
+
+ clearBootstrapError() {
+ return this._TorProcessService.TorClearBootstrapError();
+ },
+
+ torBootstrapErrorOccurred() {
+ return this._TorProcessService.TorBootstrapErrorOccurred;
+ },
+
+ // Resolves to null if ok, or an error otherwise
+ async connect() {
+ const kTorConfKeyDisableNetwork = "DisableNetwork";
+ const settings = {};
+ settings[kTorConfKeyDisableNetwork] = false;
+ const { result, error } = await this.setConfWithReply(settings);
+ if (!result) {
+ return error;
+ }
+ try {
+ await this.sendCommand("SAVECONF");
+ this.clearBootstrapError();
+ this.retrieveBootstrapStatus();
+ } catch (e) {
+ return error;
+ }
+ return null;
+ },
+
+ torLogHasWarnOrErr() {
+ return this._TorLauncherProtocolService.TorLogHasWarnOrErr;
+ },
+
+ async torStopBootstrap() {
+ // Tell tor to disable use of the network; this should stop the bootstrap
+ // process.
+ const kErrorPrefix = "Setting DisableNetwork=1 failed: ";
+ try {
+ let settings = {};
+ settings.DisableNetwork = true;
+ const { result, error } = await this.setConfWithReply(settings);
+ if (!result) {
+ console.log(
+ `Error stopping bootstrap ${kErrorPrefix} ${error.details}`
+ );
+ }
+ } catch (e) {
+ console.log(`Error stopping bootstrap ${kErrorPrefix} ${e}`);
+ }
+ this.retrieveBootstrapStatus();
+ },
+
+ get torProcessStatus() {
+ if (this._TorProcessService) {
+ return this._TorProcessService.TorProcessStatus;
+ }
+ return TorProcessStatus.Unknown;
+ },
+};
+TorProtocolService.init();
+
+// modeled after XMLHttpRequest
+// nicely encapsulates the observer register/unregister logic
+class TorBootstrapRequest {
+ constructor() {
+ // number of ms to wait before we abandon the bootstrap attempt
+ // a value of 0 implies we never wait
+ this.timeout = 0;
+ // callbacks for bootstrap process status updates
+ this.onbootstrapstatus = (progress, status) => {};
+ this.onbootstrapcomplete = () => {};
+ this.onbootstraperror = (message, details) => {};
+
+ // internal resolve() method for bootstrap
+ this._bootstrapPromiseResolve = null;
+ this._bootstrapPromise = null;
+ this._timeoutID = null;
+ }
+
+ async observe(subject, topic, data) {
+ const obj = subject?.wrappedJSObject;
+ switch(topic) {
+ case TorTopics.BootstrapStatus: {
+ const progress = obj.PROGRESS;
+ const status = TorLauncherUtil.getLocalizedBootstrapStatus(obj, "TAG");
+ if (this.onbootstrapstatus) {
+ this.onbootstrapstatus(progress, status);
+ }
+ if (progress === 100) {
+ if (this.onbootstrapcomplete) {
+ this.onbootstrapcomplete();
+ }
+ this._bootstrapPromiseResolve(true);
+ clearTimeout(this._timeoutID);
+ }
+
+ break;
+ }
+ case TorTopics.BootstrapError: {
+ // first stop our bootstrap timeout before handling the error
+ clearTimeout(this._timeoutID);
+
+ await TorProtocolService.torStopBootstrap();
+
+ const message = obj.message;
+ const details = obj.details;
+ if (this.onbootstraperror) {
+ this.onbootstraperror(message, details);
+ }
+ this._bootstrapPromiseResolve(false);
+ break;
+ }
+ }
+ }
+
+ // resolves 'true' if bootstrap succeeds, false otherwise
+ async bootstrap() {
+ if (this._bootstrapPromise) return this._bootstrapPromise;
+
+ this._bootstrapPromise = new Promise(async (resolve, reject) => {
+ this._bootstrapPromiseResolve = resolve;
+
+ // register ourselves to listen for bootstrap events
+ Services.obs.addObserver(this, TorTopics.BootstrapStatus);
+ Services.obs.addObserver(this, TorTopics.BootstrapError);
+
+ // optionally cancel bootstrap after a given timeout
+ if (this.timeout > 0) {
+ this._timeoutID = setTimeout(async () => {
+ await TorProtocolService.torStopBootstrap();
+ if (this.onbootstraperror) {
+ this.onbootstraperror("Tor Bootstrap process timed out", `Bootstrap attempt abandoned after waiting ${this.timeout} ms`);
+ }
+ this._bootstrapPromiseResolve(false);
+ }, this.timeout);
+ }
+
+ // wait for bootstrapping to begin and maybe handle error
+ let err = await TorProtocolService.connect();
+ if (err) {
+ clearTimeout(this._timeoutID);
+ await TorProtocolService.torStopBootstrap();
+
+ const message = err.message;
+ const details = err.details;
+ if (this.onbootstraperror) {
+ this.onbootstraperror(message, details);
+ }
+ this._bootstrapPromiseResolve(false);
+ }
+ }).finally(() => {
+ // and remove ourselves once bootstrap is resolved
+ Services.obs.removeObserver(this, TorTopics.BootstrapStatus);
+ Services.obs.removeObserver(this, TorTopics.BootstrapError);
+ });
+
+ return this._bootstrapPromise;
+ }
+
+ async cancel() {
+ clearTimeout(this._timeoutID);
+
+ await TorProtocolService.torStopBootstrap();
+
+ this._bootstrapPromiseResolve(false);
+ }
+};
diff --git a/browser/modules/TorSettings.jsm b/browser/modules/TorSettings.jsm
new file mode 100644
index 000000000000..1b5b564e1e62
--- /dev/null
+++ b/browser/modules/TorSettings.jsm
@@ -0,0 +1,693 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorSettings", "TorSettingsTopics", "TorSettingsData", "TorBridgeSource", "TorBuiltinBridgeTypes", "TorProxyType"];
+
+const { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+);
+
+const { TorProtocolService, TorProcessStatus } = ChromeUtils.import(
+ "resource:///modules/TorProtocolService.jsm"
+);
+
+/* Browser observer topics */
+const BrowserTopics = Object.freeze({
+ ProfileAfterChange: "profile-after-change",
+});
+
+/* tor-launcher observer topics */
+const TorTopics = Object.freeze({
+ ProcessIsReady: "TorProcessIsReady",
+});
+
+/* TorSettings observer topics */
+const TorSettingsTopics = Object.freeze({
+ Ready: "torsettings:ready",
+ SettingChanged: "torsettings:setting-changed",
+});
+
+/* TorSettings observer data (for SettingChanged topic) */
+const TorSettingsData = Object.freeze({
+ QuickStartEnabled : "torsettings:quickstart_enabled",
+});
+
+/* Prefs used to store settings in TorBrowser prefs */
+const TorSettingsPrefs = Object.freeze({
+ /* bool: are we pulling tor settings from the preferences */
+ enabled: 'torbrowser.settings.enabled',
+ quickstart : {
+ /* bool: does tor connect automatically on launch */
+ enabled: 'torbrowser.settings.quickstart.enabled',
+ },
+ bridges : {
+ /* bool: does tor use bridges */
+ enabled : 'torbrowser.settings.bridges.enabled',
+ /* int: -1=invalid|0=builtin|1=bridge_db|2=user_provided */
+ source : 'torbrowser.settings.bridges.source',
+ /* string: obfs4|meek_azure|snowflake|etc */
+ builtin_type : 'torbrowser.settings.bridges.builtin_type',
+ /* preference branch: each child branch should be a bridge string */
+ bridge_strings : 'torbrowser.settings.bridges.bridge_strings',
+ },
+ proxy : {
+ /* bool: does tor use a proxy */
+ enabled : 'torbrowser.settings.proxy.enabled',
+ /* -1=invalid|0=socks4,1=socks5,2=https */
+ type: 'torbrowser.settings.proxy.type',
+ /* string: proxy server address */
+ address: 'torbrowser.settings.proxy.address',
+ /* int: [1,65535], proxy port */
+ port: 'torbrowser.settings.proxy.port',
+ /* string: username */
+ username: 'torbrowser.settings.proxy.username',
+ /* string: password */
+ password: 'torbrowser.settings.proxy.password',
+ },
+ firewall : {
+ /* bool: does tor have a port allow list */
+ enabled: 'torbrowser.settings.firewall.enabled',
+ /* string: comma-delimitted list of port numbers */
+ allowed_ports: 'torbrowser.settings.firewall.allowed_ports',
+ },
+});
+
+/* Legacy tor-launcher prefs and pref branches*/
+const TorLauncherPrefs = Object.freeze({
+ quickstart: "extensions.torlauncher.quickstart",
+ default_bridge_type: "extensions.torlauncher.default_bridge_type",
+ default_bridge: "extensions.torlauncher.default_bridge.",
+ default_bridge_recommended_type: "extensions.torlauncher.default_bridge_recommended_type",
+ bridgedb_bridge: "extensions.torlauncher.bridgedb_bridge.",
+});
+
+/* Config Keys used to configure tor daemon */
+const TorConfigKeys = Object.freeze({
+ useBridges: "UseBridges",
+ bridgeList: "Bridge",
+ socks4Proxy: "Socks4Proxy",
+ socks5Proxy: "Socks5Proxy",
+ socks5ProxyUsername: "Socks5ProxyUsername",
+ socks5ProxyPassword: "Socks5ProxyPassword",
+ httpsProxy: "HTTPSProxy",
+ httpsProxyAuthenticator: "HTTPSProxyAuthenticator",
+ reachableAddresses: "ReachableAddresses",
+ clientTransportPlugin: "ClientTransportPlugin",
+});
+
+const TorBridgeSource = Object.freeze({
+ Invalid: -1,
+ BuiltIn: 0,
+ BridgeDB: 1,
+ UserProvided: 2,
+});
+
+const TorProxyType = Object.freeze({
+ Invalid: -1,
+ Socks4: 0,
+ Socks5: 1,
+ HTTPS: 2,
+});
+
+
+const TorBuiltinBridgeTypes = Object.freeze(
+ (() => {
+ let bridgeListBranch = Services.prefs.getBranch(TorLauncherPrefs.default_bridge);
+ 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(TorLauncherPrefs.default_bridge_recommended_type, null);
+
+ let retval = [];
+ if (recommendedBridgeType && bridgeTypes.has(recommendedBridgeType)) {
+ retval.push(recommendedBridgeType);
+ }
+
+ for (const bridgeType of bridgeTypes.values()) {
+ if (bridgeType != recommendedBridgeType) {
+ retval.push(bridgeType);
+ }
+ }
+ return retval;
+ })()
+);
+
+/* Parsing Methods */
+
+// 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)) {
+ return 0;
+ }
+
+ // ensure port value is on valid range
+ let port = Number.parseInt(aPort);
+ if (port < 1 || port > 65535) {
+ return 0;
+ }
+
+ return port;
+};
+// expects a string in the format: "ADDRESS:PORT"
+let parseAddrPort = function(aAddrColonPort) {
+ let tokens = aAddrColonPort.split(":");
+ if (tokens.length != 2) {
+ return ["", 0];
+ }
+ 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) {
+ return ["", ""];
+ }
+
+ 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(",");
+ // parse and remove duplicates
+ let portSet = new Set(splitStrings.map(val => parsePort(val.trim())));
+ // parsePort returns 0 for failed parses, so remove 0 from list
+ portSet.delete(0);
+ return Array.from(portSet);
+};
+
+let getBuiltinBridgeStrings = function(builtinType) {
+ let bridgeBranch = Services.prefs.getBranch(TorLauncherPrefs.default_bridge);
+ 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) == builtinType.length &&
+ key.startsWith(builtinType)) {
+ let bridgeStr = bridgeBranch.getCharPref(key);
+ retval.push(bridgeStr);
+ }
+ }
+
+ // shuffle so that Tor Browser users don't all try the built-in bridges in the same order
+ arrayShuffle(retval);
+
+ return retval;
+};
+
+/* Array methods */
+
+let arrayShuffle = function(array) {
+ // fisher-yates shuffle
+ for (let i = array.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 = array[i];
+ array[i] = array[j];
+ array[j] = tmp;
+ }
+}
+
+let arrayCopy = function(array) {
+ return [].concat(array);
+}
+
+/* TorSettings module */
+
+const TorSettings = (() => {
+ let self = {
+ _settings: null,
+
+ // tor daemon related settings
+ defaultSettings: function() {
+ let settings = {
+ quickstart: {
+ enabled: false
+ },
+ bridges : {
+ enabled: false,
+ source: TorBridgeSource.Invalid,
+ builtin_type: null,
+ bridge_strings: [],
+ },
+ proxy: {
+ enabled: false,
+ type: TorProxyType.Invalid,
+ address: null,
+ port: 0,
+ username: null,
+ password: null,
+ },
+ firewall: {
+ enabled: false,
+ allowed_ports: [],
+ },
+ };
+ return settings;
+ },
+
+ /* load or init our settings, and register observers */
+ init: function() {
+ if (TorProtocolService.ownsTorDaemon) {
+ // if the settings branch exists, load settings from prefs
+ if (Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)) {
+ this.loadFromPrefs();
+ } else {
+ // otherwise load defaults
+ this._settings = this.defaultSettings();
+ }
+ Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange);
+ Services.obs.addObserver(this, TorTopics.ProcessIsReady);
+ }
+ },
+
+ /* wait for relevant life-cycle events to apply saved settings */
+ observe: async function(subject, topic, data) {
+ console.log(`TorSettings: Observed ${topic}`);
+
+ // once the tor daemon is ready, we need to apply our settings
+ let handleProcessReady = async () => {
+ // push down settings to tor
+ await this.applySettings();
+ console.log("TorSettings: Ready");
+ Services.obs.notifyObservers(null, TorSettingsTopics.Ready);
+ };
+
+ switch (topic) {
+ case BrowserTopics.ProfileAfterChange: {
+ Services.obs.removeObserver(this, BrowserTopics.ProfileAfterChange);
+ if (TorProtocolService.torProcessStatus == TorProcessStatus.Running) {
+ await handleProcessReady();
+ }
+ }
+ break;
+ case TorTopics.ProcessIsReady: {
+ Services.obs.removeObserver(this, TorTopics.ProcessIsReady);
+ await handleProcessReady();
+ }
+ break;
+ }
+ },
+
+ // load our settings from prefs
+ loadFromPrefs: function() {
+ console.log("TorSettings: loadFromPrefs()");
+
+ let settings = this.defaultSettings();
+
+ /* Quickstart */
+ settings.quickstart.enabled = Services.prefs.getBoolPref(TorSettingsPrefs.quickstart.enabled);
+ /* Bridges */
+ settings.bridges.enabled = Services.prefs.getBoolPref(TorSettingsPrefs.bridges.enabled);
+ if (settings.bridges.enabled) {
+ settings.bridges.source = Services.prefs.getIntPref(TorSettingsPrefs.bridges.source);
+ // builtin bridge (obfs4, meek, snowlfake, etc)
+ if (settings.bridges.source == TorBridgeSource.BuiltIn) {
+ let builtinType = Services.prefs.getStringPref(TorSettingsPrefs.bridges.builtin_type);
+ settings.bridges.builtin_type = builtinType;
+ // always dynamically load builtin bridges rather than loading the cached versions
+ // if the user upgrades and the builtin bridges have changed, we want to ensure the user
+ // can still bootstrap using the provided bridges
+ let bridgeStrings = getBuiltinBridgeStrings(builtinType);
+ if (bridgeStrings.length > 0) {
+ settings.bridges.bridge_strings = bridgeStrings;
+ } else {
+ // in this case the user is using a builtin bridge that is no longer supported,
+ // reset to settings to default values
+ settings.bridges.enabled = false;
+ settings.bridges.source = TorBridgeSource.Invalid;
+ settings.bridges.builtin_type = null;
+ }
+ } else {
+ settings.bridges.bridge_strings = [];
+ let bridgeBranchPrefs = Services.prefs.getBranch(TorSettingsPrefs.bridges.bridge_strings).getChildList("");
+ bridgeBranchPrefs.forEach(pref => {
+ let bridgeString = Services.prefs.getStringPref(`${TorSettingsPrefs.bridges.bridge_strings}${pref}`);
+ settings.bridges.bridge_strings.push(bridgeString);
+ });
+ }
+ } else {
+ settings.bridges.source = TorBridgeSource.Invalid;
+ settings.bridges.builtin_type = null;
+ settings.bridges.bridge_strings = [];
+ }
+ /* Proxy */
+ settings.proxy.enabled = Services.prefs.getBoolPref(TorSettingsPrefs.proxy.enabled);
+ if (settings.proxy.enabled) {
+ settings.proxy.type = Services.prefs.getIntPref(TorSettingsPrefs.proxy.type);
+ settings.proxy.address = Services.prefs.getStringPref(TorSettingsPrefs.proxy.address);
+ settings.proxy.port = Services.prefs.getIntPref(TorSettingsPrefs.proxy.port);
+ settings.proxy.username = Services.prefs.getStringPref(TorSettingsPrefs.proxy.username);
+ settings.proxy.password = Services.prefs.getStringPref(TorSettingsPrefs.proxy.password);
+ } else {
+ settings.proxy.type = TorProxyType.Invalid;
+ settings.proxy.address = null;
+ settings.proxy.port = 0;
+ settings.proxy.username = null;
+ settings.proxy.password = null;
+ }
+
+ /* Firewall */
+ settings.firewall.enabled = Services.prefs.getBoolPref(TorSettingsPrefs.firewall.enabled);
+ if(settings.firewall.enabled) {
+ let portList = Services.prefs.getStringPref(TorSettingsPrefs.firewall.allowed_ports);
+ settings.firewall.allowed_ports = parsePortList(portList);
+ } else {
+ settings.firewall.allowed_ports = 0;
+ }
+
+ this._settings = settings;
+
+ return this;
+ },
+
+ // save our settings to prefs
+ saveToPrefs: function() {
+ console.log("TorSettings: saveToPrefs()");
+
+ let settings = this._settings;
+
+ /* Quickstart */
+ Services.prefs.setBoolPref(TorSettingsPrefs.quickstart.enabled, settings.quickstart.enabled);
+ /* Bridges */
+ Services.prefs.setBoolPref(TorSettingsPrefs.bridges.enabled, settings.bridges.enabled);
+ if (settings.bridges.enabled) {
+ Services.prefs.setIntPref(TorSettingsPrefs.bridges.source, settings.bridges.source);
+ if (settings.bridges.source === TorBridgeSource.BuiltIn) {
+ Services.prefs.setStringPref(TorSettingsPrefs.bridges.builtin_type, settings.bridges.builtin_type);
+ } else {
+ Services.prefs.clearUserPref(TorSettingsPrefs.bridges.builtin_type);
+ }
+ // erase existing bridge strings
+ let bridgeBranchPrefs = Services.prefs.getBranch(TorSettingsPrefs.bridges.bridge_strings).getChildList("");
+ bridgeBranchPrefs.forEach(pref => {
+ Services.prefs.clearUserPref(`${TorSettingsPrefs.bridges.bridge_strings}${pref}`);
+ });
+ // write new ones
+ settings.bridges.bridge_strings.forEach((string, index) => {
+ Services.prefs.setStringPref(`${TorSettingsPrefs.bridges.bridge_strings}.${index}`, string);
+ });
+ } else {
+ Services.prefs.clearUserPref(TorSettingsPrefs.bridges.source);
+ Services.prefs.clearUserPref(TorSettingsPrefs.bridges.builtin_type);
+ let bridgeBranchPrefs = Services.prefs.getBranch(TorSettingsPrefs.bridges.bridge_strings).getChildList("");
+ bridgeBranchPrefs.forEach(pref => {
+ Services.prefs.clearUserPref(`${TorSettingsPrefs.bridges.bridge_strings}${pref}`);
+ });
+ }
+ /* Proxy */
+ Services.prefs.setBoolPref(TorSettingsPrefs.proxy.enabled, settings.proxy.enabled);
+ if (settings.proxy.enabled) {
+ Services.prefs.setIntPref(TorSettingsPrefs.proxy.type, settings.proxy.type);
+ Services.prefs.setStringPref(TorSettingsPrefs.proxy.address, settings.proxy.address);
+ Services.prefs.setIntPref(TorSettingsPrefs.proxy.port, settings.proxy.port);
+ Services.prefs.setStringPref(TorSettingsPrefs.proxy.username, settings.proxy.username);
+ Services.prefs.setStringPref(TorSettingsPrefs.proxy.password, settings.proxy.password);
+ } else {
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.type);
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.address);
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.port);
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.username);
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.password);
+ }
+ /* Firewall */
+ Services.prefs.setBoolPref(TorSettingsPrefs.firewall.enabled, settings.firewall.enabled);
+ if (settings.firewall.enabled) {
+ Services.prefs.setStringPref(TorSettingsPrefs.firewall.allowed_ports, settings.firewall.allowed_ports.join(","));
+ } else {
+ Services.prefs.clearUserPref(TorSettingsPrefs.firewall.allowed_ports);
+ }
+
+ // all tor settings now stored in prefs :)
+ Services.prefs.setBoolPref(TorSettingsPrefs.enabled, true);
+
+ return this;
+ },
+
+ // push our settings down to the tor daemon
+ applySettings: async function() {
+ console.log("TorSettings: applySettings()");
+ let settings = this._settings;
+ let settingsMap = new Map();
+
+ /* Bridges */
+ settingsMap.set(TorConfigKeys.useBridges, settings.bridges.enabled);
+ if (settings.bridges.enabled) {
+ settingsMap.set(TorConfigKeys.bridgeList, settings.bridges.bridge_strings);
+ } else {
+ // shuffle bridge list
+ settingsMap.set(TorConfigKeys.bridgeList, null);
+ }
+
+ /* Proxy */
+ settingsMap.set(TorConfigKeys.socks4Proxy, null);
+ settingsMap.set(TorConfigKeys.socks5Proxy, null);
+ settingsMap.set(TorConfigKeys.socks5ProxyUsername, null);
+ settingsMap.set(TorConfigKeys.socks5ProxyPassword, null);
+ settingsMap.set(TorConfigKeys.httpsProxy, null);
+ settingsMap.set(TorConfigKeys.httpsProxyAuthenticator, null);
+ if (settings.proxy.enabled) {
+ let address = settings.proxy.address;
+ let port = settings.proxy.port;
+ let username = settings.proxy.username;
+ let password = settings.proxy.password;
+
+ switch (settings.proxy.type) {
+ case TorProxyType.Socks4:
+ settingsMap.set(TorConfigKeys.socks4Proxy, `${address}:${port}`);
+ break;
+ case TorProxyType.Socks5:
+ settingsMap.set(TorConfigKeys.socks5Proxy, `${address}:${port}`);
+ settingsMap.set(TorConfigKeys.socks5ProxyUsername, username);
+ settingsMap.set(TorConfigKeys.socks5ProxyPassword, password);
+ break;
+ case TorProxyType.HTTPS:
+ settingsMap.set(TorConfigKeys.httpsProxy, `${address}:${port}`);
+ settingsMap.set(TorConfigKeys.httpsProxyAuthenticator, `${username}:${password}`);
+ break;
+ }
+ }
+
+ /* Firewall */
+ if (settings.firewall.enabled) {
+ let reachableAddresses = settings.firewall.allowed_ports.map(port => `*:${port}`).join(",");
+ settingsMap.set(TorConfigKeys.reachableAddresses, reachableAddresses);
+ } else {
+ settingsMap.set(TorConfigKeys.reachableAddresses, null);
+ }
+
+ /* Push to Tor */
+ await TorProtocolService.writeSettings(settingsMap);
+
+ return this;
+ },
+
+ // set all of our settings at once from a settings object
+ setSettings: function(settings) {
+ console.log("TorSettings: setSettings()");
+ let backup = this.getSettings();
+
+ try {
+ if (settings.bridges.enabled) {
+ this._settings.bridges.enabled = true;
+ this._settings.bridges.source = settings.bridges.source;
+ switch(settings.bridges.source) {
+ case TorBridgeSource.BridgeDB:
+ case TorBridgeSource.UserProvided:
+ this._settings.bridges.bridge_strings = settings.bridges.bridge_strings
+ break;
+ case TorBridgeSource.BuiltIn: {
+ this._settings.bridges.builtin_type = settings.bridges.builtin_type;
+ let bridgeStrings = getBuiltinBridgeStrings(settings.bridges.builtin_type);
+ if (bridgeStrings.length > 0) {
+ this._settings.bridges.bridge_strings = bridgeStrings;
+ } else {
+ throw new Error(`No available builtin bridges of type ${settings.bridges.builtin_type}`);
+ }
+ break;
+ }
+ default:
+ throw new Error(`Bridge source '${settings.source}' is not a valid source`);
+ }
+ } else {
+ this.bridges.enabled = false;
+ }
+
+ // TODO: proxy and firewall
+ } catch(ex) {
+ this._settings = backup;
+ console.log(`TorSettings: setSettings failed => ${ex.message}`);
+ }
+
+ console.log("TorSettings: setSettings result");
+ console.log(this._settings);
+ },
+
+ // get a copy of all our settings
+ getSettings: function() {
+ console.log("TorSettings: getSettings()");
+ // TODO: replace with structuredClone someday (post esr94): https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
+ return JSON.parse(JSON.stringify(this._settings));
+ },
+
+ /* Getters and Setters */
+
+ // Quickstart
+ get quickstart() {
+ return {
+ get enabled() { return self._settings.quickstart.enabled; },
+ set enabled(val) {
+ if (val != self._settings.quickstart.enabled)
+ {
+ self._settings.quickstart.enabled = val;
+ Services.obs.notifyObservers({value: val}, TorSettingsTopics.SettingChanged, TorSettingsData.QuickStartEnabled);
+ }
+ },
+ };
+ },
+
+ // Bridges
+ get bridges() {
+ return {
+ get enabled() { return self._settings.bridges.enabled; },
+ set enabled(val) {
+ self._settings.bridges.enabled = val;
+ // reset bridge settings
+ self._settings.bridges.source = TorBridgeSource.Invalid;
+ self._settings.bridges.builtin_type = null;
+ self._settings.bridges.bridge_strings = [];
+ },
+ get source() { return self._settings.bridges.source; },
+ set source(val) { self._settings.bridges.source = val; },
+ get builtin_type() { return self._settings.bridges.builtin_type; },
+ set builtin_type(val) {
+ let bridgeStrings = getBuiltinBridgeStrings(val);
+ if (bridgeStrings.length > 0) {
+ self._settings.bridges.builtin_type = val;
+ self._settings.bridges.bridge_strings = bridgeStrings;
+ }
+ },
+ get bridge_strings() { return arrayCopy(self._settings.bridges.bridge_strings); },
+ set bridge_strings(val) {
+ self._settings.bridges.bridge_strings = parseBridgeStrings(val);
+ },
+ };
+ },
+
+ // Proxy
+ get proxy() {
+ return {
+ get enabled() { return self._settings.proxy.enabled; },
+ set enabled(val) {
+ self._settings.proxy.enabled = val;
+ // reset proxy settings
+ self._settings.proxy.type = TorProxyType.Invalid;
+ self._settings.proxy.address = null;
+ self._settings.proxy.port = 0;
+ self._settings.proxy.username = null;
+ self._settings.proxy.password = null;
+ },
+ get type() { return self._settings.proxy.type; },
+ set type(val) { self._settings.proxy.type = val; },
+ get address() { return self._settings.proxy.address; },
+ set address(val) { self._settings.proxy.address = val; },
+ get port() { return arrayCopy(self._settings.proxy.port); },
+ set port(val) { self._settings.proxy.port = parsePort(val); },
+ get username() { return self._settings.proxy.username; },
+ set username(val) { self._settings.proxy.username = val; },
+ get password() { return self._settings.proxy.password; },
+ set password(val) { self._settings.proxy.password = val; },
+ get uri() {
+ switch (this.type) {
+ case TorProxyType.Socks4:
+ return `socks4a://${this.address}:${this.port}`;
+ case TorProxyType.Socks5:
+ if (this.username) {
+ return `socks5://${this.username}:${this.password}@${this.address}:${this.port}`;
+ }
+ return `socks5://${this.address}:${this.port}`;
+ case TorProxyType.HTTPS:
+ if (this._proxyUsername) {
+ return `http://${this.username}:${this.password}@${this.address}:${this.port}`;
+ }
+ return `http://${this.address}:${this.port}`;
+ }
+ return null;
+ },
+ };
+ },
+
+ // Firewall
+ get firewall() {
+ return {
+ get enabled() { return self._settings.firewall.enabled; },
+ set enabled(val) {
+ self._settings.firewall.enabled = val;
+ // reset firewall settings
+ self._settings.firewall.allowed_ports = [];
+ },
+ get allowed_ports() { return self._settings.firewall.allowed_ports; },
+ set allowed_ports(val) { self._settings.firewall.allowed_ports = parsePortList(val); },
+ };
+ },
+ };
+ self.init();
+ return self;
+})();
diff --git a/browser/modules/moz.build b/browser/modules/moz.build
index b069f1b641f7..646784690c9a 100644
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -128,6 +128,7 @@ EXTRA_JS_MODULES += [
"AboutNewTab.jsm",
"AppUpdater.jsm",
"AsyncTabSwitcher.jsm",
+ "BridgeDB.jsm",
"BrowserUIUtils.jsm",
"BrowserUsageTelemetry.jsm",
"BrowserWindowTracker.jsm",
@@ -138,6 +139,7 @@ EXTRA_JS_MODULES += [
"FaviconLoader.jsm",
"HomePage.jsm",
"LaterRun.jsm",
+ 'Moat.jsm',
"NewTabPagePreloading.jsm",
"OpenInTabsUtils.jsm",
"PageActions.jsm",
@@ -152,6 +154,8 @@ EXTRA_JS_MODULES += [
"TabsList.jsm",
"TabUnloader.jsm",
"ThemeVariableMap.jsm",
+ "TorProtocolService.jsm",
+ "TorSettings.jsm",
"TransientPrefs.jsm",
"webrtcUI.jsm",
"ZoomUI.jsm",
diff --git a/toolkit/components/processsingleton/MainProcessSingleton.jsm b/toolkit/components/processsingleton/MainProcessSingleton.jsm
index ecdbf2a01d99..7bde782e54ce 100644
--- a/toolkit/components/processsingleton/MainProcessSingleton.jsm
+++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm
@@ -24,6 +24,11 @@ MainProcessSingleton.prototype = {
null
);
+ ChromeUtils.import(
+ "resource:///modules/TorSettings.jsm",
+ null
+ );
+
Services.ppmm.loadProcessScript(
"chrome://global/content/process-content.js",
true
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits