richard pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits: d339153b by Pier Angelo Vendrame at 2023-08-07T18:36:05+02:00 fixup! Bug 40933: Add tor-launcher functionality
Use the new functions whenever possible, adjust some property names and other minor fixes.
- - - - - e8d3c0b8 by Pier Angelo Vendrame at 2023-08-07T18:36:14+02:00 fixup! Bug 40597: Implement TorSettings module
Changes needed for the new control port implementation.
Also, moved to ES modules and done some refactors on Moat.
- - - - - c1b05a65 by Pier Angelo Vendrame at 2023-08-07T18:36:15+02:00 fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Changes for the new control port implementation and following ESMification.
- - - - - d23cb300 by Pier Angelo Vendrame at 2023-08-07T18:36:15+02:00 fixup! Bug 30237: Add v3 onion services client authentication prompt
Changes for the new control port implementation.
- - - - - c801d620 by Pier Angelo Vendrame at 2023-08-07T18:36:16+02:00 fixup! Bug 7494: Create local home page for TBB.
Use the TorProvider in TorCheckService
- - - - - fa105e4f by Pier Angelo Vendrame at 2023-08-07T18:36:16+02:00 fixup! Bug 3455: Add DomainIsolator, for isolating circuit by domain.
Remove TorMonitorService and TorProtocolService references.
- - - - - cd92f30e by Pier Angelo Vendrame at 2023-08-07T18:36:17+02:00 fixup! Bug 41668: Tweaks to the Base Browser updater for Tor Browser
Removed TorMonitorService reference
- - - - - 384dff97 by Pier Angelo Vendrame at 2023-08-07T18:36:17+02:00 fixup! Bug 40933: Add tor-launcher functionality
Remove the final references to TorMonitorService and TorProtocolService.
- - - - -
24 changed files:
- browser/components/abouttor/TorCheckService.sys.mjs - browser/components/onionservices/content/authPrompt.js - browser/components/onionservices/content/savedKeysDialog.js - browser/components/torpreferences/content/builtinBridgeDialog.jsm - browser/components/torpreferences/content/connectionPane.js - browser/components/torpreferences/content/connectionSettingsDialog.jsm - browser/components/torpreferences/content/provideBridgeDialog.jsm - browser/components/torpreferences/content/requestBridgeDialog.jsm - browser/components/torpreferences/content/torLogDialog.jsm - browser/modules/BridgeDB.jsm → browser/modules/BridgeDB.sys.mjs - browser/modules/Moat.jsm → browser/modules/Moat.sys.mjs - browser/modules/TorConnect.jsm → browser/modules/TorConnect.sys.mjs - browser/modules/TorSettings.jsm → browser/modules/TorSettings.sys.mjs - browser/modules/moz.build - toolkit/components/tor-launcher/TorBootstrapRequest.sys.mjs - toolkit/components/tor-launcher/TorControlPort.sys.mjs - toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs - − toolkit/components/tor-launcher/TorMonitorService.sys.mjs - toolkit/components/tor-launcher/TorParsers.sys.mjs - toolkit/components/tor-launcher/TorProtocolService.sys.mjs → toolkit/components/tor-launcher/TorProvider.sys.mjs - toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs - toolkit/components/tor-launcher/TorStartupService.sys.mjs - toolkit/components/tor-launcher/moz.build - toolkit/mozapps/update/UpdateService.sys.mjs
Changes:
===================================== browser/components/abouttor/TorCheckService.sys.mjs ===================================== @@ -11,14 +11,9 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, { ConsoleAPI: "resource://gre/modules/Console.sys.mjs", + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", });
-ChromeUtils.defineModuleGetter( - lazy, - "TorProtocolService", - "resource://gre/modules/TorProtocolService.jsm" -); - export const TorCheckService = { kCheckNotInitiated: 0, // Possible values for status. kCheckSuccessful: 1, @@ -109,7 +104,7 @@ export const TorCheckService = {
let listeners; try { - listeners = await lazy.TorProtocolService.getSocksListeners(); + listeners = await lazy.TorProviderBuilder.build().getSocksListeners(); } catch (e) { this._logger.error("Failed to get the SOCKS listerner addresses.", e); return false;
===================================== browser/components/onionservices/content/authPrompt.js ===================================== @@ -4,10 +4,13 @@
/* globals gBrowser, PopupNotifications, Services, XPCOMUtils */
+ChromeUtils.defineESModuleGetters(this, { + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", +}); + XPCOMUtils.defineLazyModuleGetters(this, { OnionAuthUtil: "chrome://browser/content/onionservices/authUtil.jsm", CommonUtils: "resource://services-common/utils.js", - TorProtocolService: "resource://gre/modules/TorProtocolService.jsm", TorStrings: "resource:///modules/TorStrings.jsm", });
@@ -203,7 +206,8 @@ const OnionAuthPrompt = (function () {
let checkboxElem = this._getCheckboxElement(); let isPermanent = checkboxElem && checkboxElem.checked; - TorProtocolService.onionAuthAdd(onionServiceId, base64key, isPermanent) + TorProviderBuilder.build() + .onionAuthAdd(onionServiceId, base64key, isPermanent) .then(aResponse => { // Success! Reload the page. this._browser.sendMessageToActor(
===================================== browser/components/onionservices/content/savedKeysDialog.js ===================================== @@ -8,11 +8,9 @@ ChromeUtils.defineModuleGetter( "resource:///modules/TorStrings.jsm" );
-ChromeUtils.defineModuleGetter( - this, - "TorProtocolService", - "resource://gre/modules/TorProtocolService.jsm" -); +ChromeUtils.defineESModuleGetters(this, { + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", +});
var gOnionServicesSavedKeysDialog = { selector: { @@ -54,6 +52,7 @@ var gOnionServicesSavedKeysDialog = { await this._deleteOneKey(indexesToDelete[i]); } } catch (e) { + console.error("Removing a saved key failed", e); if (e.torMessage) { this._showError(e.torMessage); } else { @@ -125,22 +124,16 @@ var gOnionServicesSavedKeysDialog = { try { this._tree.view = this;
- const keyInfoList = await TorProtocolService.onionAuthViewKeys(); + const keyInfoList = await TorProviderBuilder.build().onionAuthViewKeys(); if (keyInfoList) { // Filter out temporary keys. - this._keyInfoList = keyInfoList.filter(aKeyInfo => { - if (!aKeyInfo.Flags) { - return false; - } - - const flags = aKeyInfo.Flags.split(","); - return flags.includes("Permanent"); - }); - + this._keyInfoList = keyInfoList.filter(aKeyInfo => + aKeyInfo.flags?.includes("Permanent") + ); // Sort by the .onion address. this._keyInfoList.sort((aObj1, aObj2) => { - const hsAddr1 = aObj1.hsAddress.toLowerCase(); - const hsAddr2 = aObj2.hsAddress.toLowerCase(); + const hsAddr1 = aObj1.address.toLowerCase(); + const hsAddr2 = aObj2.address.toLowerCase(); if (hsAddr1 < hsAddr2) { return -1; } @@ -164,7 +157,7 @@ var gOnionServicesSavedKeysDialog = { // This method may throw; callers should catch errors. async _deleteOneKey(aIndex) { const keyInfoObj = this._keyInfoList[aIndex]; - await TorProtocolService.onionAuthRemove(keyInfoObj.hsAddress); + await TorProviderBuilder.build().onionAuthRemove(keyInfoObj.address); this._tree.view.selection.clearRange(aIndex, aIndex); this._keyInfoList.splice(aIndex, 1); this._tree.rowCountChanged(aIndex + 1, -1); @@ -193,26 +186,20 @@ var gOnionServicesSavedKeysDialog = {
// XUL tree widget view implementation. get rowCount() { - return this._keyInfoList ? this._keyInfoList.length : 0; + return this._keyInfoList?.length ?? 0; },
getCellText(aRow, aCol) { - let val = ""; if (this._keyInfoList && aRow < this._keyInfoList.length) { const keyInfo = this._keyInfoList[aRow]; if (aCol.id.endsWith("-siteCol")) { - val = keyInfo.hsAddress; + return keyInfo.address; } else if (aCol.id.endsWith("-keyCol")) { - val = keyInfo.typeAndKey; - // Omit keyType because it is always "x25519". - const idx = val.indexOf(":"); - if (idx > 0) { - val = val.substring(idx + 1); - } + // keyType is always "x25519", so do not show it. + return keyInfo.keyBlob; } } - - return val; + return ""; },
isSeparator(index) {
===================================== browser/components/torpreferences/content/builtinBridgeDialog.jsm ===================================== @@ -7,10 +7,10 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
const { TorSettings, TorBridgeSource, TorBuiltinBridgeTypes } = - ChromeUtils.import("resource:///modules/TorSettings.jsm"); + ChromeUtils.importESModule("resource:///modules/TorSettings.sys.mjs");
-const { TorConnect, TorConnectTopics } = ChromeUtils.import( - "resource:///modules/TorConnect.jsm" +const { TorConnect, TorConnectTopics } = ChromeUtils.importESModule( + "resource:///modules/TorConnect.sys.mjs" );
class BuiltinBridgeDialog {
===================================== browser/components/torpreferences/content/connectionPane.js ===================================== @@ -12,20 +12,17 @@ const { setTimeout, clearTimeout } = ChromeUtils.import( );
const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource } = - ChromeUtils.import("resource:///modules/TorSettings.jsm"); + ChromeUtils.importESModule("resource:///modules/TorSettings.sys.mjs");
const { TorParsers } = ChromeUtils.importESModule( "resource://gre/modules/TorParsers.sys.mjs" ); -const { TorProtocolService } = ChromeUtils.importESModule( - "resource://gre/modules/TorProtocolService.sys.mjs" -); -const { TorMonitorService, TorMonitorTopics } = ChromeUtils.import( - "resource://gre/modules/TorMonitorService.jsm" +const { TorProviderBuilder, TorProviderTopics } = ChromeUtils.importESModule( + "resource://gre/modules/TorProviderBuilder.sys.mjs" );
const { TorConnect, TorConnectTopics, TorConnectState, TorCensorshipLevel } = - ChromeUtils.import("resource:///modules/TorConnect.jsm"); + ChromeUtils.importESModule("resource:///modules/TorConnect.sys.mjs");
const { TorLogDialog } = ChromeUtils.import( "chrome://browser/content/torpreferences/torLogDialog.jsm" @@ -51,7 +48,9 @@ const { ProvideBridgeDialog } = ChromeUtils.import( "chrome://browser/content/torpreferences/provideBridgeDialog.jsm" );
-const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm"); +const { MoatRPC } = ChromeUtils.importESModule( + "resource:///modules/Moat.sys.mjs" +);
const { QRCode } = ChromeUtils.import("resource://gre/modules/QRCode.jsm");
@@ -156,7 +155,7 @@ const gConnectionPane = (function () { _populateXUL() { // saves tor settings to disk when navigate away from about:preferences window.addEventListener("blur", val => { - TorProtocolService.flushSettings(); + TorProviderBuilder.build().flushSettings(); });
document @@ -751,7 +750,7 @@ const gConnectionPane = (function () { // TODO: We could make sure TorSettings is in sync by monitoring also // changes of settings. At that point, we could query it, instead of // doing a query over the control port. - const bridge = TorMonitorService.currentBridge; + const bridge = TorProviderBuilder.build().currentBridge; if (bridge?.fingerprint !== this._currentBridgeId) { this._currentBridgeId = bridge?.fingerprint ?? null; this._updateConnectedBridges(); @@ -850,7 +849,7 @@ const gConnectionPane = (function () { });
Services.obs.addObserver(this, TorConnectTopics.StateChange); - Services.obs.addObserver(this, TorMonitorTopics.BridgeChanged); + Services.obs.addObserver(this, TorProviderTopics.BridgeChanged); Services.obs.addObserver(this, "intl:app-locales-changed"); },
@@ -875,7 +874,7 @@ const gConnectionPane = (function () { // unregister our observer topics Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged); Services.obs.removeObserver(this, TorConnectTopics.StateChange); - Services.obs.removeObserver(this, TorMonitorTopics.BridgeChanged); + Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged); Services.obs.removeObserver(this, "intl:app-locales-changed"); },
@@ -907,7 +906,7 @@ const gConnectionPane = (function () { this.onStateChange(); break; } - case TorMonitorTopics.BridgeChanged: { + case TorProviderTopics.BridgeChanged: { if (data?.fingerprint !== this._currentBridgeId) { this._checkConnectedBridge(); }
===================================== browser/components/torpreferences/content/connectionSettingsDialog.jsm ===================================== @@ -2,8 +2,8 @@
var EXPORTED_SYMBOLS = ["ConnectionSettingsDialog"];
-const { TorSettings, TorProxyType } = ChromeUtils.import( - "resource:///modules/TorSettings.jsm" +const { TorSettings, TorProxyType } = ChromeUtils.importESModule( + "resource:///modules/TorSettings.sys.mjs" );
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
===================================== browser/components/torpreferences/content/provideBridgeDialog.jsm ===================================== @@ -6,12 +6,12 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
-const { TorSettings, TorBridgeSource } = ChromeUtils.import( - "resource:///modules/TorSettings.jsm" +const { TorSettings, TorBridgeSource } = ChromeUtils.importESModule( + "resource:///modules/TorSettings.sys.mjs" );
-const { TorConnect, TorConnectTopics } = ChromeUtils.import( - "resource:///modules/TorConnect.jsm" +const { TorConnect, TorConnectTopics } = ChromeUtils.importESModule( + "resource:///modules/TorConnect.sys.mjs" );
class ProvideBridgeDialog {
===================================== browser/components/torpreferences/content/requestBridgeDialog.jsm ===================================== @@ -4,11 +4,13 @@ var EXPORTED_SYMBOLS = ["RequestBridgeDialog"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const { BridgeDB } = ChromeUtils.import("resource:///modules/BridgeDB.jsm"); +const { BridgeDB } = ChromeUtils.importESModule( + "resource:///modules/BridgeDB.sys.mjs" +); const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
-const { TorConnect, TorConnectTopics } = ChromeUtils.import( - "resource:///modules/TorConnect.jsm" +const { TorConnect, TorConnectTopics } = ChromeUtils.importESModule( + "resource:///modules/TorConnect.sys.mjs" );
class RequestBridgeDialog {
===================================== browser/components/torpreferences/content/torLogDialog.jsm ===================================== @@ -2,12 +2,12 @@
var EXPORTED_SYMBOLS = ["TorLogDialog"];
-const { setTimeout, clearTimeout } = ChromeUtils.import( - "resource://gre/modules/Timer.jsm" +const { setTimeout, clearTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" );
-const { TorMonitorService } = ChromeUtils.import( - "resource://gre/modules/TorMonitorService.jsm" +const { TorProviderBuilder } = ChromeUtils.importESModule( + "resource://gre/modules/TorProviderBuilder.sys.mjs" ); const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
@@ -56,7 +56,7 @@ class TorLogDialog { }, RESTORE_TIME); });
- this._logTextarea.value = TorMonitorService.getLog(); + this._logTextarea.value = TorProviderBuilder.build().getLog(); }
init(window, aDialog) {
===================================== browser/modules/BridgeDB.jsm → browser/modules/BridgeDB.sys.mjs ===================================== @@ -1,10 +1,14 @@ -"use strict"; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-var EXPORTED_SYMBOLS = ["BridgeDB"]; +const lazy = {};
-const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm"); +ChromeUtils.defineESModuleGetters(lazy, { + MoatRPC: "resource:///modules/Moat.sys.mjs", +});
-var BridgeDB = { +export var BridgeDB = { _moatRPC: null, _challenge: null, _image: null, @@ -20,7 +24,7 @@ var BridgeDB = {
async submitCaptchaGuess(solution) { if (!this._moatRPC) { - this._moatRPC = new MoatRPC(); + this._moatRPC = new lazy.MoatRPC(); await this._moatRPC.init(); }
@@ -37,7 +41,7 @@ var BridgeDB = { async requestNewCaptchaImage() { try { if (!this._moatRPC) { - this._moatRPC = new MoatRPC(); + this._moatRPC = new lazy.MoatRPC(); await this._moatRPC.init(); }
===================================== browser/modules/Moat.jsm → browser/modules/Moat.sys.mjs ===================================== @@ -1,24 +1,19 @@ -"use strict"; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-var EXPORTED_SYMBOLS = ["MoatRPC"]; +import { + TorSettings, + TorBridgeSource, +} from "resource:///modules/TorSettings.sys.mjs";
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const lazy = {};
-const { Subprocess } = ChromeUtils.import( - "resource://gre/modules/Subprocess.jsm" -); - -const { TorLauncherUtil } = ChromeUtils.import( - "resource://gre/modules/TorLauncherUtil.jsm" -); - -const { TorProtocolService } = ChromeUtils.import( - "resource://gre/modules/TorProtocolService.jsm" -); - -const { TorSettings, TorBridgeSource } = ChromeUtils.import( - "resource:///modules/TorSettings.jsm" -); +ChromeUtils.defineESModuleGetters(lazy, { + Subprocess: "resource://gre/modules/Subprocess.sys.mjs", + TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs", + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", +});
const TorLauncherPrefs = Object.freeze({ bridgedb_front: "extensions.torlauncher.bridgedb_front", @@ -26,73 +21,54 @@ const TorLauncherPrefs = Object.freeze({ 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; - } + // These members are used by consumers to setup the proxy to do requests over + // meek. They are passed to newProxyInfoWithAuth. + proxyType = null; + proxyAddress = null; + proxyPort = 0; + proxyUsername = null; + proxyPassword = null; + + #inited = false; + #meekClientProcess = null;
// launches the meekprocess async init() { // ensure we haven't already init'd - if (this._inited) { + 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 + const proxy = ( + await lazy.TorProviderBuilder.build().getPluggableTransports() + ).find( + pt => + pt.type === "exec" && + supportedTransports.some(t => pt.transports.includes(t)) ); + if (!proxy) { + throw new Error("No supported transport found."); + }
- 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, - }; - })(); - + const meekTransport = proxy.transports.find(t => + supportedTransports.includes(t) + ); // Convert meek client path to absolute path if necessary - let meekWorkDir = TorLauncherUtil.getTorFile("pt-startup-dir", false); - if (TorLauncherUtil.isPathRelative(meekClientPath)) { - let meekPath = meekWorkDir.clone(); - meekPath.appendRelativePath(meekClientPath); - meekClientPath = meekPath.path; + const meekWorkDir = lazy.TorLauncherUtil.getTorFile( + "pt-startup-dir", + false + ); + if (lazy.TorLauncherUtil.isPathRelative(proxy.pathToBinary)) { + const meekPath = meekWorkDir.clone(); + meekPath.appendRelativePath(proxy.pathToBinary); + proxy.pathToBinary = meekPath.path; }
// Construct the per-connection arguments. @@ -105,16 +81,13 @@ class MeekTransport { // 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; - }; + const escapeArgValue = aValue => + aValue + ? aValue + .replaceAll("\", "\\") + .replaceAll("=", "\=") + .replaceAll(";", "\;") + : "";
if (meekReflector) { meekClientEscapedArgs += "url="; @@ -132,10 +105,10 @@ class MeekTransport { }
// Setup env and start meek process - let ptStateDir = TorLauncherUtil.getTorFile("tordatadir", false); + const ptStateDir = lazy.TorLauncherUtil.getTorFile("tordatadir", false); ptStateDir.append("pt_state"); // Match what tor uses.
- let envAdditions = { + const envAdditions = { TOR_PT_MANAGED_TRANSPORT_VER: "1", TOR_PT_STATE_LOCATION: ptStateDir.path, TOR_PT_EXIT_ON_STDIN_CLOSE: "1", @@ -145,9 +118,9 @@ class MeekTransport { envAdditions.TOR_PT_PROXY = TorSettings.proxy.uri; }
- let opts = { - command: meekClientPath, - arguments: meekClientArgs, + const opts = { + command: proxy.pathToBinary, + arguments: proxy.options.split(/s+/), workdir: meekWorkDir.path, environmentAppend: true, environment: envAdditions, @@ -155,27 +128,23 @@ class MeekTransport { };
// Launch meek client - let meekClientProcess = await Subprocess.call(opts); - // kill our process if exception is thrown - onException = () => { - meekClientProcess.kill(); - }; + this.#meekClientProcess = await lazy.Subprocess.call(opts);
// 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(); + const stderrLogger = async () => { + while (this.#meekClientProcess) { + const errString = await this.#meekClientProcess.stderr.readString(); + if (errString) { + console.log(`MeekTransport: stderr => ${errString}`); + } } }; 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; + const getInitLines = async (stdout = "") => { + stdout += await this.#meekClientProcess.stdout.readString();
// look for the final message const CMETHODS_DONE = "CMETHODS DONE"; @@ -188,20 +157,16 @@ class MeekTransport { };
// read our lines from pt's stdout - let meekInitLines = await getInitLines(); + const meekInitLines = await getInitLines(); // tokenize our pt lines - let meekInitTokens = meekInitLines.map(line => { - let tokens = line.split(" "); + const meekInitTokens = meekInitLines.map(line => { + const 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(" "); @@ -251,9 +216,9 @@ class MeekTransport { }
// convert proxy type to strings used by protocol-proxy-servce - meekProxyType = proxyType === "socks5" ? "socks" : "socks4"; - meekProxyAddr = addr; - meekProxyPort = port; + this.proxyType = proxyType === "socks5" ? "socks" : "socks4"; + this.proxyAddress = addr; + this.proxyPort = port;
break; } @@ -278,49 +243,47 @@ class MeekTransport { } }
- this._meekClientProcess = meekClientProcess; // register callback to cleanup on process exit - this._meekClientProcess.wait().then(exitObj => { - this._meekClientProcess = null; + this.#meekClientProcess.wait().then(exitObj => { + this.#meekClientProcess = null; this.uninit(); });
- this._meekProxyType = meekProxyType; - this._meekProxyAddress = meekProxyAddr; - this._meekProxyPort = meekProxyPort; - // socks5 - if (meekProxyType === "socks") { + if (this.proxyType === "socks") { if (meekClientEscapedArgs.length <= 255) { - this._meekProxyUsername = meekClientEscapedArgs; - this._meekProxyPassword = "\x00"; + this.proxyUsername = meekClientEscapedArgs; + this.proxyPassword = "\x00"; } else { - this._meekProxyUsername = meekClientEscapedArgs.substring(0, 255); - this._meekProxyPassword = meekClientEscapedArgs.substring(255); + this.proxyUsername = meekClientEscapedArgs.substring(0, 255); + this.proxyPassword = meekClientEscapedArgs.substring(255); } // socks4 } else { - this._meekProxyUsername = meekClientEscapedArgs; - this._meekProxyPassword = undefined; + this.proxyUsername = meekClientEscapedArgs; + this.proxyPassword = undefined; }
- this._inited = true; + this.#inited = true; } catch (ex) { - onException(); + if (this.#meekClientProcess) { + this.#meekClientProcess.kill(); + this.#meekClientProcess = null; + } 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; + this.#inited = false; + + await this.#meekClientProcess?.kill(); + this.#meekClientProcess = null; + this.proxyType = null; + this.proxyAddress = null; + this.proxyPort = 0; + this.proxyUsername = null; + this.proxyPassword = null; } }
@@ -328,21 +291,25 @@ class MeekTransport { // Callback object with a cached promise for the returned Moat data // class MoatResponseListener { + #response = ""; + #responsePromise; + #resolve; + #reject; constructor() { - this._response = ""; + 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; + this.#responsePromise = new Promise((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; }); }
// callers wait on this for final response response() { - return this._responsePromise; + return this.#responsePromise; }
// noop @@ -352,16 +319,17 @@ class MoatResponseListener { onStopRequest(request, status) { try { if (!Components.isSuccessCode(status)) { - const errorMessage = TorLauncherUtil.getLocalizedStringForError(status); - this._reject(new Error(errorMessage)); + const errorMessage = + lazy.TorLauncherUtil.getLocalizedStringForError(status); + this.#reject(new Error(errorMessage)); } if (request.responseStatus != 200) { - this._reject(new Error(request.responseStatusText)); + this.#reject(new Error(request.responseStatusText)); } } catch (err) { - this._reject(err); + this.#reject(err); } - this._resolve(this._response); + this.#resolve(this.#response); }
// read response data @@ -370,30 +338,32 @@ class MoatResponseListener { "@mozilla.org/scriptableinputstream;1" ].createInstance(Ci.nsIScriptableInputStream); scriptableStream.init(stream); - this._response += scriptableStream.read(length); + this.#response += scriptableStream.read(length); } }
class InternetTestResponseListener { + #promise; + #resolve; + #reject; constructor() { - this._promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; + this.#promise = new Promise((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; }); }
// callers wait on this for final response get status() { - return this._promise; + return this.#promise; }
onStartRequest(request) {}
// resolve or reject our Promise onStopRequest(request, status) { - let statuses = {}; try { - statuses = { + const statuses = { components: status, successful: Components.isSuccessCode(status), }; @@ -408,56 +378,51 @@ class InternetTestResponseListener { err ); } + this.#resolve(statuses); } catch (err) { - this._reject(err); + this.#reject(err); } - this._resolve(statuses); }
onDataAvailable(request, stream, offset, length) { - // We do not care of the actual data, as long as we have a successful + // We do not care of the actual data, as long as we have a successful // connection } }
// constructs the json objects and sends the request over moat -class MoatRPC { - constructor() { - this._meekTransport = null; - this._inited = false; - } +export class MoatRPC { + #inited = false; + #meekTransport = null;
get inited() { - return this._inited; + return this.#inited; }
async init() { - if (this._inited) { + if (this.#inited) { throw new Error("MoatRPC: Already initialized"); }
let meekTransport = new MeekTransport(); await meekTransport.init(); - this._meekTransport = meekTransport; - this._inited = true; + this.#meekTransport = meekTransport; + this.#inited = true; }
async uninit() { - await this._meekTransport?.uninit(); - this._meekTransport = null; - this._inited = false; + await this.#meekTransport?.uninit(); + this.#meekTransport = null; + this.#inited = false; }
- _makeHttpHandler(uriString) { - if (!this._inited) { + #makeHttpHandler(uriString) { + 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 { proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword } = + this.#meekTransport;
const proxyPS = Cc[ "@mozilla.org/network/protocol-proxy-service;1" @@ -511,11 +476,11 @@ class MoatRPC { return ch; }
- async _makeRequest(procedure, args) { + async #makeRequest(procedure, args) { const procedureURIString = `${Services.prefs.getStringPref( TorLauncherPrefs.moat_service )}/${procedure}`; - const ch = this._makeHttpHandler(procedureURIString); + const ch = this.#makeHttpHandler(procedureURIString);
// Arrange for the POST data to be sent. const argsJson = JSON.stringify(args); @@ -544,7 +509,7 @@ class MoatRPC { const uri = `${Services.prefs.getStringPref( TorLauncherPrefs.moat_service )}/circumvention/countries`; - const ch = this._makeHttpHandler(uri); + const ch = this.#makeHttpHandler(uri); ch.requestMethod = "HEAD";
const listener = new InternetTestResponseListener(); @@ -582,7 +547,7 @@ class MoatRPC { }, ], }; - const response = await this._makeRequest("fetch", args); + const response = await this.#makeRequest("fetch", args); if ("errors" in response) { const code = response.errors[0].code; const detail = response.errors[0].detail; @@ -623,7 +588,7 @@ class MoatRPC { }, ], }; - const response = await this._makeRequest("check", args); + const response = await this.#makeRequest("check", args); if ("errors" in response) { const code = response.errors[0].code; const detail = response.errors[0].detail; @@ -642,7 +607,7 @@ class MoatRPC {
// Convert received settings object to format used by TorSettings module // In the event of error, just return null - _fixupSettings(settings) { + #fixupSettings(settings) { try { let retval = TorSettings.defaultSettings(); if ("bridges" in settings) { @@ -691,11 +656,11 @@ class MoatRPC { // 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) { + #fixupSettingsList(settingsList) { try { let retval = []; for (let settings of settingsList) { - settings = this._fixupSettings(settings); + settings = this.#fixupSettings(settings); if (settings != null) { retval.push(settings); } @@ -724,7 +689,7 @@ class MoatRPC { transports: transports ? transports : [], country, }; - const response = await this._makeRequest("circumvention/settings", args); + const response = await this.#makeRequest("circumvention/settings", args); let settings = {}; if ("errors" in response) { const code = response.errors[0].code; @@ -739,7 +704,7 @@ class MoatRPC {
throw new Error(`MoatRPC: ${detail} (${code})`); } else if ("settings" in response) { - settings.settings = this._fixupSettingsList(response.settings); + settings.settings = this.#fixupSettingsList(response.settings); } if ("country" in response) { settings.country = response.country; @@ -753,7 +718,7 @@ class MoatRPC { // for async circumvention_countries() { const args = {}; - return this._makeRequest("circumvention/countries", args); + return this.#makeRequest("circumvention/countries", args); }
// Request a copy of the builtin bridges, takes the following parameters: @@ -766,7 +731,7 @@ class MoatRPC { const args = { transports: transports ? transports : [], }; - const response = await this._makeRequest("circumvention/builtin", args); + const response = await this.#makeRequest("circumvention/builtin", args); if ("errors" in response) { const code = response.errors[0].code; const detail = response.errors[0].detail; @@ -791,13 +756,13 @@ class MoatRPC { const args = { transports: transports ? transports : [], }; - const response = await this._makeRequest("circumvention/defaults", args); + const response = await this.#makeRequest("circumvention/defaults", args); if ("errors" in response) { const code = response.errors[0].code; const detail = response.errors[0].detail; throw new Error(`MoatRPC: ${detail} (${code})`); } else if ("settings" in response) { - return this._fixupSettingsList(response.settings); + return this.#fixupSettingsList(response.settings); } return []; }
===================================== browser/modules/TorConnect.jsm → browser/modules/TorConnect.sys.mjs ===================================== @@ -1,36 +1,32 @@ -"use strict"; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-var EXPORTED_SYMBOLS = [ - "InternetStatus", - "TorConnect", - "TorConnectTopics", - "TorConnectState", -]; +import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const lazy = {};
-const { setTimeout, clearTimeout } = ChromeUtils.import( - "resource://gre/modules/Timer.jsm" -); +ChromeUtils.defineESModuleGetters(lazy, { + MoatRPC: "resource:///modules/Moat.sys.mjs", + TorBootstrapRequest: "resource://gre/modules/TorBootstrapRequest.sys.mjs", + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", +});
-const { BrowserWindowTracker } = ChromeUtils.import( +// TODO: Should we move this to the about:torconnect actor? +ChromeUtils.defineModuleGetter( + lazy, + "BrowserWindowTracker", "resource:///modules/BrowserWindowTracker.jsm" );
-const { TorMonitorService } = ChromeUtils.import( - "resource://gre/modules/TorMonitorService.jsm" -); -const { TorBootstrapRequest } = ChromeUtils.import( - "resource://gre/modules/TorBootstrapRequest.jsm" -); - -const { TorSettings, TorSettingsTopics, TorBuiltinBridgeTypes } = - ChromeUtils.import("resource:///modules/TorSettings.jsm"); +import { + TorSettings, + TorSettingsTopics, + TorBuiltinBridgeTypes, +} from "resource:///modules/TorSettings.sys.mjs";
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
-const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm"); - const TorTopics = Object.freeze({ LogHasWarnOrErr: "TorLogHasWarnOrErr", ProcessExited: "TorProcessExited", @@ -46,7 +42,7 @@ const TorConnectPrefs = Object.freeze({ allow_internet_test: "torbrowser.bootstrap.allow_internet_test", });
-const TorConnectState = Object.freeze({ +export const TorConnectState = Object.freeze({ /* Our initial state */ Initial: "Initial", /* In-between initial boot and bootstrapping, users can change tor network settings during this state */ @@ -156,7 +152,7 @@ const TorConnectStateTransitions = Object.freeze( );
/* Topics Notified by the TorConnect module */ -const TorConnectTopics = Object.freeze({ +export const TorConnectTopics = Object.freeze({ StateChange: "torconnect:state-change", BootstrapProgress: "torconnect:bootstrap-progress", BootstrapComplete: "torconnect:bootstrap-complete", @@ -238,7 +234,7 @@ const debug_sleep = async ms => { }); };
-const InternetStatus = Object.freeze({ +export const InternetStatus = Object.freeze({ Unknown: -1, Offline: 0, Online: 1, @@ -302,7 +298,7 @@ class InternetTest { // waiting both for the bootstrap, and for the Internet test. // However, managing Moat with async/await is much easier as it avoids a // callback hell, and it makes extra explicit that we are uniniting it. - const mrpc = new MoatRPC(); + const mrpc = new lazy.MoatRPC(); let status = null; let error = null; try { @@ -340,7 +336,7 @@ class InternetTest { } }
-const TorConnect = (() => { +export const TorConnect = (() => { let retval = { _state: TorConnectState.Initial, _bootstrapProgress: 0, @@ -459,7 +455,7 @@ const TorConnect = (() => { return; }
- const tbr = new TorBootstrapRequest(); + const tbr = new lazy.TorBootstrapRequest(); const internetTest = new InternetTest(); let cancelled = false;
@@ -604,7 +600,7 @@ const TorConnect = (() => {
// lookup user's potential censorship circumvention settings from Moat service try { - this.mrpc = new MoatRPC(); + this.mrpc = new lazy.MoatRPC(); await this.mrpc.init();
if (this.transitioning) { @@ -678,7 +674,7 @@ const TorConnect = (() => { await TorSettings.applySettings();
// build out our bootstrap request - const tbr = new TorBootstrapRequest(); + const tbr = new lazy.TorBootstrapRequest(); tbr.onbootstrapstatus = (progress, status) => { TorConnect._updateBootstrapStatus(progress, status); }; @@ -915,7 +911,7 @@ const TorConnect = (() => { * @type {boolean} */ get enabled() { - return TorMonitorService.ownsTorDaemon; + return lazy.TorProviderBuilder.build().ownsTorDaemon; },
get shouldShowTorConnect() { @@ -1053,7 +1049,7 @@ const TorConnect = (() => { Further external commands and helper methods */ openTorPreferences() { - const win = BrowserWindowTracker.getTopWindow(); + const win = lazy.BrowserWindowTracker.getTopWindow(); win.switchToTabHavingURI("about:preferences#connection", true); },
@@ -1073,7 +1069,7 @@ const TorConnect = (() => { * begin AutoBootstrapping, if possible. */ openTorConnect(options) { - const win = BrowserWindowTracker.getTopWindow(); + const win = lazy.BrowserWindowTracker.getTopWindow(); win.switchToTabHavingURI("about:torconnect", true, { ignoreQueryString: true, }); @@ -1094,7 +1090,7 @@ const TorConnect = (() => { },
viewTorLogs() { - const win = BrowserWindowTracker.getTopWindow(); + const win = lazy.BrowserWindowTracker.getTopWindow(); win.switchToTabHavingURI("about:preferences#connection-viewlogs", true); },
@@ -1104,7 +1100,7 @@ const TorConnect = (() => { if (this._countryCodes.length) { return this._countryCodes; } - const mrpc = new MoatRPC(); + const mrpc = new lazy.MoatRPC(); try { await mrpc.init(); this._countryCodes = await mrpc.circumvention_countries();
===================================== browser/modules/TorSettings.jsm → browser/modules/TorSettings.sys.mjs ===================================== @@ -1,36 +1,22 @@ -"use strict"; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-var EXPORTED_SYMBOLS = [ - "TorSettings", - "TorSettingsTopics", - "TorSettingsData", - "TorBridgeSource", - "TorBuiltinBridgeTypes", - "TorProxyType", -]; +const lazy = {};
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - -const { TorMonitorService } = ChromeUtils.import( - "resource://gre/modules/TorMonitorService.jsm" -); -const { TorProtocolService } = ChromeUtils.import( - "resource://gre/modules/TorProtocolService.jsm" -); - -/* tor-launcher observer topics */ -const TorTopics = Object.freeze({ - ProcessIsReady: "TorProcessIsReady", +ChromeUtils.defineESModuleGetters(lazy, { + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", + TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs", });
/* TorSettings observer topics */ -const TorSettingsTopics = Object.freeze({ +export const TorSettingsTopics = Object.freeze({ Ready: "torsettings:ready", SettingChanged: "torsettings:setting-changed", });
/* TorSettings observer data (for SettingChanged topic) */ -const TorSettingsData = Object.freeze({ +export const TorSettingsData = Object.freeze({ QuickStartEnabled: "torsettings:quickstart_enabled", });
@@ -98,21 +84,21 @@ const TorConfigKeys = Object.freeze({ clientTransportPlugin: "ClientTransportPlugin", });
-const TorBridgeSource = Object.freeze({ +export const TorBridgeSource = Object.freeze({ Invalid: -1, BuiltIn: 0, BridgeDB: 1, UserProvided: 2, });
-const TorProxyType = Object.freeze({ +export const TorProxyType = Object.freeze({ Invalid: -1, Socks4: 0, Socks5: 1, HTTPS: 2, });
-const TorBuiltinBridgeTypes = Object.freeze( +export const TorBuiltinBridgeTypes = Object.freeze( (() => { const bridgeListBranch = Services.prefs.getBranch( TorLauncherPrefs.default_bridge @@ -254,7 +240,7 @@ const arrayCopy = function (array) {
/* TorSettings module */
-const TorSettings = (() => { +export const TorSettings = (() => { const self = { _settings: null,
@@ -288,7 +274,8 @@ const TorSettings = (() => {
/* load or init our settings, and register observers */ init() { - if (TorMonitorService.ownsTorDaemon) { + const provider = lazy.TorProviderBuilder.build(); + if (provider.ownsTorDaemon) { // if the settings branch exists, load settings from prefs if (Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)) { this.loadFromPrefs(); @@ -296,9 +283,9 @@ const TorSettings = (() => { // otherwise load defaults this._settings = this.defaultSettings(); } - Services.obs.addObserver(this, TorTopics.ProcessIsReady); + Services.obs.addObserver(this, lazy.TorProviderTopics.ProcessIsReady);
- if (TorMonitorService.isRunning) { + if (provider.isRunning) { this.handleProcessReady(); } } @@ -309,8 +296,11 @@ const TorSettings = (() => { console.log(`TorSettings: Observed ${topic}`);
switch (topic) { - case TorTopics.ProcessIsReady: - Services.obs.removeObserver(this, TorTopics.ProcessIsReady); + case lazy.TorProviderTopics.ProcessIsReady: + Services.obs.removeObserver( + this, + lazy.TorProviderTopics.ProcessIsReady + ); await this.handleProcessReady(); break; } @@ -569,7 +559,7 @@ const TorSettings = (() => { }
/* Push to Tor */ - await TorProtocolService.writeSettings(settingsMap); + await lazy.TorProviderBuilder.build().writeSettings(settingsMap);
return this; },
===================================== browser/modules/moz.build ===================================== @@ -123,7 +123,7 @@ XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] EXTRA_JS_MODULES += [ "AboutNewTab.jsm", "AsyncTabSwitcher.jsm", - "BridgeDB.jsm", + "BridgeDB.sys.mjs", "BrowserUIUtils.jsm", "BrowserUsageTelemetry.jsm", "BrowserWindowTracker.jsm", @@ -135,7 +135,7 @@ EXTRA_JS_MODULES += [ "FeatureCallout.sys.mjs", "HomePage.jsm", "LaterRun.jsm", - 'Moat.jsm', + "Moat.sys.mjs", "NewTabPagePreloading.jsm", "OpenInTabsUtils.jsm", "PageActions.jsm", @@ -149,8 +149,8 @@ EXTRA_JS_MODULES += [ "SitePermissions.sys.mjs", "TabsList.jsm", "TabUnloader.jsm", - "TorConnect.jsm", - "TorSettings.jsm", + "TorConnect.sys.mjs", + "TorSettings.sys.mjs", "TorStrings.jsm", "TransientPrefs.jsm", "URILoadingHelper.sys.mjs",
===================================== toolkit/components/tor-launcher/TorBootstrapRequest.sys.mjs ===================================== @@ -1,6 +1,6 @@ import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
-import { TorProtocolService } from "resource://gre/modules/TorProtocolService.sys.mjs"; +import { TorProviderBuilder } from "resource://gre/modules/TorProviderBuilder.sys.mjs"; import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
/* tor-launcher observer topics */ @@ -13,19 +13,23 @@ export const TorTopics = Object.freeze({ // modeled after XMLHttpRequest // nicely encapsulates the observer register/unregister logic export class TorBootstrapRequest { + // number of ms to wait before we abandon the bootstrap attempt + // a value of 0 implies we never wait + timeout = 0; + + // callbacks for bootstrap process status updates + onbootstrapstatus = (progress, status) => {}; + onbootstrapcomplete = () => {}; + onbootstraperror = (message, details) => {}; + + // internal resolve() method for bootstrap + #bootstrapPromiseResolve = null; + #bootstrapPromise = null; + #timeoutID = null; + #provider = null; + 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; + this.#provider = TorProviderBuilder.build(); }
observe(subject, topic, data) { @@ -41,15 +45,16 @@ export class TorBootstrapRequest { if (this.onbootstrapcomplete) { this.onbootstrapcomplete(); } - this._bootstrapPromiseResolve(true); - clearTimeout(this._timeoutID); + this.#bootstrapPromiseResolve(true); + clearTimeout(this.#timeoutID); + this.#timeoutID = null; }
break; } case TorTopics.BootstrapError: { console.info("TorBootstrapRequest: observerd TorBootstrapError", obj); - this._stop(obj?.message, obj?.details); + this.#stop(obj?.message, obj?.details); break; } } @@ -57,12 +62,12 @@ export class TorBootstrapRequest {
// resolves 'true' if bootstrap succeeds, false otherwise bootstrap() { - if (this._bootstrapPromise) { - return this._bootstrapPromise; + if (this.#bootstrapPromise) { + return this.#bootstrapPromise; }
- this._bootstrapPromise = new Promise((resolve, reject) => { - this._bootstrapPromiseResolve = resolve; + this.#bootstrapPromise = new Promise((resolve, reject) => { + this.#bootstrapPromiseResolve = resolve;
// register ourselves to listen for bootstrap events Services.obs.addObserver(this, TorTopics.BootstrapStatus); @@ -70,10 +75,10 @@ export class TorBootstrapRequest {
// optionally cancel bootstrap after a given timeout if (this.timeout > 0) { - this._timeoutID = setTimeout(async () => { - this._timeoutID = null; + this.#timeoutID = setTimeout(async () => { + this.#timeoutID = null; // TODO: Translate, if really used - await this._stop( + await this.#stop( "Tor Bootstrap process timed out", `Bootstrap attempt abandoned after waiting ${this.timeout} ms` ); @@ -81,38 +86,45 @@ export class TorBootstrapRequest { }
// wait for bootstrapping to begin and maybe handle error - TorProtocolService.connect().catch(err => { - this._stop(err.message, ""); + this.#provider.connect().catch(err => { + this.#stop(err.message, ""); }); }).finally(() => { // and remove ourselves once bootstrap is resolved Services.obs.removeObserver(this, TorTopics.BootstrapStatus); Services.obs.removeObserver(this, TorTopics.BootstrapError); - this._bootstrapPromise = null; + this.#bootstrapPromise = null; });
- return this._bootstrapPromise; + return this.#bootstrapPromise; }
async cancel() { - await this._stop(); + await this.#stop(); }
// Internal implementation. Do not use directly, but call cancel, instead. - async _stop(message, details) { + async #stop(message, details) { // first stop our bootstrap timeout before handling the error - if (this._timeoutID !== null) { - clearTimeout(this._timeoutID); - this._timeoutID = null; + if (this.#timeoutID !== null) { + clearTimeout(this.#timeoutID); + this.#timeoutID = null; }
- // stopBootstrap never throws - await TorProtocolService.stopBootstrap(); + try { + await this.#provider.stopBootstrap(); + } catch (e) { + console.error("Failed to stop the bootstrap.", e); + if (!message) { + message = e.message; + details = ""; + } + }
if (this.onbootstraperror && message) { this.onbootstraperror(message, details); }
- this._bootstrapPromiseResolve(false); + this.#bootstrapPromiseResolve(false); } }
===================================== toolkit/components/tor-launcher/TorControlPort.sys.mjs ===================================== @@ -274,6 +274,44 @@ class AsyncSocket { * the command */
+/** + * @typedef {object} Bridge + * @property {string} transport The transport of the bridge, or vanilla if not + * specified. + * @property {string} addr The IP address and port of the bridge + * @property {string} id The fingerprint of the bridge + * @property {string} args Optional arguments passed to the bridge + */ +/** + * @typedef {object} PTInfo The information about a pluggable transport + * @property {string[]} transports An array with all the transports supported by + * this configuration. + * @property {string} type Either socks4, socks5 or exec + * @property {string} [ip] The IP address of the proxy (only for socks4 and + * socks5) + * @property {integer} [port] The port of the proxy (only for socks4 and socks5) + * @property {string} [pathToBinary] Path to the binary that is run (only for + * exec) + * @property {string} [options] Optional options passed to the binary (only for + * exec) + */ +/** + * @typedef {object} OnionAuthKeyInfo + * @property {string} address The address of the onion service + * @property {string} typeAndKey Onion service key and type of key, as + * `type:base64-private-key` + * @property {string} Flags Additional flags, such as Permanent + */ +/** + * @callback EventFilterCallback + * @param {any} data Either a raw string, or already parsed data + * @returns {boolean} + */ +/** + * @callback EventCallback + * @param {any} data Either a raw string, or already parsed data + */ + class TorError extends Error { constructor(command, reply) { super(`${command} -> ${reply}`); @@ -584,319 +622,6 @@ class ControlSocket { } }
-// ## utils -// A namespace for utility functions -let utils = {}; - -// __utils.identity(x)__. -// Returns its argument unchanged. -utils.identity = function (x) { - return x; -}; - -// __utils.capture(string, regex)__. -// Takes a string and returns an array of capture items, where regex must have a single -// capturing group and use the suffix /.../g to specify a global search. -utils.capture = function (string, regex) { - let matches = []; - // Special trick to use string.replace for capturing multiple matches. - string.replace(regex, function (a, captured) { - matches.push(captured); - }); - return matches; -}; - -// __utils.extractor(regex)__. -// Returns a function that takes a string and returns an array of regex matches. The -// regex must use the suffix /.../g to specify a global search. -utils.extractor = function (regex) { - return function (text) { - return utils.capture(text, regex); - }; -}; - -// __utils.splitLines(string)__. -// Splits a string into an array of strings, each corresponding to a line. -utils.splitLines = function (string) { - return string.split(/\r?\n/); -}; - -// __utils.splitAtSpaces(string)__. -// Splits a string into chunks between spaces. Does not split at spaces -// inside pairs of quotation marks. -utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g); - -// __utils.splitAtFirst(string, regex)__. -// Splits a string at the first instance of regex match. If no match is -// found, returns the whole string. -utils.splitAtFirst = function (string, regex) { - let match = string.match(regex); - return match - ? [ - string.substring(0, match.index), - string.substring(match.index + match[0].length), - ] - : string; -}; - -// __utils.splitAtEquals(string)__. -// Splits a string into chunks between equals. Does not split at equals -// inside pairs of quotation marks. -utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g); - -// __utils.mergeObjects(arrayOfObjects)__. -// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object. -// Pure function. -utils.mergeObjects = function (arrayOfObjects) { - let result = {}; - for (let obj of arrayOfObjects) { - for (let key in obj) { - result[key] = obj[key]; - } - } - return result; -}; - -// __utils.listMapData(parameterString, listNames)__. -// Takes a list of parameters separated by spaces, of which the first several are -// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames -// to the unnamed parameters, and combine them in a map with the named parameters. -// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH` -// -// utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH", -// ["streamID", "event", "circuitID", "IP"]) -// // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0", -// // "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}" -utils.listMapData = function (parameterString, listNames) { - // Split out the space-delimited parameters. - let parameters = utils.splitAtSpaces(parameterString), - dataMap = {}; - // Assign listNames to the first n = listNames.length parameters. - for (let i = 0; i < listNames.length; ++i) { - dataMap[listNames[i]] = parameters[i]; - } - // Read key-value pairs and copy these to the dataMap. - for (let i = listNames.length; i < parameters.length; ++i) { - let [key, value] = utils.splitAtEquals(parameters[i]); - if (key && value) { - dataMap[key] = value; - } - } - return dataMap; -}; - -// ## info -// A namespace for functions related to tor's GETINFO and GETCONF command. -let info = {}; - -// __info.keyValueStringsFromMessage(messageText)__. -// Takes a message (text) response to GETINFO or GETCONF and provides -// a series of key-value strings, which are either multiline (with a `250+` prefix): -// -// 250+config/defaults= -// AccountingMax "0 bytes" -// AllowDotExit "0" -// . -// -// or single-line (with a `250-` or `250 ` prefix): -// -// 250-version=0.2.6.0-alpha-dev (git-b408125288ad6943) -info.keyValueStringsFromMessage = utils.extractor( - /^(250+[\s\S]+?^.|250[- ].+?)$/gim -); - -// __info.applyPerLine(transformFunction)__. -// Returns a function that splits text into lines, -// and applies transformFunction to each line. -info.applyPerLine = function (transformFunction) { - return function (text) { - return utils.splitLines(text.trim()).map(transformFunction); - }; -}; - -// __info.routerStatusParser(valueString)__. -// Parses a router status entry as, described in -// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt -// (search for "router status entry") -info.routerStatusParser = function (valueString) { - let lines = utils.splitLines(valueString), - objects = []; - for (let line of lines) { - // Drop first character and grab data following it. - let myData = line.substring(2), - // Accumulate more maps with data, depending on the first character in the line. - dataFun = { - r: data => - utils.listMapData(data, [ - "nickname", - "identity", - "digest", - "publicationDate", - "publicationTime", - "IP", - "ORPort", - "DirPort", - ]), - a: data => ({ IPv6: data }), - s: data => ({ statusFlags: utils.splitAtSpaces(data) }), - v: data => ({ version: data }), - w: data => utils.listMapData(data, []), - p: data => ({ portList: data.split(",") }), - }[line.charAt(0)]; - if (dataFun !== undefined) { - objects.push(dataFun(myData)); - } - } - return utils.mergeObjects(objects); -}; - -// __info.circuitStatusParser(line)__. -// Parse the output of a circuit status line. -info.circuitStatusParser = function (line) { - let data = utils.listMapData(line, ["id", "status", "circuit"]), - circuit = data.circuit; - // Parse out the individual circuit IDs and names. - if (circuit) { - data.circuit = circuit.split(",").map(function (x) { - return x.split(/~|=/); - }); - } - return data; -}; - -// __info.streamStatusParser(line)__. -// Parse the output of a stream status line. -info.streamStatusParser = function (text) { - return utils.listMapData(text, [ - "StreamID", - "StreamStatus", - "CircuitID", - "Target", - ]); -}; - -// TODO: fix this parsing logic to handle bridgeLine correctly -// fingerprint/id is an optional parameter -// __info.bridgeParser(bridgeLine)__. -// Takes a single line from a `getconf bridge` result and returns -// a map containing the bridge's type, address, and ID. -info.bridgeParser = function (bridgeLine) { - let result = {}, - tokens = bridgeLine.split(/\s+/); - // First check if we have a "vanilla" bridge: - if (tokens[0].match(/^\d+.\d+.\d+.\d+/)) { - result.type = "vanilla"; - [result.address, result.ID] = tokens; - // Several bridge types have a similar format: - } else { - result.type = tokens[0]; - if ( - [ - "flashproxy", - "fte", - "meek", - "meek_lite", - "obfs3", - "obfs4", - "scramblesuit", - "snowflake", - ].includes(result.type) - ) { - [result.address, result.ID] = tokens.slice(1); - } - } - return result.type ? result : null; -}; - -// __info.parsers__. -// A map of GETINFO and GETCONF keys to parsing function, which convert -// result strings to JavaScript data. -info.parsers = { - "ns/id/": info.routerStatusParser, - "ip-to-country/": utils.identity, - "circuit-status": info.applyPerLine(info.circuitStatusParser), - bridge: info.bridgeParser, - // Currently unused parsers: - // "ns/name/" : info.routerStatusParser, - // "stream-status" : info.applyPerLine(info.streamStatusParser), - // "version" : utils.identity, - // "config-file" : utils.identity, -}; - -// __info.getParser(key)__. -// Takes a key and determines the parser function that should be used to -// convert its corresponding valueString to JavaScript data. -info.getParser = function (key) { - return ( - info.parsers[key] || - info.parsers[key.substring(0, key.lastIndexOf("/") + 1)] - ); -}; - -// __info.stringToValue(string)__. -// Converts a key-value string as from GETINFO or GETCONF to a value. -info.stringToValue = function (string) { - // key should look something like `250+circuit-status=` or `250-circuit-status=...` - // or `250 circuit-status=...` - let matchForKey = string.match(/^250[ +-](.+?)=/), - key = matchForKey ? matchForKey[1] : null; - if (key === null) { - return null; - } - // matchResult finds a single-line result for `250-` or `250 `, - // or a multi-line one for `250+`. - let matchResult = - string.match(/^250[ -].+?=(.*)$/) || - string.match(/^250+.+?=([\s\S]*?)^.$/m), - // Retrieve the captured group (the text of the value in the key-value pair) - valueString = matchResult ? matchResult[1] : null, - // Get the parser function for the key found. - parse = info.getParser(key.toLowerCase()); - if (parse === undefined) { - throw new Error("No parser found for '" + key + "'"); - } - // Return value produced by the parser. - return parse(valueString); -}; - -/** - * @typedef {object} Bridge - * @property {string} transport The transport of the bridge, or vanilla if not - * specified. - * @property {string} addr The IP address and port of the bridge - * @property {string} id The fingerprint of the bridge - * @property {string} args Optional arguments passed to the bridge - */ -/** - * @typedef {object} PTInfo The information about a pluggable transport - * @property {string[]} transports An array with all the transports supported by - * this configuration. - * @property {string} type Either socks4, socks5 or exec - * @property {string} [ip] The IP address of the proxy (only for socks4 and - * socks5) - * @property {integer} [port] The port of the proxy (only for socks4 and socks5) - * @property {string} [pathToBinary] Path to the binary that is run (only for - * exec) - * @property {string} [options] Optional options passed to the binary (only for - * exec) - */ -/** - * @typedef {object} OnionAuthKeyInfo - * @property {string} address The address of the onion service - * @property {string} typeAndKey Onion service key and type of key, as - * `type:base64-private-key` - * @property {string} Flags Additional flags, such as Permanent - */ -/** - * @callback EventFilterCallback - * @param {any} data Either a raw string, or already parsed data - * @returns {boolean} - */ -/** - * @callback EventCallback - * @param {any} data Either a raw string, or already parsed data - */ - class TorController { /** * The control socket @@ -905,16 +630,6 @@ class TorController { */ #socket;
- /** - * A map of EVENT keys to parsing functions, which convert result strings to - * JavaScript data. - */ - #eventParsers = { - stream: info.streamStatusParser, - // Currently unused: - // "circ" : info.circuitStatusParser, - }; - /** * Builds a new TorController. * @@ -981,18 +696,6 @@ class TorController { await this.#sendCommandSimple(`authenticate ${password || ""}`); }
- /** - * Sends a GETINFO for a single key. - * - * @param {string} key The key to get value for - * @returns {any} The return value depends on the requested key - */ - async getInfo(key) { - this.#expectString(key, "key"); - const response = await this.sendCommand(`getinfo ${key}`); - return this.#getMultipleResponseValues(response)[0]; - } - /** * Sends a GETINFO for a single key. * control-spec.txt says "one ReplyLine is sent for each requested value", so, @@ -1054,9 +757,7 @@ class TorController { const addresses = [v4[5]]; // a address:port // dir-spec.txt also states only the first one should be taken - // TODO: The consumers do not care about the port or the square brackets - // either. Remove them when integrating this function with the rest - const v6 = reply.match(/^a\s+([[0-9a-fA-F:]+]:[0-9]{1,5})$/m); + const v6 = reply.match(/^a\s+[([0-9a-fA-F:]+)]:\d{1,5}$/m); if (v6) { addresses.push(v6[1]); } @@ -1091,23 +792,6 @@ class TorController {
// Configuration
- /** - * Sends a GETCONF for a single key. - * GETCONF with a single argument returns results with one or more lines that - * look like `250[- ]key=value`. - * Any GETCONF lines that contain a single keyword only are currently dropped. - * So we can use similar parsing to that for getInfo. - * - * @param {string} key The key to get value for - * @returns {any} A parsed config value (it depends if a parser is known) - */ - async getConf(key) { - this.#expectString(key, "key"); - return this.#getMultipleResponseValues( - await this.sendCommand(`getconf ${key}`) - ); - } - /** * Sends a GETCONF for a single key. * The function could be easily generalized to get multiple keys at once, but @@ -1264,12 +948,14 @@ class TorController { // TODO: Change the consumer and make the fields more consistent with what // we get (e.g., separate key and type, and use a boolen for permanent). const info = { - hsAddress: match.groups.HSAddress, - typeAndKey: `${match.groups.KeyType}:${match.groups.PrivateKeyBlob}`, + address: match.groups.HSAddress, + keyType: match.groups.KeyType, + keyBlob: match.groups.PrivateKeyBlob, + flags: [], }; const maybeFlags = match.groups.other?.match(/Flags=(\S+)/); if (maybeFlags) { - info.Flags = maybeFlags[1]; + info.flags = maybeFlags[1].split(","); } return info; }); @@ -1369,28 +1055,12 @@ class TorController { * first. * * @param {string} type The event type to catch - * @param {EventFilterCallback?} filter An optional callback to filter - * events for which the callback will be called. If null, all events will be - * passed. * @param {EventCallback} callback The callback that will handle the event - * @param {boolean} raw Tell whether to ignore the data parser, even if - * supported */ - watchEvent(type, filter, callback, raw = false) { + watchEvent(type, callback) { this.#expectString(type, "type"); const start = `650 ${type}`; - this.#socket.addNotificationCallback(new RegExp(`^${start}`), message => { - // Remove also the initial text - const dataText = message.substring(start.length + 1); - const parser = this.#eventParsers[type.toLowerCase()]; - const data = dataText && parser ? parser(dataText) : null; - // FIXME: This is the original code, but we risk of not filtering on the - // data, if we ask for raw data (which we always do at the moment, but we - // do not use a filter either...) - if (filter === null || filter(data)) { - callback(data && !raw ? data : message); - } - }); + this.#socket.addNotificationCallback(new RegExp(`^${start}`), callback); }
// Other helpers @@ -1453,19 +1123,6 @@ class TorController { ) ); } - - /** - * Process multiple responses to a GETINFO or GETCONF request. - * - * @param {string} message The message to process - * @returns {object[]} The keys depend on the message - */ - #getMultipleResponseValues(message) { - return info - .keyValueStringsFromMessage(message) - .map(info.stringToValue) - .filter(x => x); - } }
const controlPortInfo = {};
===================================== toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs ===================================== @@ -12,6 +12,11 @@ import {
const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, { + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", + TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs", +}); + XPCOMUtils.defineLazyServiceGetters(lazy, { ProtocolProxyService: [ "@mozilla.org/network/protocol-proxy-service;1", @@ -19,11 +24,6 @@ XPCOMUtils.defineLazyServiceGetters(lazy, { ], });
-ChromeUtils.defineESModuleGetters(lazy, { - TorMonitorTopics: "resource://gre/modules/TorMonitorService.sys.mjs", - TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs", -}); - const logger = new ConsoleAPI({ prefix: "TorDomainIsolator", maxLogLevel: "warn", @@ -143,7 +143,7 @@ class TorDomainIsolatorImpl {
Services.prefs.addObserver(NON_TOR_PROXY_PREF, this); Services.obs.addObserver(this, NEW_IDENTITY_TOPIC); - Services.obs.addObserver(this, lazy.TorMonitorTopics.StreamSucceeded); + Services.obs.addObserver(this, lazy.TorProviderTopics.StreamSucceeded);
this.#cleanupIntervalId = setInterval( this.#clearKnownCircuits.bind(this), @@ -158,7 +158,7 @@ class TorDomainIsolatorImpl { uninit() { Services.prefs.removeObserver(NON_TOR_PROXY_PREF, this); Services.obs.removeObserver(this, NEW_IDENTITY_TOPIC); - Services.obs.removeObserver(this, lazy.TorMonitorTopics.StreamSucceeded); + Services.obs.removeObserver(this, lazy.TorProviderTopics.StreamSucceeded); clearInterval(this.#cleanupIntervalId); this.#cleanupIntervalId = null; this.clearIsolation(); @@ -257,12 +257,12 @@ class TorDomainIsolatorImpl { ); this.clearIsolation(); try { - await lazy.TorProtocolService.newnym(); + await lazy.TorProviderBuilder.build().newnym(); } catch (e) { logger.error("Could not send the newnym command", e); // TODO: What UX to use here? See tor-browser#41708 } - } else if (topic === lazy.TorMonitorTopics.StreamSucceeded) { + } else if (topic === lazy.TorProviderTopics.StreamSucceeded) { const { username, password, circuit } = subject.wrappedJSObject; this.#updateCircuit(username, password, circuit); } @@ -553,7 +553,7 @@ class TorDomainIsolatorImpl {
data = await Promise.all( circuit.map(fingerprint => - lazy.TorProtocolService.getNodeInfo(fingerprint) + lazy.TorProviderBuilder.build().getNodeInfo(fingerprint) ) ); this.#knownCircuits.set(id, data);
===================================== toolkit/components/tor-launcher/TorMonitorService.sys.mjs deleted ===================================== @@ -1,42 +0,0 @@ -// Copyright (c) 2022, The Tor Project, Inc. - -import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs"; - -const lazy = {}; -ChromeUtils.defineESModuleGetters(lazy, { - TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs", -}); - -export const TorMonitorTopics = Object.freeze({ - BridgeChanged: TorProviderTopics.BridgeChanged, - StreamSucceeded: TorProviderTopics.StreamSucceeded, -}); - -/** - * This service monitors an existing Tor instance, or starts one, if needed, and - * then starts monitoring it. - * - * This is the service which should be queried to know information about the - * status of the bootstrap, the logs, etc... - */ -export const TorMonitorService = { - get currentBridge() { - return lazy.TorProtocolService.currentBridge; - }, - - get ownsTorDaemon() { - return lazy.TorProtocolService.ownsTorDaemon; - }, - - get isRunning() { - return lazy.TorProtocolService.isRunning; - }, - - get isBootstrapDone() { - return lazy.TorProtocolService.isBootstrapDone; - }, - - getLog() { - return lazy.TorProtocolService.getLog(); - }, -};
===================================== toolkit/components/tor-launcher/TorParsers.sys.mjs ===================================== @@ -269,11 +269,14 @@ export const TorParsers = Object.freeze({ },
parseBridgeLine(line) { + if (!line) { + return null; + } const re = /\s*(?:(?<transport>\S+)\s+)?(?<addr>[0-9a-fA-F.[]:]+:\d{1,5})(?:\s+(?<id>[0-9a-fA-F]{40}))?(?:\s+(?<args>.+))?/; const match = re.exec(line); if (!match) { - throw new Error("Invalid bridge line."); + throw new Error(`Invalid bridge line: ${line}.`); } const bridge = match.groups; if (!bridge.transport) {
===================================== toolkit/components/tor-launcher/TorProtocolService.sys.mjs → toolkit/components/tor-launcher/TorProvider.sys.mjs ===================================== @@ -1,4 +1,6 @@ -// Copyright (c) 2021, The Tor Project, Inc. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs"; @@ -11,7 +13,6 @@ import { import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs";
const lazy = {}; - ChromeUtils.defineESModuleGetters(lazy, { controller: "resource://gre/modules/TorControlPort.sys.mjs", configureControlPortModule: "resource://gre/modules/TorControlPort.sys.mjs", @@ -21,7 +22,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
const logger = new ConsoleAPI({ maxLogLevel: "warn", - prefix: "TorProtocolService", + maxLogLevelPref: "browser.tor_provider.log_level", + prefix: "TorProvider", });
/** @@ -70,7 +72,7 @@ const ControlConnTimings = Object.freeze({ * It can start a new tor instance, or connect to an existing one. * In the former case, it also takes its ownership by default. */ -class TorProvider { +export class TorProvider { #inited = false;
// Maintain a map of tor settings set by Tor Browser so that we don't @@ -85,7 +87,6 @@ class TorProvider { #SOCKSPortInfo = null; // An object that contains ipcFile, host, port.
#controlConnection = null; // This is cached and reused. - #connectionQueue = [];
// Public methods
@@ -123,39 +124,34 @@ class TorProvider { // takes a Map containing tor settings // throws on error async writeSettings(aSettingsObj) { + const entries = + aSettingsObj instanceof Map + ? Array.from(aSettingsObj.entries()) + : Object.entries(aSettingsObj); // only write settings that have changed - const newSettings = Array.from(aSettingsObj).filter(([setting, value]) => { - // make sure we have valid data here - this.#assertValidSetting(setting, value); - + const newSettings = entries.filter(([setting, value]) => { if (!this.#settingsCache.has(setting)) { // no cached setting, so write return true; }
const cachedValue = this.#settingsCache.get(setting); - if (value === cachedValue) { - return false; - } else if (Array.isArray(value) && Array.isArray(cachedValue)) { - // compare arrays member-wise - if (value.length !== cachedValue.length) { - return true; - } - for (let i = 0; i < value.length; i++) { - if (value[i] !== cachedValue[i]) { - return true; - } - } - return false; + // Arrays are the only special case for which === could fail. + // The other values we accept (strings, booleans, numbers, null and + // undefined) work correctly with ===. + if (Array.isArray(value) && Array.isArray(cachedValue)) { + return ( + value.length !== cachedValue.length || + value.some((val, idx) => val !== cachedValue[idx]) + ); } - // some other different values - return true; + return value !== cachedValue; });
// only write if new setting to save if (newSettings.length) { - const settingsObject = Object.fromEntries(newSettings); - await this.setConfWithReply(settingsObject); + const conn = await this.#getConnection(); + await conn.setConf(Object.fromEntries(newSettings));
// save settings to cache after successfully writing to Tor for (const [setting, value] of newSettings) { @@ -164,23 +160,15 @@ class TorProvider { } }
- async readStringArraySetting(aSetting) { - const value = await this.#readSetting(aSetting); - this.#settingsCache.set(aSetting, value); - return value; - } - // writes current tor settings to disk async flushSettings() { - await this.sendCommand("SAVECONF"); + const conn = await this.#getConnection(); + await conn.flushSettings(); }
async connect() { - const kTorConfKeyDisableNetwork = "DisableNetwork"; - const settings = {}; - settings[kTorConfKeyDisableNetwork] = false; - await this.setConfWithReply(settings); - await this.sendCommand("SAVECONF"); + const conn = await this.#getConnection(); + await conn.setNetworkEnabled(true); this.clearBootstrapError(); this.retrieveBootstrapStatus(); } @@ -188,12 +176,8 @@ class TorProvider { async stopBootstrap() { // Tell tor to disable use of the network; this should stop the bootstrap // process. - try { - const settings = { DisableNetwork: true }; - await this.setConfWithReply(settings); - } catch (e) { - logger.error("Error stopping bootstrap", e); - } + const conn = await this.#getConnection(); + await conn.setNetworkEnabled(false); // We are not interested in waiting for this, nor in **catching its error**, // so we do not await this. We just want to be notified when the bootstrap // status is actually updated through observers. @@ -201,28 +185,31 @@ class TorProvider { }
async newnym() { - return this.sendCommand("SIGNAL NEWNYM"); + const conn = await this.#getConnection(); + await conn.newnym(); }
// Ask tor which ports it is listening to for SOCKS connections. // At the moment this is used only in TorCheckService. async getSocksListeners() { - const cmd = "GETINFO"; - const keyword = "net/listeners/socks"; - const response = await this.sendCommand(cmd, keyword); - return TorParsers.parseReply(cmd, keyword, response); + const conn = await this.#getConnection(); + return conn.getSocksListeners(); }
async getBridges() { + const conn = await this.#getConnection(); // Ideally, we would not need this function, because we should be the one // setting them with TorSettings. However, TorSettings is not notified of // change of settings. So, asking tor directly with the control connection // is the most reliable way of getting the configured bridges, at the // moment. Also, we are using this for the circuit display, which should // work also when we are not configuring the tor daemon, but just using it. - return this.#withConnection(conn => { - return conn.getConf("bridge"); - }); + return conn.getBridges(); + } + + async getPluggableTransports() { + const conn = await this.#getConnection(); + return conn.getPluggableTransports(); }
/** @@ -232,68 +219,55 @@ class TorProvider { * @returns {Promise<NodeData>} */ async getNodeInfo(id) { - return this.#withConnection(async conn => { - const node = { - fingerprint: id, - ipAddrs: [], - bridgeType: null, - regionCode: null, - }; - const bridge = (await conn.getConf("bridge"))?.find( - foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase() - ); - const addrRe = /^[?([^]]+)]?:\d+$/; - if (bridge) { - node.bridgeType = bridge.type ?? ""; - // Attempt to get an IP address from bridge address string. - const ip = bridge.address.match(addrRe)?.[1]; - if (ip && !ip.startsWith("0.")) { - node.ipAddrs.push(ip); - } - } else { - // Either dealing with a relay, or a bridge whose fingerprint is not - // saved in torrc. - const info = await conn.getInfo(`ns/id/${id}`); - if (info.IP && !info.IP.startsWith("0.")) { - node.ipAddrs.push(info.IP); - } - const ip6 = info.IPv6?.match(addrRe)?.[1]; - if (ip6) { - node.ipAddrs.push(ip6); - } + const conn = await this.#getConnection(); + const node = { + fingerprint: id, + ipAddrs: [], + bridgeType: null, + regionCode: null, + }; + const bridge = (await conn.getBridges())?.find( + foundBridge => foundBridge.id?.toUpperCase() === id.toUpperCase() + ); + if (bridge) { + node.bridgeType = bridge.transport ?? ""; + // Attempt to get an IP address from bridge address string. + const ip = bridge.addr.match(/^[?([^]]+)]?:\d+$/)?.[1]; + if (ip && !ip.startsWith("0.")) { + node.ipAddrs.push(ip); } - if (node.ipAddrs.length) { - // Get the country code for the node's IP address. - let regionCode; - try { - // Expect a 2-letter ISO3166-1 code, which should also be a valid - // BCP47 Region subtag. - regionCode = await conn.getInfo("ip-to-country/" + node.ipAddrs[0]); - } catch {} + } else { + node.ipAddrs = await conn.getNodeAddresses(id); + } + if (node.ipAddrs.length) { + // Get the country code for the node's IP address. + try { + // Expect a 2-letter ISO3166-1 code, which should also be a valid + // BCP47 Region subtag. + const regionCode = await conn.getIPCountry(node.ipAddrs[0]); if (regionCode && regionCode !== "??") { node.regionCode = regionCode.toUpperCase(); } + } catch (e) { + logger.warn(`Cannot get a country for IP ${node.ipAddrs[0]}`, e); } - return node; - }); + } + return node; }
- async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) { - return this.#withConnection(conn => { - return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent); - }); + async onionAuthAdd(address, b64PrivateKey, isPermanent) { + const conn = await this.#getConnection(); + return conn.onionAuthAdd(address, b64PrivateKey, isPermanent); }
- async onionAuthRemove(hsAddress) { - return this.#withConnection(conn => { - return conn.onionAuthRemove(hsAddress); - }); + async onionAuthRemove(address) { + const conn = await this.#getConnection(); + return conn.onionAuthRemove(address); }
async onionAuthViewKeys() { - return this.#withConnection(conn => { - return conn.onionAuthViewKeys(); - }); + const conn = await this.#getConnection(); + return conn.onionAuthViewKeys(); }
// TODO: transform the following 4 functions in getters. @@ -333,106 +307,6 @@ class TorProvider { return this.#SOCKSPortInfo; }
- // Public, but called only internally - - // Executes a command on the control port. - // Return a reply object or null if a fatal error occurs. - async sendCommand(cmd, args) { - const maxTimeout = 1000; - let leftConnAttempts = 5; - let timeout = 250; - let reply; - while (leftConnAttempts-- > 0) { - const response = await this.#trySend(cmd, args, leftConnAttempts === 0); - if (response.connected) { - reply = response.reply; - break; - } - // We failed to acquire the controller after multiple attempts. - // Try again after some time. - logger.warn( - "sendCommand: Acquiring control connection failed, trying again later.", - cmd, - args - ); - await new Promise(resolve => setTimeout(() => resolve(), timeout)); - timeout = Math.min(2 * timeout, maxTimeout); - } - - // We sent the command, but we still got an empty response. - // Something must be busted elsewhere. - if (!reply) { - throw new Error(`${cmd} sent an empty response`); - } - - // TODO: Move the parsing of the reply to the controller, because anyone - // calling sendCommand on it actually wants a parsed reply. - - reply = TorParsers.parseCommandResponse(reply); - if (!TorParsers.commandSucceeded(reply)) { - if (reply?.lineArray) { - throw new Error(reply.lineArray.join("\n")); - } - throw new Error(`${cmd} failed with code ${reply.statusCode}`); - } - - return reply; - } - - // Perform a SETCONF command. - // aSettingsObj should be a JavaScript object with keys (property values) - // that correspond to tor config. keys. The value associated with each - // key should be a simple string, a string array, or a Boolean value. - // If an associated value is undefined or null, a key with no value is - // passed in the SETCONF command. - // Throws in case of error, or returns a reply object. - async setConfWithReply(settings) { - if (!settings) { - throw new Error("Empty settings object"); - } - const args = Object.entries(settings) - .map(([key, val]) => { - if (val === undefined || val === null) { - return key; - } - const valType = typeof val; - let rv = `${key}=`; - if (valType === "boolean") { - rv += val ? "1" : "0"; - } else if (Array.isArray(val)) { - rv += val.map(TorParsers.escapeString).join(` ${key}=`); - } else if (valType === "string") { - rv += TorParsers.escapeString(val); - } else { - logger.error(`Got unsupported type for ${key}`, val); - throw new Error(`Unsupported type ${valType} (key ${key})`); - } - return rv; - }) - .filter(arg => arg); - if (!args.length) { - throw new Error("No settings to set"); - } - - await this.sendCommand("SETCONF", args.join(" ")); - } - - // Public, never called? - - 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; - } - - // Private - async #setSockets() { try { const isWindows = TorLauncherUtil.isWindows; @@ -511,167 +385,24 @@ class TorProvider { } }
- #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); - switch (typeof aValue) { - case "boolean": - case "string": - return; - case "object": - if (aValue === null) { - return; - } else if (Array.isArray(aValue)) { - for (const element of aValue) { - if (typeof element !== "string") { - throw new Error( - `Setting '${aSetting}' array contains value of invalid type '${typeof element}'` - ); - } - } - return; - } - // fall through - default: - throw new Error( - `Invalid object type received for setting '${aSetting}'` - ); - } - } - - // Perform a GETCONF command. - async #readSetting(aSetting) { - this.#assertValidSettingKey(aSetting); - - const cmd = "GETCONF"; - let reply = await this.sendCommand(cmd, aSetting); - return TorParsers.parseReply(cmd, aSetting, reply); - } - - 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 #readBoolSetting(aSetting) { - const value = this.#readStringSetting(aSetting); - switch (value) { - case "0": - return false; - case "1": - return true; - default: - throw new Error(`Expected boolean (1 or 0) but received '${value}'`); - } - } - - async #trySend(cmd, args, rethrow) { - let connected = false; - let reply; - let leftAttempts = 2; - while (leftAttempts-- > 0) { - let conn; - try { - conn = await this.#getConnection(); - } catch (e) { - logger.error("Cannot get a connection to the control port", e); - if (leftAttempts == 0 && rethrow) { - throw e; - } - } - if (!conn) { - continue; - } - // If we _ever_ got a connection, the caller should not try again - connected = true; - try { - reply = await conn.sendCommand(cmd + (args ? " " + args : "")); - if (reply) { - // Return for reuse. - this.#returnConnection(); - } else { - // Connection is bad. - logger.warn( - "sendCommand returned an empty response, taking the connection as broken and closing it." - ); - this.#closeConnection(); - } - } catch (e) { - logger.error(`Cannot send the command ${cmd}`, e); - this.#closeConnection(); - if (leftAttempts == 0 && rethrow) { - throw e; - } - } - } - return { connected, reply }; - } - - // Opens an authenticated connection, sets it to this.#controlConnection, and - // return it. async #getConnection() { - if (!this.#controlConnection) { + if (!this.#controlConnection?.isOpen) { this.#controlConnection = await lazy.controller(); } - if (this.#controlConnection.inUse) { - await new Promise((resolve, reject) => - this.#connectionQueue.push({ resolve, reject }) - ); - } else { - this.#controlConnection.inUse = true; - } return this.#controlConnection; }
- #returnConnection() { - if (this.#connectionQueue.length) { - this.#connectionQueue.shift().resolve(); - } else { - this.#controlConnection.inUse = false; - } - } - - async #withConnection(func) { - // TODO: Make more robust? - const conn = await this.#getConnection(); - try { - return await func(conn); - } finally { - this.#returnConnection(); - } - } - - // If aConn is omitted, the cached connection is closed. #closeConnection() { if (this.#controlConnection) { logger.info("Closing the control connection"); this.#controlConnection.close(); this.#controlConnection = null; } - for (const promise of this.#connectionQueue) { - promise.reject("Connection closed"); - } - this.#connectionQueue = []; }
async #reconnect() { this.#closeConnection(); - const conn = await this.#getConnection(); - logger.debug("Reconnected to the control port."); - this.#returnConnection(conn); + await this.#getConnection(); }
async #readAuthenticationCookie(aPath) { @@ -777,8 +508,9 @@ class TorProvider { if (this.ownsTorDaemon) { // When we own the tor daemon, we listen to more events, that are used // for about:torconnect or for showing the logs in the settings page. - this._eventHandlers.set("STATUS_CLIENT", (_eventType, lines) => - this._processBootstrapStatus(lines[0], false) + this._eventHandlers.set( + "STATUS_CLIENT", + this._processStatusClient.bind(this) ); this._eventHandlers.set("NOTICE", this._processLog.bind(this)); this._eventHandlers.set("WARN", this._processLog.bind(this)); @@ -809,23 +541,10 @@ class TorProvider { throw new Error("Event monitor connection not available"); }
- // TODO: Unify with TorProtocolService.sendCommand and put everything in the - // reviewed torbutton replacement. - const cmd = "GETINFO"; - const key = "status/bootstrap-phase"; - let reply = await this._connection.sendCommand(`${cmd} ${key}`); - - // A typical reply looks like: - // 250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done" - // 250 OK - reply = TorParsers.parseCommandResponse(reply); - if (!TorParsers.commandSucceeded(reply)) { - throw new Error(`${cmd} failed`); - } - reply = TorParsers.parseReply(cmd, key, reply); - if (reply.length) { - this._processBootstrapStatus(reply[0], true); - } + this._processBootstrapStatus( + await this._connection.getBootstrapPhase(), + true + ); }
// Returns captured log message as a text string (one message per line). @@ -1058,37 +777,32 @@ class TorProvider { _monitorEvent(type, callback) { logger.info(`Watching events of type ${type}.`); let replyObj = {}; - this._connection.watchEvent( - type, - null, - line => { - if (!line) { - return; - } - logger.debug("Event response: ", line); - const isComplete = TorParsers.parseReplyLine(line, replyObj); - if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) { - return; - } - const reply = replyObj; - replyObj = {}; - if (reply.statusCode !== TorStatuses.EventNotification) { - logger.error("Unexpected event status code:", reply.statusCode); - return; - } - if (!reply.lineArray[0].startsWith(`${type} `)) { - logger.error("Wrong format for the first line:", reply.lineArray[0]); - return; - } - reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1); - try { - callback(type, reply.lineArray); - } catch (e) { - logger.error("Exception while handling an event", reply, e); - } - }, - true - ); + this._connection.watchEvent(type, line => { + if (!line) { + return; + } + logger.debug("Event response: ", line); + const isComplete = TorParsers.parseReplyLine(line, replyObj); + if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) { + return; + } + const reply = replyObj; + replyObj = {}; + if (reply.statusCode !== TorStatuses.EventNotification) { + logger.error("Unexpected event status code:", reply.statusCode); + return; + } + if (!reply.lineArray[0].startsWith(`${type} `)) { + logger.error("Wrong format for the first line:", reply.lineArray[0]); + return; + } + reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1); + try { + callback(type, reply.lineArray); + } catch (e) { + logger.error("Exception while handling an event", reply, e); + } + }); }
_processLog(type, lines) { @@ -1116,15 +830,12 @@ class TorProvider { // to TorBootstrapStatus observers. // If aSuppressErrors is true, errors are ignored. This is used when we // are handling the response to a "GETINFO status/bootstrap-phase" command. - _processBootstrapStatus(aStatusMsg, aSuppressErrors) { - const statusObj = TorParsers.parseBootstrapStatus(aStatusMsg); - if (!statusObj) { - return; - } - + _processBootstrapStatus(statusObj, suppressErrors) { // Notify observers - statusObj.wrappedJSObject = statusObj; - Services.obs.notifyObservers(statusObj, "TorBootstrapStatus"); + Services.obs.notifyObservers( + { wrappedJSObject: statusObj }, + "TorBootstrapStatus" + );
if (statusObj.PROGRESS === 100) { this._isBootstrapDone = true; @@ -1141,7 +852,7 @@ class TorProvider { if ( statusObj.TYPE === "WARN" && statusObj.RECOMMENDATION !== "ignore" && - !aSuppressErrors + !suppressErrors ) { this._notifyBootstrapError(statusObj); } @@ -1184,6 +895,15 @@ class TorProvider { } }
+ _processStatusClient(_type, lines) { + const statusObj = TorParsers.parseBootstrapStatus(lines[0]); + if (!statusObj) { + // No `BOOTSTRAP` in the line + return; + } + this._processBootstrapStatus(statusObj, false); + } + async _processCircEvent(_type, lines) { const builtEvent = /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec( @@ -1295,7 +1015,3 @@ class TorProvider { this.clearBootstrapError(); } } - -// TODO: Stop defining TorProtocolService, make the builder instance the -// TorProvider. -export const TorProtocolService = new TorProvider();
===================================== toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs ===================================== @@ -4,7 +4,7 @@
const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs", + TorProvider: "resource://gre/modules/TorProvider.sys.mjs", });
export const TorProviderTopics = Object.freeze({ @@ -19,16 +19,25 @@ export const TorProviderTopics = Object.freeze({ });
export class TorProviderBuilder { + static #provider = null; + static async init() { - await lazy.TorProtocolService.init(); + const provider = new lazy.TorProvider(); + await provider.init(); + // Assign it only when initialization succeeds. + TorProviderBuilder.#provider = provider; }
static uninit() { - lazy.TorProtocolService.uninit(); + TorProviderBuilder.#provider.uninit(); + TorProviderBuilder.#provider = null; }
// TODO: Switch to an async build? static build() { - return lazy.TorProtocolService; + if (!TorProviderBuilder.#provider) { + throw new Error("TorProviderBuilder has not been initialized yet."); + } + return TorProviderBuilder.#provider; } }
===================================== toolkit/components/tor-launcher/TorStartupService.sys.mjs ===================================== @@ -3,22 +3,13 @@ const lazy = {}; // We will use the modules only when the profile is loaded, so prefer lazy // loading ChromeUtils.defineESModuleGetters(lazy, { + TorConnect: "resource:///modules/TorConnect.sys.mjs", TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs", TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs", TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", + TorSettings: "resource:///modules/TorSettings.sys.mjs", });
-ChromeUtils.defineModuleGetter( - lazy, - "TorConnect", - "resource:///modules/TorConnect.jsm" -); -ChromeUtils.defineModuleGetter( - lazy, - "TorSettings", - "resource:///modules/TorSettings.jsm" -); - /* Browser observer topis */ const BrowserTopics = Object.freeze({ ProfileAfterChange: "profile-after-change",
===================================== toolkit/components/tor-launcher/moz.build ===================================== @@ -3,10 +3,9 @@ EXTRA_JS_MODULES += [ "TorControlPort.sys.mjs", "TorDomainIsolator.sys.mjs", "TorLauncherUtil.sys.mjs", - "TorMonitorService.sys.mjs", "TorParsers.sys.mjs", "TorProcess.sys.mjs", - "TorProtocolService.sys.mjs", + "TorProvider.sys.mjs", "TorProviderBuilder.sys.mjs", "TorStartupService.sys.mjs", ]
===================================== toolkit/mozapps/update/UpdateService.sys.mjs ===================================== @@ -23,18 +23,13 @@ ChromeUtils.defineESModuleGetters(lazy, { AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", CertUtils: "resource://gre/modules/CertUtils.sys.mjs", DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs", ctypes: "resource://gre/modules/ctypes.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", });
-ChromeUtils.defineModuleGetter( - lazy, - "TorMonitorService", - "resource://gre/modules/TorMonitorService.jsm" -); - XPCOMUtils.defineLazyServiceGetter( lazy, "AUS", @@ -394,10 +389,11 @@ XPCOMUtils.defineLazyGetter( );
function _shouldRegisterBootstrapObserver(errorCode) { + const provider = lazy.TorProviderBuilder.build(); return ( errorCode == PROXY_SERVER_CONNECTION_REFUSED && - !lazy.TorMonitorService.isBootstrapDone && - lazy.TorMonitorService.ownsTorDaemon + !provider.isBootstrapDone && + provider.ownsTorDaemon ); }
@@ -5833,10 +5829,7 @@ Downloader.prototype = { // we choose to compute these hashes. hash = hash.finish(false); digest = Array.from(hash, (c, i) => - hash - .charCodeAt(i) - .toString(16) - .padStart(2, "0") + hash.charCodeAt(i).toString(16).padStart(2, "0") ).join(""); } catch (e) { LOG(
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/ab8a15b...