commit 7def59d740ef0a4807e20c47928d3cca42b41c18
parent f939b6229b83e880e4c638318f173acf7d49b0cc
Author: Richard Pospesel <richard@torproject.org>
Date: Fri, 6 Aug 2021 16:39:03 +0200
TB 40597: Implement TorSettings module
- migrated in-page settings read/write implementation from about:preferences#tor
to the TorSettings module
- TorSettings initially loads settings from the tor daemon, and saves them to
firefox prefs
- TorSettings notifies observers when a setting has changed; currently only
QuickStart notification is implemented for parity with previous preference
notify logic in about:torconnect and about:preferences#tor
- about:preferences#tor, and about:torconnect now read and write settings
thorugh the TorSettings module
- all tor settings live in the torbrowser.settings.* preference branch
- removed unused pref modify permission for about:torconnect content page from
AsyncPrefs.jsm
Bug 40645: Migrate Moat APIs to Moat.jsm module
Diffstat:
13 files changed, 4366 insertions(+), 0 deletions(-)
diff --git a/.prettierignore b/.prettierignore
@@ -1799,3 +1799,5 @@ xpcom/idl-parser/xpidl/fixtures/xpctest.d.json
browser/app/profile/001-base-profile.js
browser/app/profile/000-tor-browser.js
mobile/android/app/000-tor-browser-android.js
+toolkit/content/pt_config.json
+toolkit/content/moat_countries_dev_build.json
diff --git a/browser/app/profile/000-tor-browser.js b/browser/app/profile/000-tor-browser.js
@@ -134,3 +134,6 @@ pref("extensions.torlauncher.moat_service", "https://bridges.torproject.org/moat
pref("browser.tor_provider.log_level", "Warn");
pref("browser.tor_provider.cp_log_level", "Warn");
pref("lox.log_level", "Warn");
+pref("torbrowser.bootstrap.log_level", "Info");
+pref("browser.torsettings.log_level", "Warn");
+pref("browser.torMoat.loglevel", "Warn");
diff --git a/eslint-ignores.config.mjs b/eslint-ignores.config.mjs
@@ -316,4 +316,6 @@ export default [
"browser/app/profile/001-base-profile.js",
"browser/app/profile/000-tor-browser.js",
"mobile/android/app/000-tor-browser-android.js",
+ "toolkit/content/pt_config.json",
+ "toolkit/content/moat_contries_dev_build.json",
];
diff --git a/toolkit/components/tor-launcher/TorStartupService.sys.mjs b/toolkit/components/tor-launcher/TorStartupService.sys.mjs
@@ -3,8 +3,10 @@ const lazy = {};
// We will use the modules only when the profile is loaded, so prefer lazy
// loading
ChromeUtils.defineESModuleGetters(lazy, {
+ TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
});
/* Browser observer topis */
@@ -33,11 +35,15 @@ export class TorStartupService {
#init() {
Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
+ lazy.TorSettings.init();
+
// Theoretically, build() is expected to await the initialization of the
// provider, and anything needing the Tor Provider should be able to just
// await on TorProviderBuilder.build().
lazy.TorProviderBuilder.init();
+ lazy.TorConnect.init();
+
gInited = true;
}
@@ -46,5 +52,6 @@ export class TorStartupService {
lazy.TorProviderBuilder.uninit();
lazy.TorLauncherUtil.cleanupTempDirectories();
+ lazy.TorSettings.uninit();
}
}
diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn
@@ -216,3 +216,10 @@ toolkit.jar:
content/global/vendor/react-transition-group.js (vendor/react/react-transition-group.js)
content/global/vendor/redux.js (vendor/react/redux.js)
content/global/vendor/react-redux.js (vendor/react/react-redux.js)
+
+ # The pt_config.json content should be replaced in the omni.ja in
+ # tor-browser-build. See tor-browser#42343.
+ content/global/pt_config.json (pt_config.json)
+ # The moat_countries.json content should be replaced in the omni.ja in
+ # tor-browser-build. See tor-browser#43463.
+ content/global/moat_countries.json (moat_countries_dev_build.json)
diff --git a/toolkit/content/moat_countries_dev_build.json b/toolkit/content/moat_countries_dev_build.json
@@ -0,0 +1,7 @@
+[
+ {
+ "_comment1": "Used for dev build, replaced for release builds in tor-browser-build.",
+ "_comment2": "List is taken from tpo/anti-censorship/rdsys-admin 810fb24b:conf/circumvention.json and filtered with `jq -c keys`."
+ },
+ "by","cn","eg","hk","ir","mm","ru","tm"
+]
diff --git a/toolkit/content/pt_config.json b/toolkit/content/pt_config.json
@@ -0,0 +1,27 @@
+{
+ "_comment": "Used for dev build, replaced for release builds in tor-browser-build. This file is copied from tor-browser-build cf5a3f623f94cf5bc093db61fe64f9b38b03fce0:projects/tor-expert-bundle/pt_config.json",
+ "recommendedDefault" : "obfs4",
+ "pluggableTransports" : {
+ "lyrebird" : "ClientTransportPlugin meek_lite,obfs2,obfs3,obfs4,scramblesuit,webtunnel exec ${pt_path}lyrebird${pt_extension}",
+ "snowflake": "ClientTransportPlugin snowflake exec ${pt_path}lyrebird${pt_extension}",
+ "conjure" : "ClientTransportPlugin conjure exec ${pt_path}conjure-client${pt_extension} -registerURL https://registration.refraction.network/api"
+ },
+ "bridges" : {
+ "meek" : [
+ "meek_lite 192.0.2.20:80 url=https://1603026938.rsc.cdn77.org front=www.phpmyadmin.net utls=HelloRandomizedALPN"
+ ],
+ "obfs4" : [
+ "obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0",
+ "obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0",
+ "obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0",
+ "obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0",
+ "obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0",
+ "obfs4 212.83.43.95:443 BFE712113A72899AD685764B211FACD30FF52C31 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=1",
+ "obfs4 212.83.43.74:443 39562501228A4D5E27FCA4C0C81A01EE23AE3EE4 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=1"
+ ],
+ "snowflake" : [
+ "snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://1098762253.rsc.cdn77.org/ fronts=app.datapacket.com,www.datapacket.com ice=stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn",
+ "snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=app.datapacket.com,www.datapacket.com ice=stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn"
+ ]
+ }
+}
diff --git a/toolkit/modules/BridgeDB.sys.mjs b/toolkit/modules/BridgeDB.sys.mjs
@@ -0,0 +1,95 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ MoatRPC: "resource://gre/modules/Moat.sys.mjs",
+});
+
+export var BridgeDB = {
+ _moatRPC: null,
+ _challenge: null,
+ _image: null,
+ _bridges: null,
+ /**
+ * A collection of controllers to abort any ongoing Moat requests if the
+ * dialog is closed.
+ *
+ * NOTE: We do not expect this set to ever contain more than one instance.
+ * However the public API has no assurances to prevent multiple calls.
+ *
+ * @type {Set<AbortController>}
+ */
+ _moatAbortControllers: new Set(),
+
+ get currentCaptchaImage() {
+ return this._image;
+ },
+
+ get currentBridges() {
+ return this._bridges;
+ },
+
+ async submitCaptchaGuess(solution) {
+ if (!this._moatRPC) {
+ this._moatRPC = new lazy.MoatRPC();
+ await this._moatRPC.init();
+ }
+
+ const abortController = new AbortController();
+ this._moatAbortControllers.add(abortController);
+ try {
+ this._bridges = await this._moatRPC.check(
+ "obfs4",
+ this._challenge,
+ solution,
+ abortController.signal
+ );
+ } finally {
+ this._moatAbortControllers.delete(abortController);
+ }
+ return this._bridges;
+ },
+
+ async requestNewCaptchaImage() {
+ try {
+ if (!this._moatRPC) {
+ this._moatRPC = new lazy.MoatRPC();
+ await this._moatRPC.init();
+ }
+
+ const abortController = new AbortController();
+ this._moatAbortControllers.add(abortController);
+ let response;
+ try {
+ response = await this._moatRPC.fetch(["obfs4"], abortController.signal);
+ } finally {
+ this._moatAbortControllers.delete(abortController);
+ }
+ if (response) {
+ // Not cancelled.
+ this._challenge = response.challenge;
+ this._image =
+ "data:image/jpeg;base64," + encodeURIComponent(response.image);
+ }
+ } catch (err) {
+ console.error("Could not request a captcha image", err);
+ }
+ return this._image;
+ },
+
+ close() {
+ // Abort any ongoing requests.
+ for (const controller of this._moatAbortControllers) {
+ controller.abort();
+ }
+ this._moatAbortControllers.clear();
+ this._moatRPC?.uninit();
+ this._moatRPC = null;
+ this._challenge = null;
+ this._image = null;
+ this._bridges = null;
+ },
+};
diff --git a/toolkit/modules/DomainFrontedRequests.sys.mjs b/toolkit/modules/DomainFrontedRequests.sys.mjs
@@ -0,0 +1,616 @@
+/* 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/. */
+
+const lazy = {};
+
+const log = console.createInstance({
+ maxLogLevel: "Warn",
+ prefix: "DomainFrontendRequests",
+});
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
+ Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
+ TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
+});
+
+/**
+ * The meek pluggable transport takes the reflector URL and front domain as
+ * proxy credentials, which can be prepared with this function.
+ *
+ * @param {string} proxyType The proxy type (socks for socks5 or socks4)
+ * @param {string} reflector The URL of the service hosted by the CDN
+ * @param {string} front The domain to use as a front
+ * @returns {string[]} An array containing [username, password]
+ */
+function makeMeekCredentials(proxyType, reflector, front) {
+ // Construct the per-connection arguments.
+ let meekClientEscapedArgs = "";
+
+ // Escape aValue per section 3.5 of the PT specification:
+ // First the "<Key>=<Value>" formatted arguments MUST be escaped,
+ // such that all backslash, equal sign, and semicolon characters
+ // are escaped with a backslash.
+ const escapeArgValue = aValue =>
+ aValue
+ ? aValue
+ .replaceAll("\\", "\\\\")
+ .replaceAll("=", "\\=")
+ .replaceAll(";", "\\;")
+ : "";
+
+ if (reflector) {
+ meekClientEscapedArgs += "url=";
+ meekClientEscapedArgs += escapeArgValue(reflector);
+ }
+
+ if (front) {
+ if (meekClientEscapedArgs.length) {
+ meekClientEscapedArgs += ";";
+ }
+ meekClientEscapedArgs += "front=";
+ meekClientEscapedArgs += escapeArgValue(front);
+ }
+
+ // socks5
+ if (proxyType === "socks") {
+ if (meekClientEscapedArgs.length <= 255) {
+ return [meekClientEscapedArgs, "\x00"];
+ }
+ return [
+ meekClientEscapedArgs.substring(0, 255),
+ meekClientEscapedArgs.substring(255),
+ ];
+ } else if (proxyType === "socks4") {
+ return [meekClientEscapedArgs, undefined];
+ }
+ throw new Error(`Unsupported proxy type ${proxyType}.`);
+}
+
+/**
+ * Subprocess-based implementation to launch and control a PT process.
+ */
+class MeekTransport {
+ // 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(reflector, front) {
+ // ensure we haven't already init'd
+ if (this.#inited) {
+ throw new Error("MeekTransport: Already initialized");
+ }
+
+ try {
+ // figure out which pluggable transport to use
+ const supportedTransports = ["meek", "meek_lite"];
+ const provider = await lazy.TorProviderBuilder.build();
+ const proxy = (await provider.getPluggableTransports()).find(
+ pt =>
+ pt.type === "exec" &&
+ supportedTransports.some(t => pt.transports.includes(t))
+ );
+ if (!proxy) {
+ throw new Error("No supported transport found.");
+ }
+
+ const meekTransport = proxy.transports.find(t =>
+ supportedTransports.includes(t)
+ );
+ // Convert meek client path to absolute path if necessary
+ 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;
+ }
+
+ // Setup env and start meek process
+ const ptStateDir = lazy.TorLauncherUtil.getTorFile("tordatadir", false);
+ ptStateDir.append("pt_state"); // Match what tor uses.
+
+ const envAdditions = {
+ TOR_PT_MANAGED_TRANSPORT_VER: "1",
+ TOR_PT_STATE_LOCATION: ptStateDir.path,
+ TOR_PT_EXIT_ON_STDIN_CLOSE: "1",
+ TOR_PT_CLIENT_TRANSPORTS: meekTransport,
+ };
+ if (lazy.TorSettings.proxy.enabled) {
+ envAdditions.TOR_PT_PROXY = lazy.TorSettings.proxyUri;
+ }
+
+ const opts = {
+ command: proxy.pathToBinary,
+ arguments: proxy.options.split(/s+/),
+ workdir: meekWorkDir.path,
+ environmentAppend: true,
+ environment: envAdditions,
+ stderr: "pipe",
+ };
+
+ // Launch meek client
+ this.#meekClientProcess = await lazy.Subprocess.call(opts);
+
+ // Callback chain for reading stderr
+ const stderrLogger = async () => {
+ while (this.#meekClientProcess) {
+ const errString = await this.#meekClientProcess.stderr.readString();
+ if (errString) {
+ log.error(`MeekTransport: stderr => ${errString}`);
+ }
+ }
+ };
+ stderrLogger();
+
+ // Read pt's stdout until terminal (CMETHODS DONE) is reached
+ // returns array of lines for parsing
+ const getInitLines = async (stdout = "") => {
+ stdout += await this.#meekClientProcess.stdout.readString();
+
+ // look for the final message
+ const CMETHODS_DONE = "CMETHODS DONE";
+ let endIndex = stdout.lastIndexOf(CMETHODS_DONE);
+ if (endIndex !== -1) {
+ endIndex += CMETHODS_DONE.length;
+ return stdout.substring(0, endIndex).split("\n");
+ }
+ return getInitLines(stdout);
+ };
+
+ // read our lines from pt's stdout
+ const meekInitLines = await getInitLines();
+ // tokenize our pt lines
+ const meekInitTokens = meekInitLines.map(line => {
+ const tokens = line.split(" ");
+ return {
+ keyword: tokens[0],
+ args: tokens.slice(1),
+ };
+ });
+
+ // parse our pt tokens
+ for (const { keyword, args } of meekInitTokens) {
+ const argsJoined = args.join(" ");
+ let keywordError = false;
+ switch (keyword) {
+ case "VERSION": {
+ if (args.length !== 1 || args[0] !== "1") {
+ keywordError = true;
+ }
+ break;
+ }
+ case "PROXY": {
+ if (args.length !== 1 || args[0] !== "DONE") {
+ keywordError = true;
+ }
+ break;
+ }
+ case "CMETHOD": {
+ if (args.length !== 3) {
+ keywordError = true;
+ break;
+ }
+ const transport = args[0];
+ const proxyType = args[1];
+ const addrPortString = args[2];
+ const addrPort = addrPortString.split(":");
+
+ if (transport !== meekTransport) {
+ throw new Error(
+ `MeekTransport: Expected ${meekTransport} but found ${transport}`
+ );
+ }
+ if (!["socks4", "socks4a", "socks5"].includes(proxyType)) {
+ throw new Error(
+ `MeekTransport: Invalid proxy type => ${proxyType}`
+ );
+ }
+ if (addrPort.length !== 2) {
+ throw new Error(
+ `MeekTransport: Invalid proxy address => ${addrPortString}`
+ );
+ }
+ const addr = addrPort[0];
+ const port = parseInt(addrPort[1]);
+ if (port < 1 || port > 65535) {
+ throw new Error(`MeekTransport: Invalid proxy port => ${port}`);
+ }
+
+ // convert proxy type to strings used by protocol-proxy-servce
+ this.proxyType = proxyType === "socks5" ? "socks" : "socks4";
+ this.proxyAddress = addr;
+ this.proxyPort = port;
+
+ break;
+ }
+ // terminal
+ case "CMETHODS": {
+ if (args.length !== 1 || args[0] !== "DONE") {
+ keywordError = true;
+ }
+ break;
+ }
+ // errors (all fall through):
+ case "VERSION-ERROR":
+ case "ENV-ERROR":
+ case "PROXY-ERROR":
+ case "CMETHOD-ERROR":
+ throw new Error(`MeekTransport: ${keyword} => '${argsJoined}'`);
+ }
+ if (keywordError) {
+ throw new Error(
+ `MeekTransport: Invalid ${keyword} keyword args => '${argsJoined}'`
+ );
+ }
+ }
+
+ // register callback to cleanup on process exit
+ this.#meekClientProcess.wait().then(() => {
+ this.#meekClientProcess = null;
+ this.uninit();
+ });
+ [this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
+ this.proxyType,
+ reflector,
+ front
+ );
+ this.#inited = true;
+ } catch (ex) {
+ if (this.#meekClientProcess) {
+ this.#meekClientProcess.kill();
+ this.#meekClientProcess = null;
+ }
+ throw ex;
+ }
+ }
+
+ async uninit() {
+ 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;
+ }
+}
+
+/**
+ * Android implementation of the Meek process.
+ *
+ * GeckoView does not provide the subprocess module, so we have to use the
+ * EventDispatcher, and have a Java handler start and stop the proxy process.
+ */
+class MeekTransportAndroid {
+ // 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;
+
+ /**
+ * An id for process this instance is linked to.
+ *
+ * Since we do not restrict the transport to be a singleton, we need a handle to
+ * identify the process we want to stop when the transport owner is done.
+ * We use a counter incremented on the Java side for now.
+ *
+ * This number must be a positive integer (i.e., 0 is an invalid handler).
+ *
+ * @type {number}
+ */
+ #id = 0;
+
+ async init(reflector, front) {
+ // ensure we haven't already init'd
+ if (this.#id) {
+ throw new Error("MeekTransport: Already initialized");
+ }
+ const details = await lazy.EventDispatcher.instance.sendRequestForResult({
+ type: "GeckoView:Tor:StartMeek",
+ });
+ this.#id = details.id;
+ this.proxyType = "socks";
+ this.proxyAddress = details.address;
+ this.proxyPort = details.port;
+ [this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
+ this.proxyType,
+ reflector,
+ front
+ );
+ }
+
+ async uninit() {
+ lazy.EventDispatcher.instance.sendRequest({
+ type: "GeckoView:Tor:StopMeek",
+ id: this.#id,
+ });
+ this.#id = 0;
+ this.proxyType = null;
+ this.proxyAddress = null;
+ this.proxyPort = 0;
+ this.proxyUsername = null;
+ this.proxyPassword = null;
+ }
+}
+
+/**
+ * Corresponds to a Network error with the request.
+ */
+export class DomainFrontRequestNetworkError extends Error {
+ constructor(request, statusCode) {
+ super(`Error fetching ${request.name}: ${statusCode}`);
+ this.name = "DomainFrontRequestNetworkError";
+ this.statusCode = statusCode;
+ }
+}
+
+/**
+ * Corresponds to a non-ok response from the server.
+ */
+export class DomainFrontRequestResponseError extends Error {
+ constructor(request) {
+ super(
+ `Error response from ${request.name} server: ${request.responseStatus}`
+ );
+ this.name = "DomainFrontRequestResponseError";
+ this.status = request.responseStatus;
+ this.statusText = request.responseStatusText;
+ }
+}
+
+/**
+ * Thrown when the caller cancels the request.
+ */
+export class DomainFrontRequestCancelledError extends Error {
+ constructor(url) {
+ super(`Cancelled request to ${url}`);
+ }
+}
+
+/**
+ * Callback object to promisify the XPCOM request.
+ */
+class ResponseListener {
+ #response = "";
+ #responsePromise;
+ #resolve;
+ #reject;
+ constructor() {
+ this.#response = "";
+ // we need this promise here because await nsIHttpChannel::asyncOpen does
+ // not return only once the request is complete, it seems to return
+ // after it begins, so we have to get the result from this listener object.
+ // This promise is only resolved once onStopRequest is called
+ this.#responsePromise = new Promise((resolve, reject) => {
+ this.#resolve = resolve;
+ this.#reject = reject;
+ });
+ }
+
+ /**
+ * A promise that resolves to the response body from the request.
+ *
+ * @type {Promise<string>}
+ */
+ get response() {
+ return this.#responsePromise;
+ }
+
+ // noop
+ onStartRequest() {}
+
+ // resolve or reject our Promise
+ onStopRequest(request, status) {
+ try {
+ if (!Components.isSuccessCode(status)) {
+ // Assume this is a network error.
+ this.#reject(new DomainFrontRequestNetworkError(request, status));
+ }
+ if (request.responseStatus !== 200) {
+ this.#reject(new DomainFrontRequestResponseError(request));
+ }
+ } catch (err) {
+ this.#reject(err);
+ }
+ this.#resolve(this.#response);
+ }
+
+ // read response data
+ onDataAvailable(request, stream, offset, length) {
+ const scriptableStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ scriptableStream.init(stream);
+ this.#response += scriptableStream.read(length);
+ }
+}
+
+/**
+ * Factory to create HTTP(S) requests over a domain fronted transport.
+ */
+export class DomainFrontRequestBuilder {
+ #inited = false;
+ #meekTransport = null;
+
+ get inited() {
+ return this.#inited;
+ }
+
+ async init(reflector, front) {
+ if (this.#inited) {
+ throw new Error("DomainFrontRequestBuilder: Already initialized");
+ }
+
+ const meekTransport =
+ Services.appinfo.OS === "Android"
+ ? new MeekTransportAndroid()
+ : new MeekTransport();
+ await meekTransport.init(reflector, front);
+ this.#meekTransport = meekTransport;
+ this.#inited = true;
+ }
+
+ async uninit() {
+ await this.#meekTransport?.uninit();
+ this.#meekTransport = null;
+ this.#inited = false;
+ }
+
+ buildHttpHandler(uriString) {
+ if (!this.#inited) {
+ throw new Error("DomainFrontRequestBuilder: Not initialized");
+ }
+
+ const { proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword } =
+ this.#meekTransport;
+
+ const proxyPS = Cc[
+ "@mozilla.org/network/protocol-proxy-service;1"
+ ].getService(Ci.nsIProtocolProxyService);
+ const flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+ const noTimeout = 0xffffffff; // UINT32_MAX
+ const proxyInfo = proxyPS.newProxyInfoWithAuth(
+ proxyType,
+ proxyAddress,
+ proxyPort,
+ proxyUsername,
+ proxyPassword,
+ undefined,
+ undefined,
+ flags,
+ noTimeout,
+ undefined
+ );
+
+ const uri = Services.io.newURI(uriString);
+ // There does not seem to be a way to directly create an nsILoadInfo from
+ // JavaScript, so we create a throw away non-proxied channel to get one.
+ const secFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
+ const loadInfo = Services.io.newChannelFromURI(
+ uri,
+ undefined,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ undefined,
+ secFlags,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ ).loadInfo;
+
+ const httpHandler = Services.io
+ .getProtocolHandler("http")
+ .QueryInterface(Ci.nsIHttpProtocolHandler);
+ const ch = httpHandler
+ .newProxiedChannel(uri, proxyInfo, 0, undefined, loadInfo)
+ .QueryInterface(Ci.nsIHttpChannel);
+
+ // remove all headers except for 'Host"
+ const headers = [];
+ ch.visitRequestHeaders({
+ visitHeader: key => {
+ if (key !== "Host") {
+ headers.push(key);
+ }
+ },
+ });
+ headers.forEach(key => ch.setRequestHeader(key, "", false));
+
+ return ch;
+ }
+
+ /**
+ * Make a request.
+ *
+ * @param {string} url The URL to request.
+ * @param {object} args The arguments to send to the procedure.
+ * @param {string} args.method The request method.
+ * @param {string} args.body The request body.
+ * @param {string} args.contentType The "Content-Type" header to set.
+ * @param {AbortSignal} [args.signal] An optional means of cancelling
+ * the request early. Will throw DomainFrontRequestCancelledError if
+ * aborted.
+ * @returns {Promise<string>} A promise that resolves to the response body.
+ */
+ buildRequest(url, args) {
+ // Pre-fetch the argument values from `args` so the caller cannot change the
+ // parameters mid-call.
+ const { body, method, contentType, signal } = args;
+ let cancel = null;
+ const promise = new Promise((resolve, reject) => {
+ if (signal?.aborted) {
+ // Unexpected, cancel immediately.
+ reject(new DomainFrontRequestCancelledError(url));
+ return;
+ }
+
+ let ch = null;
+
+ if (signal) {
+ cancel = () => {
+ // Reject prior to calling cancel, since we want to ignore any error
+ // responses from ResponseListener.
+ // NOTE: In principle we could let ResponseListener throw this error
+ // when it receives NS_ERROR_ABORT, but that would rely on mozilla
+ // never calling this error either.
+ reject(new DomainFrontRequestCancelledError(url));
+ ch?.cancel(Cr.NS_ERROR_ABORT);
+ };
+ signal.addEventListener("abort", cancel);
+ }
+
+ ch = this.buildHttpHandler(url);
+
+ const inStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ inStream.setByteStringData(body);
+ const upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
+ upChannel.setUploadStream(inStream, contentType, body.length);
+ ch.requestMethod = method;
+
+ // Make request
+ const listener = new ResponseListener();
+ ch.asyncOpen(listener);
+ listener.response.then(
+ body => {
+ resolve(body);
+ },
+ error => {
+ reject(error);
+ }
+ );
+ });
+ // Clean up. Do not return this `Promise.finally` since the caller should
+ // not depend on it.
+ // We pre-catch and suppress all errors for this `.finally` to stop the
+ // errors from being duplicated in the console log.
+ promise
+ .catch(() => {})
+ .finally(() => {
+ // Remove the callback for the AbortSignal so that it doesn't hold onto
+ // our channel reference if the caller continues to hold a reference to
+ // AbortSignal.
+ if (cancel) {
+ signal.removeEventListener("abort", cancel);
+ }
+ });
+ return promise;
+ }
+}
diff --git a/toolkit/modules/Moat.sys.mjs b/toolkit/modules/Moat.sys.mjs
@@ -0,0 +1,392 @@
+/* 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/. */
+
+const lazy = {};
+
+const log = console.createInstance({
+ prefix: "Moat",
+ maxLogLevelPref: "browser.torMoat.loglevel",
+});
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ DomainFrontRequestBuilder:
+ "resource://gre/modules/DomainFrontedRequests.sys.mjs",
+ DomainFrontRequestCancelledError:
+ "resource://gre/modules/DomainFrontedRequests.sys.mjs",
+ TorBridgeSource: "resource://gre/modules/TorSettings.sys.mjs",
+});
+
+const TorLauncherPrefs = Object.freeze({
+ bridgedb_front: "extensions.torlauncher.bridgedb_front",
+ bridgedb_reflector: "extensions.torlauncher.bridgedb_reflector",
+ moat_service: "extensions.torlauncher.moat_service",
+});
+
+/**
+ * @typedef {object} MoatBridges
+ *
+ * Bridge settings that can be passed to TorSettings.bridges.
+ *
+ * @property {number} source - The `TorBridgeSource` type.
+ * @property {string} [builtin_type] - The built-in bridge type.
+ * @property {string[]} [bridge_strings] - The bridge lines.
+ */
+
+/**
+ * @typedef {object} MoatSettings
+ *
+ * The settings returned by Moat.
+ *
+ * @property {MoatBridges[]} bridgesList - The list of bridges found.
+ * @property {string} [country] - The detected country (region).
+ */
+
+/**
+ * @typedef {object} CaptchaChallenge
+ *
+ * The details for a captcha challenge.
+ *
+ * @property {string} transport - The transport type selected by the Moat
+ * server.
+ * @property {string} image - A base64 encoded jpeg with the captcha to
+ * complete.
+ * @property {string} challenge - A nonce/cookie string associated with this
+ * request.
+ */
+
+/**
+ * Constructs JSON objects and sends requests over Moat.
+ * The documentation about the JSON schemas to use are available at
+ * https://gitlab.torproject.org/tpo/anti-censorship/rdsys/-/blob/main/doc/moat.md.
+ */
+export class MoatRPC {
+ #requestBuilder = null;
+
+ async init() {
+ if (this.#requestBuilder !== null) {
+ return;
+ }
+
+ const reflector = Services.prefs.getStringPref(
+ TorLauncherPrefs.bridgedb_reflector
+ );
+ const front = Services.prefs.getStringPref(TorLauncherPrefs.bridgedb_front);
+ this.#requestBuilder = new lazy.DomainFrontRequestBuilder();
+ try {
+ await this.#requestBuilder.init(reflector, front);
+ } catch (e) {
+ this.#requestBuilder = null;
+ throw e;
+ }
+ }
+
+ async uninit() {
+ await this.#requestBuilder?.uninit();
+ this.#requestBuilder = null;
+ }
+
+ /**
+ * @typedef {object} MoatResult
+ *
+ * The result of a Moat request.
+ *
+ * @property {any} response - The parsed JSON response from the Moat server,
+ * or `undefined` if the request was cancelled.
+ * @property {boolean} cancelled - Whether the request was cancelled.
+ */
+
+ /**
+ * Make a request to Moat.
+ *
+ * @param {string} procedure - The name of the procedure.
+ * @param {object} args - The arguments to pass in as a JSON string.
+ * @param {AbortSignal} [abortSignal] - An optional signal to be able to abort
+ * the request early.
+ * @returns {MoatResult} - The result of the request.
+ */
+ async #makeRequest(procedure, args, abortSignal) {
+ const procedureURIString = `${Services.prefs.getStringPref(
+ TorLauncherPrefs.moat_service
+ )}/${procedure}`;
+ log.info(`Making request to ${procedureURIString}:`, args);
+ let response = undefined;
+ let cancelled = false;
+ try {
+ response = JSON.parse(
+ await this.#requestBuilder.buildRequest(procedureURIString, {
+ method: "POST",
+ contentType: "application/vnd.api+json",
+ body: JSON.stringify(args),
+ signal: abortSignal,
+ })
+ );
+ log.info(`Response to ${procedureURIString}:`, response);
+ } catch (e) {
+ if (abortSignal && e instanceof lazy.DomainFrontRequestCancelledError) {
+ log.info(`Request to ${procedureURIString} cancelled`);
+ cancelled = true;
+ } else {
+ throw e;
+ }
+ }
+ return { response, cancelled };
+ }
+
+ /**
+ * Request a CAPTCHA challenge.
+ *
+ * @param {string[]} transports - List of transport strings available to us
+ * eg: ["obfs4", "meek"].
+ * @param {AbortSignal} abortSignal - A signal to abort the request early.
+ * @returns {?CaptchaChallenge} - The captcha challenge, or `null` if the
+ * request was aborted by the caller.
+ */
+ async fetch(transports, abortSignal) {
+ if (
+ // ensure this is an array
+ Array.isArray(transports) &&
+ // ensure array has values
+ !!transports.length &&
+ // ensure each value in the array is a string
+ transports.reduce((acc, cur) => acc && typeof cur === "string", true)
+ ) {
+ const args = {
+ data: [
+ {
+ version: "0.1.0",
+ type: "client-transports",
+ supported: transports,
+ },
+ ],
+ };
+ const { response, cancelled } = await this.#makeRequest(
+ "fetch",
+ args,
+ abortSignal
+ );
+ if (cancelled) {
+ return null;
+ }
+
+ if ("errors" in response) {
+ const code = response.errors[0].code;
+ const detail = response.errors[0].detail;
+ throw new Error(`MoatRPC: ${detail} (${code})`);
+ }
+
+ const transport = response.data[0].transport;
+ const image = response.data[0].image;
+ const challenge = response.data[0].challenge;
+
+ return { transport, image, challenge };
+ }
+ throw new Error("MoatRPC: fetch() expects a non-empty array of strings");
+ }
+
+ /**
+ * Submit an answer for a previous CAPTCHA fetch to get bridges.
+ *
+ * @param {string} transport - The transport associated with the fetch.
+ * @param {string} challenge - The nonce string associated with the fetch.
+ * @param {string} solution - The solution to the CAPTCHA.
+ * @param {AbortSignal} abortSignal - A signal to abort the request early.
+ * @returns {?string[]} - The bridge lines for a correct solution, or `null`
+ * if the solution was incorrect or the request was aborted by the caller.
+ */
+ async check(transport, challenge, solution, abortSignal) {
+ const args = {
+ data: [
+ {
+ id: "2",
+ version: "0.1.0",
+ type: "moat-solution",
+ transport,
+ challenge,
+ solution,
+ qrcode: "false",
+ },
+ ],
+ };
+ const { response, cancelled } = await this.#makeRequest(
+ "check",
+ args,
+ abortSignal
+ );
+ if (cancelled) {
+ return null;
+ }
+
+ if ("errors" in response) {
+ const code = response.errors[0].code;
+ const detail = response.errors[0].detail;
+ if (code == 419 && detail === "The CAPTCHA solution was incorrect.") {
+ return null;
+ }
+
+ throw new Error(`MoatRPC: ${detail} (${code})`);
+ }
+
+ return response.data[0].bridges;
+ }
+
+ /**
+ * Extract bridges from the received Moat settings object.
+ *
+ * @param {object} settings - The received settings.
+ * @returns {MoatBridge} The extracted bridges.
+ */
+ #extractBridges(settings) {
+ if (!("bridges" in settings)) {
+ throw new Error("Expected to find `bridges` in the settings object.");
+ }
+ const bridges = {};
+ switch (settings.bridges.source) {
+ case "builtin":
+ bridges.source = lazy.TorBridgeSource.BuiltIn;
+ bridges.builtin_type = String(settings.bridges.type);
+ // Ignore the bridge_strings argument since we will use our built-in
+ // bridge strings instead.
+ break;
+ case "bridgedb":
+ bridges.source = lazy.TorBridgeSource.BridgeDB;
+ if (settings.bridges.bridge_strings?.length) {
+ bridges.bridge_strings = Array.from(
+ settings.bridges.bridge_strings,
+ item => String(item)
+ );
+ } else {
+ throw new Error(
+ "Received no bridge-strings for BridgeDB bridge source"
+ );
+ }
+ break;
+ default:
+ throw new Error(
+ `Unexpected bridge source '${settings.bridges.source}'`
+ );
+ }
+ return bridges;
+ }
+
+ /**
+ * Extract a list of bridges from the received Moat settings object.
+ *
+ * @param {object} settingsList - The received settings.
+ * @returns {MoatBridge[]} The list of extracted bridges.
+ */
+ #extractBridgesList(settingsList) {
+ const bridgesList = [];
+ for (const settings of settingsList) {
+ try {
+ bridgesList.push(this.#extractBridges(settings));
+ } catch (ex) {
+ log.error(ex);
+ }
+ }
+ return bridgesList;
+ }
+
+ /**
+ * Request tor settings for the user optionally based on their location
+ * (derived from their IP). Takes the following parameters:
+ *
+ * @param {string[]} transports - A list of transports we support.
+ * @param {?string} country - The region to request bridges for, as an
+ * ISO 3166-1 alpha-2 region code, or `null` to have the server
+ * automatically determine the region.
+ * @param {AbortSignal} abortSignal - A signal to abort the request early.
+ * @returns {?MoatSettings} - The returned settings from the server, or `null`
+ * if the region could not be determined by the server or the caller
+ * cancelled the request.
+ */
+ async circumvention_settings(transports, country, abortSignal) {
+ const args = {
+ transports: transports ? transports : [],
+ country,
+ };
+ const { response, cancelled } = await this.#makeRequest(
+ "circumvention/settings",
+ args,
+ abortSignal
+ );
+ if (cancelled) {
+ return null;
+ }
+ let settings = {};
+ if ("errors" in response) {
+ const code = response.errors[0].code;
+ const detail = response.errors[0].detail;
+ if (code == 406) {
+ log.error(
+ "MoatRPC::circumvention_settings(): Cannot automatically determine user's country-code"
+ );
+ // cannot determine user's country
+ return null;
+ }
+
+ throw new Error(`MoatRPC: ${detail} (${code})`);
+ } else if ("settings" in response) {
+ settings.bridgesList = this.#extractBridgesList(response.settings);
+ }
+ if ("country" in response) {
+ settings.country = response.country;
+ }
+ return settings;
+ }
+
+ // Request a copy of the builtin bridges, takes the following parameters:
+ // - transports: optional, an array of transports we would like the latest
+ // bridge strings for; if empty (or not given) returns all of them
+ //
+ // returns a map whose keys are pluggable transport types and whose values are
+ // arrays of bridge strings for that type
+ async circumvention_builtin(transports) {
+ const args = {
+ transports: transports ? transports : [],
+ };
+ const { response } = await this.#makeRequest("circumvention/builtin", args);
+ if ("errors" in response) {
+ const code = response.errors[0].code;
+ const detail = response.errors[0].detail;
+ throw new Error(`MoatRPC: ${detail} (${code})`);
+ }
+
+ let map = new Map();
+ for (const [transport, bridge_strings] of Object.entries(response)) {
+ map.set(transport, bridge_strings);
+ }
+
+ return map;
+ }
+
+ /**
+ * Request a copy of the default/fallback bridge settings.
+ *
+ * @param {string[]} transports - A list of transports we support.
+ * @param {AbortSignal} abortSignal - A signal to abort the request early.
+ * @returns {?MoatBridges[]} - The list of bridges found, or `null` if the
+ * caller cancelled the request.
+ */
+ async circumvention_defaults(transports, abortSignal) {
+ const args = {
+ transports: transports ? transports : [],
+ };
+ const { response, cancelled } = await this.#makeRequest(
+ "circumvention/defaults",
+ args,
+ abortSignal
+ );
+ if (cancelled) {
+ return null;
+ }
+ 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.#extractBridgesList(response.settings);
+ }
+ return [];
+ }
+}
diff --git a/toolkit/modules/TorConnect.sys.mjs b/toolkit/modules/TorConnect.sys.mjs
@@ -0,0 +1,1633 @@
+/* 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";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ MoatRPC: "resource://gre/modules/Moat.sys.mjs",
+ TorBootstrapRequest: "resource://gre/modules/TorBootstrapRequest.sys.mjs",
+ TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorBootstrapError: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorProviderInitError: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
+ TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
+ TorSettingsTopics: "resource://gre/modules/TorSettings.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "NetworkLinkService", () => {
+ // NetworkLinkService is unavailable on some platforms like openBSD.
+ // See tor-browser#43628.
+ return Cc["@mozilla.org/network/network-link-service;1"]?.getService(
+ Ci.nsINetworkLinkService
+ );
+});
+
+const NETWORK_LINK_TOPIC = "network:link-status-changed";
+
+const TorConnectPrefs = Object.freeze({
+ censorship_level: "torbrowser.debug.censorship_level",
+ log_level: "torbrowser.bootstrap.log_level",
+ /* prompt_at_startup now controls whether the quickstart can trigger. */
+ prompt_at_startup: "extensions.torlauncher.prompt_at_startup",
+ quickstart: "torbrowser.settings.quickstart.enabled",
+});
+
+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 */
+ Configuring: "Configuring",
+ /* Tor is attempting to bootstrap with settings from censorship-circumvention db */
+ AutoBootstrapping: "AutoBootstrapping",
+ /* Tor is bootstrapping */
+ Bootstrapping: "Bootstrapping",
+ /* Passthrough state back to Configuring */
+ Error: "Error",
+ /* Final state, after successful bootstrap */
+ Bootstrapped: "Bootstrapped",
+ /* If we are using System tor or the legacy Tor-Launcher */
+ Disabled: "Disabled",
+});
+
+/**
+ * A class for exceptions thrown during the bootstrap process.
+ */
+export class TorConnectError extends Error {
+ static get Offline() {
+ return "Offline";
+ }
+ static get BootstrapError() {
+ return "BootstrapError";
+ }
+ static get CannotDetermineCountry() {
+ return "CannotDetermineCountry";
+ }
+ static get NoSettingsForCountry() {
+ return "NoSettingsForCountry";
+ }
+ static get AllSettingsFailed() {
+ return "AllSettingsFailed";
+ }
+ static get ExternalError() {
+ return "ExternalError";
+ }
+
+ constructor(code, cause) {
+ super(cause?.message ?? `TorConnectError: ${code}`, cause ? { cause } : {});
+ this.name = "TorConnectError";
+ this.code = code;
+ }
+}
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () =>
+ console.createInstance({
+ maxLogLevelPref: TorConnectPrefs.log_level,
+ prefix: "TorConnect",
+ })
+);
+
+/* Topics Notified by the TorConnect module */
+export const TorConnectTopics = Object.freeze({
+ StageChange: "torconnect:stage-change",
+ // TODO: Remove torconnect:state-change when pages have switched to stage.
+ StateChange: "torconnect:state-change",
+ QuickstartChange: "torconnect:quickstart-change",
+ InternetStatusChange: "torconnect:internet-status-change",
+ RegionNamesChange: "torconnect:region-names-change",
+ BootstrapProgress: "torconnect:bootstrap-progress",
+ BootstrapComplete: "torconnect:bootstrap-complete",
+ // TODO: Remove torconnect:error when pages have switched to stage.
+ Error: "torconnect:error",
+});
+
+/**
+ * @callback ProgressCallback
+ *
+ * @param {integer} progress - The progress percent.
+ */
+/**
+ * @typedef {object} BootstrapOptions
+ *
+ * Options for a bootstrap attempt.
+ *
+ * @property {boolean} [simulateCensorship] - Whether to simulate a failing
+ * bootstrap.
+ * @property {integer} [simulateDelay] - The delay in microseconds to apply to
+ * simulated bootstraps.
+ * @property {MoatSettings} [simulateMoatResponse] - Simulate a Moat response
+ * for circumvention settings. Should include a "bridgesList" property, and
+ * optionally a "country" property. The "bridgesList" property should be an
+ * Array of MoatBridges objects that match the bridge settings accepted by
+ * TorSettings.bridges, plus you may add a "simulateCensorship" property to
+ * make only their bootstrap attempts fail.
+ * @property {string} [regionCode] - The region code to use to fetch
+ * auto-bootstrap settings, or "automatic" to automatically choose the region.
+ */
+/**
+ * @typedef {object} BootstrapResult
+ *
+ * The result of a bootstrap attempt.
+ *
+ * @property {string} [result] - The bootstrap result.
+ * @property {Error} [error] - An error from the attempt.
+ */
+/**
+ * @callback ResolveBootstrap
+ *
+ * Resolve a bootstrap attempt.
+ *
+ * @param {BootstrapResult} [result] - The result, or error.
+ */
+
+/**
+ * Each instance can be used to attempt one bootstrapping.
+ */
+class BootstrapAttempt {
+ /**
+ * The ongoing bootstrap request.
+ *
+ * @type {?TorBootstrapRequest}
+ */
+ #bootstrap = null;
+ /**
+ * The method to call to complete the `run` promise.
+ *
+ * @type {?ResolveBootstrap}
+ */
+ #resolveRun = null;
+ /**
+ * Whether the `run` promise has been, or is about to be, resolved.
+ *
+ * @type {boolean}
+ */
+ #resolved = false;
+ /**
+ * Whether a cancel request has been started.
+ *
+ * @type {boolean}
+ */
+ #cancelled = false;
+
+ /**
+ * Run a bootstrap attempt.
+ *
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ *
+ * @returns {Promise<string, Error>} - The result of the bootstrap.
+ */
+ run(progressCallback, options) {
+ const { promise, resolve, reject } = Promise.withResolvers();
+ this.#resolveRun = arg => {
+ if (this.#resolved) {
+ // Already been called once.
+ if (arg.error) {
+ lazy.logger.error("Delayed bootstrap error", arg.error);
+ }
+ return;
+ }
+ this.#resolved = true;
+ if (arg.error) {
+ reject(arg.error);
+ } else {
+ resolve(arg.result);
+ }
+ };
+ try {
+ this.#runInternal(progressCallback, options);
+ } catch (error) {
+ this.#resolveRun({ error });
+ }
+
+ return promise;
+ }
+
+ /**
+ * Method to call just after the Bootstrapped stage is set in response to this
+ * bootstrap attempt.
+ */
+ postBootstrapped() {
+ // Nothing to do.
+ }
+
+ /**
+ * Run the attempt.
+ *
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ */
+ #runInternal(progressCallback, options) {
+ if (options.simulateCensorship) {
+ // Create a fake request.
+ this.#bootstrap = {
+ _timeout: 0,
+ bootstrap() {
+ this._timeout = setTimeout(() => {
+ const err = new lazy.TorBootstrapError({
+ summary: "Censorship simulation",
+ phase: "conn",
+ reason: "noroute",
+ });
+ this.onbootstraperror(err);
+ }, options.simulateDelay || 0);
+ },
+ cancel() {
+ clearTimeout(this._timeout);
+ },
+ };
+ } else {
+ this.#bootstrap = new lazy.TorBootstrapRequest();
+ }
+
+ this.#bootstrap.onbootstrapstatus = (progress, _status) => {
+ if (!this.#resolved) {
+ progressCallback(progress);
+ }
+ };
+ this.#bootstrap.onbootstrapcomplete = () => {
+ this.#resolveRun({ result: "complete" });
+ };
+ this.#bootstrap.onbootstraperror = error => {
+ if (this.#resolved || this.#cancelled) {
+ // We ignore this error since it occurred after cancelling (by the
+ // user), or we have already resolved. We assume the error is just a
+ // side effect of the cancelling.
+ // E.g. If the cancelling is triggered late in the process, we get
+ // "Building circuits: Establishing a Tor circuit failed".
+ // TODO: Maybe move this logic deeper in the process to know when to
+ // filter out such errors triggered by cancelling.
+ lazy.logger.warn("Post-complete bootstrap error.", error);
+ return;
+ }
+
+ this.#resolveRun({ error });
+ };
+
+ this.#bootstrap.bootstrap();
+ }
+
+ /**
+ * Cancel the bootstrap attempt.
+ */
+ async cancel() {
+ if (this.#cancelled) {
+ lazy.logger.warn(
+ "Cancelled bootstrap after it has already been cancelled"
+ );
+ return;
+ }
+ this.#cancelled = true;
+ if (this.#resolved) {
+ lazy.logger.warn("Cancelled bootstrap after it has already resolved");
+ return;
+ }
+ // Wait until after #bootstrap.cancel returns before we resolve with
+ // cancelled. In particular:
+ // + there is a small chance that the bootstrap completes, in which case we
+ // want to be able to resolve with a success instead.
+ // + we want to make sure that we only resolve this BootstrapAttempt
+ // when the current TorBootstrapRequest instance is fully resolved so
+ // there are never two competing instances.
+ await this.#bootstrap?.cancel();
+ this.#resolveRun({ result: "cancelled" });
+ }
+}
+
+/**
+ * Each instance can be used to attempt one auto-bootstrapping sequence.
+ */
+class AutoBootstrapAttempt {
+ /**
+ * The current bootstrap attempt, if any.
+ *
+ * @type {?BootstrapAttempt}
+ */
+ #bootstrapAttempt = null;
+ /**
+ * The method to call to complete the `run` promise.
+ *
+ * @type {?ResolveBootstrap}
+ */
+ #resolveRun = null;
+ /**
+ * Whether the `run` promise has been, or is about to be, resolved.
+ *
+ * @type {boolean}
+ */
+ #resolved = false;
+ /**
+ * Whether a cancel request has been started.
+ *
+ * @type {boolean}
+ */
+ #cancelled = false;
+ /**
+ * The method to call when the cancelled value is set to true.
+ *
+ * @type {?Function}
+ */
+ #resolveCancelled = null;
+ /**
+ * A promise that resolves when the cancelled value is set to true. We can use
+ * this with Promise.race to end early when the user cancels.
+ *
+ * @type {?Promise}
+ */
+ #cancelledPromise = null;
+ /**
+ * The list of bridge configurations from Moat.
+ *
+ * @type {?MoatBridges[]}
+ */
+ #bridgesList = null;
+ /**
+ * The detected region code returned by Moat, if any.
+ *
+ * @type {?string}
+ */
+ detectedRegion = null;
+
+ /**
+ * Run an auto-bootstrap attempt.
+ *
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ *
+ * @returns {Promise<string, Error>} - The result of the bootstrap.
+ */
+ run(progressCallback, options) {
+ const { promise, resolve, reject } = Promise.withResolvers();
+
+ this.#resolveRun = async arg => {
+ if (this.#resolved) {
+ // Already been called once.
+ if (arg.error) {
+ lazy.logger.error("Delayed auto-bootstrap error", arg.error);
+ }
+ return;
+ }
+ this.#resolved = true;
+
+ if (lazy.TorSettings.initialized) {
+ // If not initialized, then we won't have applied any settings.
+ try {
+ // Run cleanup before we resolve the promise to ensure two instances
+ // of AutoBootstrapAttempt are not trying to change the settings at
+ // the same time.
+ if (arg.result !== "complete") {
+ // Should do nothing if we never called applyTemporaryBridges.
+ await lazy.TorSettings.clearTemporaryBridges();
+ }
+ } catch (error) {
+ lazy.logger.error(
+ "Unexpected error in auto-bootstrap cleanup",
+ error
+ );
+ }
+ }
+ if (arg.error) {
+ reject(arg.error);
+ } else {
+ resolve(arg.result);
+ }
+ };
+
+ ({ promise: this.#cancelledPromise, resolve: this.#resolveCancelled } =
+ Promise.withResolvers());
+
+ this.#runInternal(progressCallback, options).catch(error => {
+ this.#resolveRun({ error });
+ });
+
+ return promise;
+ }
+
+ /**
+ * Method to call just after the Bootstrapped stage is set in response to this
+ * bootstrap attempt.
+ */
+ postBootstrapped() {
+ // Persist the current settings to preferences.
+ lazy.TorSettings.saveTemporaryBridges();
+ }
+
+ /**
+ * Run the attempt.
+ *
+ * Note, this is an async method, but should *not* be awaited by the `run`
+ * method.
+ *
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ */
+ async #runInternal(progressCallback, options) {
+ // Wait for TorSettings to be initialised, which is used for the
+ // AutoBootstrapping set up.
+ await Promise.race([
+ lazy.TorSettings.initializedPromise,
+ this.#cancelledPromise,
+ ]);
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+
+ await this.#fetchBridges(options);
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+
+ if (!this.#bridgesList?.length) {
+ this.#resolveRun({
+ error: new TorConnectError(
+ options.regionCode === "automatic" && !this.detectedRegion
+ ? TorConnectError.CannotDetermineCountry
+ : TorConnectError.NoSettingsForCountry
+ ),
+ });
+ }
+
+ // Apply each of our settings and try to bootstrap with each.
+ for (const [index, bridges] of this.#bridgesList.entries()) {
+ lazy.logger.info(
+ `Attempting Bootstrap with configuration ${index + 1}/${
+ this.#bridgesList.length
+ }`
+ );
+
+ await this.#tryBridges(bridges, progressCallback, options);
+
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+ }
+
+ this.#resolveRun({
+ error: new TorConnectError(TorConnectError.AllSettingsFailed),
+ });
+ }
+
+ /**
+ * Lookup user's potential censorship circumvention settings from Moat
+ * service.
+ *
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ */
+ async #fetchBridges(options) {
+ if (options.simulateMoatResponse) {
+ await Promise.race([
+ new Promise(res => setTimeout(res, options.simulateDelay || 0)),
+ this.#cancelledPromise,
+ ]);
+
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+
+ this.detectedRegion = options.simulateMoatResponse.country || null;
+ this.#bridgesList = options.simulateMoatResponse.bridgesList ?? null;
+
+ return;
+ }
+
+ const moat = new lazy.MoatRPC();
+ try {
+ // We need to wait Moat's initialization even when we are requested to
+ // transition to another state to be sure its uninit will have its
+ // intended effect. So, do not use Promise.race here.
+ await moat.init();
+
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+
+ let moatAbortController = new AbortController();
+ // For now, throw any errors we receive from the backend, except when it
+ // was unable to detect user's country/region.
+ // If we use specialized error objects, we could pass the original errors
+ // to them.
+ const maybeSettings = await Promise.race([
+ moat.circumvention_settings(
+ [...lazy.TorSettings.builtinBridgeTypes, "vanilla"],
+ options.regionCode === "automatic" ? null : options.regionCode,
+ moatAbortController.signal
+ ),
+ // This might set maybeSettings to undefined.
+ this.#cancelledPromise,
+ ]);
+ if (this.#cancelled) {
+ // Ended early due to being cancelled. Abort the ongoing Moat request so
+ // that it does not continue unnecessarily in the background.
+ // NOTE: We do not care about circumvention_settings return value or
+ // errors at this point. Nor do we need to await its return. We just
+ // want it to resolve quickly.
+ moatAbortController.abort();
+ }
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+
+ this.detectedRegion = maybeSettings?.country || null;
+
+ if (maybeSettings?.bridgesList?.length) {
+ this.#bridgesList = maybeSettings.bridgesList;
+ } else {
+ // In principle we could reuse the existing moatAbortController
+ // instance, since its abort method has not been called. But it is
+ // cleaner to use a new instance to avoid triggering any potential
+ // lingering callbacks attached to the AbortSignal.
+ moatAbortController = new AbortController();
+ // Keep consistency with the other call.
+ this.#bridgesList = await Promise.race([
+ moat.circumvention_defaults(
+ [...lazy.TorSettings.builtinBridgeTypes, "vanilla"],
+ moatAbortController.signal
+ ),
+ // This might set this.#bridgesList to undefined.
+ this.#cancelledPromise,
+ ]);
+ if (this.#cancelled) {
+ // Ended early due to being cancelled. Abort the ongoing Moat request
+ // so that it does not continue in the background.
+ moatAbortController.abort();
+ }
+ }
+ } finally {
+ // Do not await the uninit.
+ moat.uninit();
+ }
+ }
+
+ /**
+ * Try to apply the settings we fetched.
+ *
+ * @param {MoatBridges} bridges - The bridges to try.
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ */
+ async #tryBridges(bridges, progressCallback, options) {
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+
+ if (options.simulateMoatResponse && bridges.simulateCensorship) {
+ // Move the simulateCensorship option to the options for the next
+ // BootstrapAttempt.
+ bridges = structuredClone(bridges);
+ delete bridges.simulateCensorship;
+ options = { ...options, simulateCensorship: true };
+ }
+
+ // Send the new settings directly to the provider. We will save them only
+ // if the bootstrap succeeds.
+ await lazy.TorSettings.applyTemporaryBridges(bridges);
+
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+
+ let result;
+ try {
+ this.#bootstrapAttempt = new BootstrapAttempt();
+ // At this stage, cancelling AutoBootstrap will also cancel this
+ // bootstrapAttempt.
+ result = await this.#bootstrapAttempt.run(progressCallback, options);
+ } catch (error) {
+ // Only re-try with the next settings *if* we have a TorBootstrapError.
+ // Other errors will end this auto-bootstrap attempt entirely.
+ if (error instanceof lazy.TorBootstrapError) {
+ lazy.logger.info("TorConnect setting failed", bridges, error);
+ // Try with the next settings.
+ // NOTE: We do not restore the user settings in between these runs.
+ // Instead we wait for #resolveRun callback to do so.
+ // This means there is a window of time where the setting is applied, but
+ // no bootstrap is running.
+ return;
+ }
+ // Pass error up.
+ throw error;
+ } finally {
+ this.#bootstrapAttempt = null;
+ }
+
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+
+ // Pass the BootstrapAttempt result up.
+ this.#resolveRun({ result });
+ }
+
+ /**
+ * Cancel the bootstrap attempt.
+ */
+ async cancel() {
+ if (this.#cancelled) {
+ lazy.logger.warn(
+ "Cancelled auto-bootstrap after it has already been cancelled"
+ );
+ return;
+ }
+ this.#cancelled = true;
+ this.#resolveCancelled();
+ if (this.#resolved) {
+ lazy.logger.warn(
+ "Cancelled auto-bootstrap after it has already resolved"
+ );
+ return;
+ }
+
+ // Wait until after #bootstrapAttempt.cancel returns before we resolve with
+ // cancelled. In particular:
+ // + there is a small chance that the bootstrap completes, in which case we
+ // want to be able to resolve with a success instead.
+ // + we want to make sure that we only resolve this AutoBootstrapAttempt
+ // when the current TorBootstrapRequest instance is fully resolved so
+ // there are never two competing instances.
+ if (this.#bootstrapAttempt) {
+ await this.#bootstrapAttempt.cancel();
+ }
+ // In case no bootstrap is running, we resolve with "cancelled".
+ this.#resolveRun({ result: "cancelled" });
+ }
+}
+
+export const InternetStatus = Object.freeze({
+ Unknown: -1,
+ Offline: 0,
+ Online: 1,
+});
+
+// This enum is mirrored for Android in TorConnectStageName.java. Changes should be mirrored there
+export const TorConnectStage = Object.freeze({
+ Disabled: "Disabled",
+ Loading: "Loading",
+ Start: "Start",
+ Bootstrapping: "Bootstrapping",
+ Offline: "Offline",
+ ChooseRegion: "ChooseRegion",
+ RegionNotFound: "RegionNotFound",
+ ConfirmRegion: "ConfirmRegion",
+ FinalError: "FinalError",
+ Bootstrapped: "Bootstrapped",
+});
+
+/**
+ * @typedef {object} ConnectStage
+ *
+ * A summary of the user stage.
+ * (This class is mirrored for Android in TorConnectStage.java. Changes should be mirrored there)
+ *
+ * @property {string} name - The name of the stage. One of the values in
+ * `TorConnectStage`.
+ * @property {string} defaultRegion - The default region to show in the UI.
+ * @property {?string} bootstrapTrigger - The name of the stage prior to this
+ * bootstrap attempt. Only set during the "Bootstrapping" stage.
+ * @property {boolean} isQuickstart - Whether the current bootstrap attempt was
+ * triggered by the `TorConnect.quickstart` preference. Will be `false`
+ * outside of the "Bootstrapping" stage.
+ * @property {?BootstrapError} error - The last bootstrapping error.
+ * @property {boolean} tryAgain - Whether a bootstrap attempt has failed, so
+ * that a normal bootstrap should be shown as "Try Again" instead of
+ * "Connect". NOTE: to be removed when about:torconnect no longer uses
+ * breadcrumbs.
+ * @property {boolean} potentiallyBlocked - Whether bootstrapping has ever
+ * failed, not including being cancelled or being offline. I.e. whether we
+ * have reached an error stage at some point before being bootstrapped.
+ * @property {BootstrappingStatus} bootstrappingStatus - The current
+ * bootstrapping status.
+ */
+
+/**
+ * @typedef {object} BootstrappingStatus
+ *
+ * The status of a bootstrap.
+ * (This class is mirrored for Android in TorBootstrapping.java. Changes should be mirrored there)
+ *
+ * @property {number} progress - The percent progress.
+ * @property {boolean} hasWarning - Whether this bootstrap has a warning in the
+ * Tor log.
+ */
+
+/**
+ * @typedef {object} BootstrapError
+ *
+ * Details about the error that caused bootstrapping to fail.
+ * (This class is mirrored for Android in TorError.java. Changes should be mirrored there)
+ *
+ * @property {string} code - The error code type.
+ * @property {string} message - The error message.
+ * @property {?string} phase - The bootstrapping phase that failed.
+ * @property {?string} reason - The bootstrapping failure reason.
+ */
+
+export const TorConnect = {
+ /**
+ * Default bootstrap options for simulation.
+ *
+ * @type {BootstrapOptions}
+ */
+ simulateBootstrapOptions: {},
+
+ /**
+ * Whether to simulate being offline.
+ *
+ * @type {boolean}
+ */
+ simulateOffline: false,
+
+ /**
+ * The name of the current stage the user is in.
+ *
+ * @type {string}
+ */
+ _stageName: TorConnectStage.Loading,
+
+ get stageName() {
+ return this._stageName;
+ },
+
+ /**
+ * The stage that triggered bootstrapping.
+ *
+ * @type {?string}
+ */
+ _bootstrapTrigger: null,
+
+ /**
+ * Whether the current bootstrapping attempt was triggered by the quickstart
+ * preference.
+ *
+ * @type {boolean}
+ */
+ _isQuickstart: false,
+
+ /**
+ * The alternative stage that we should move to after bootstrapping completes.
+ *
+ * @type {?string}
+ */
+ _requestedStage: null,
+
+ /**
+ * The default region to show in the UI for auto-bootstrapping.
+ *
+ * @type {string}
+ */
+ _defaultRegion: "automatic",
+
+ /**
+ * The current bootstrap attempt, if any.
+ *
+ * @type {?(BootstrapAttempt|AutoBootstrapAttempt)}
+ */
+ _bootstrapAttempt: null,
+
+ /**
+ * The bootstrap error that was last generated.
+ *
+ * @type {?TorConnectError}
+ */
+ _errorDetails: null,
+
+ /**
+ * Whether a bootstrap attempt has failed, so that a normal bootstrap should
+ * be shown as "Try Again" instead of "Connect".
+ *
+ * @type {boolean}
+ */
+ // TODO: Drop tryAgain when we remove breadcrumbs and use "Start again"
+ // instead.
+ _tryAgain: false,
+
+ /**
+ * Whether bootstrapping has ever returned an error.
+ *
+ * @type {boolean}
+ */
+ _potentiallyBlocked: false,
+
+ /**
+ * Get a summary of the current user stage.
+ *
+ * @type {ConnectStage}
+ */
+ get stage() {
+ return {
+ name: this._stageName,
+ defaultRegion: this._defaultRegion,
+ bootstrapTrigger: this._bootstrapTrigger,
+ isQuickstart: this._isQuickstart,
+ error: this._errorDetails
+ ? {
+ code: this._errorDetails.code,
+ message: String(this._errorDetails.message ?? ""),
+ phase: this._errorDetails.cause?.phase ?? null,
+ reason: this._errorDetails.cause?.reason ?? null,
+ }
+ : null,
+ tryAgain: this._tryAgain,
+ potentiallyBlocked: this._potentiallyBlocked,
+ bootstrappingStatus: structuredClone(this._bootstrappingStatus),
+ };
+ },
+
+ /**
+ * Promise that resolves to a list of region codes that Moat has special
+ * bridge settings for.
+ *
+ * @type {Promise<string[]>}
+ */
+ _moatRegionsPromise: null,
+
+ /**
+ * The map of all regions and their localized names. Or `null` when
+ * uninitialized.
+ *
+ * @type {?object}
+ */
+ _regionNames: null,
+
+ /**
+ * The status of the most recent bootstrap attempt.
+ *
+ * @type {BootstrappingStatus}
+ */
+ _bootstrappingStatus: {
+ progress: 0,
+ hasWarning: false,
+ },
+
+ /**
+ * Notify the bootstrap progress.
+ */
+ _notifyBootstrapProgress() {
+ lazy.logger.debug("BootstrappingStatus", this._bootstrappingStatus);
+ Services.obs.notifyObservers(
+ this._bootstrappingStatus,
+ TorConnectTopics.BootstrapProgress
+ );
+ },
+
+ /**
+ * The current internet status. One of the InternetStatus values.
+ *
+ * @type {integer}
+ */
+ _internetStatus: InternetStatus.Unknown,
+
+ get internetStatus() {
+ return this._internetStatus;
+ },
+
+ /**
+ * Update the _internetStatus value.
+ */
+ _updateInternetStatus() {
+ let newStatus;
+ if (lazy.NetworkLinkService?.linkStatusKnown) {
+ newStatus = lazy.NetworkLinkService.isLinkUp
+ ? InternetStatus.Online
+ : InternetStatus.Offline;
+ } else {
+ newStatus = InternetStatus.Unknown;
+ }
+ if (newStatus === this._internetStatus) {
+ return;
+ }
+ this._internetStatus = newStatus;
+ Services.obs.notifyObservers(null, TorConnectTopics.InternetStatusChange);
+ },
+
+ // init should be called by TorStartupService
+ init() {
+ lazy.logger.debug("TorConnect.init()");
+
+ if (!this.enabled) {
+ // Disabled
+ this._setStage(TorConnectStage.Disabled);
+ return;
+ }
+
+ this._moatRegionsPromise = fetch(
+ "chrome://global/content/moat_countries.json"
+ )
+ .then(req => req.json())
+ // Filter out the "_comment" object in the moat_countries_dev_build.json
+ // file.
+ .then(regionList => regionList.filter(r => typeof r === "string"))
+ .catch(e => {
+ lazy.logger.error("Failed to fetch Moat region codes", e);
+ return [];
+ });
+
+ let observeTopic = addTopic => {
+ Services.obs.addObserver(this, addTopic);
+ lazy.logger.debug(`Observing topic '${addTopic}'`);
+ };
+
+ // register the Tor topics we always care about
+ observeTopic(lazy.TorProviderTopics.ProcessExited);
+ observeTopic(lazy.TorProviderTopics.HasWarnOrErr);
+ observeTopic(lazy.TorSettingsTopics.SettingsChanged);
+ observeTopic(NETWORK_LINK_TOPIC);
+ observeTopic("intl:app-locales-changed");
+
+ this._updateInternetStatus();
+
+ // NOTE: At this point, _requestedStage should still be `null`.
+ this._setStage(TorConnectStage.Start);
+ if (
+ // Quickstart setting is enabled.
+ this.quickstart &&
+ // And the previous bootstrap attempt must have succeeded.
+ !Services.prefs.getBoolPref(TorConnectPrefs.prompt_at_startup, true)
+ ) {
+ this._beginBootstrappingInternal(undefined, true);
+ }
+ },
+
+ async observe(subject, topic) {
+ lazy.logger.debug(`Observed ${topic}`);
+
+ switch (topic) {
+ case lazy.TorProviderTopics.HasWarnOrErr:
+ if (this._bootstrappingStatus.hasWarning) {
+ // No change.
+ return;
+ }
+ if (this._stageName === "Bootstrapping") {
+ this._bootstrappingStatus.hasWarning = true;
+ this._notifyBootstrapProgress();
+ }
+ break;
+ case lazy.TorProviderTopics.ProcessExited:
+ lazy.logger.info("Starting again since the tor process exited");
+ // Treat a failure as a possibly broken configuration.
+ // So, prevent quickstart at the next start.
+ Services.prefs.setBoolPref(TorConnectPrefs.prompt_at_startup, true);
+ this._makeStageRequest(TorConnectStage.Start, true);
+ break;
+ case lazy.TorSettingsTopics.SettingsChanged:
+ if (
+ this._stageName !== TorConnectStage.Bootstrapped &&
+ this._stageName !== TorConnectStage.Loading &&
+ this._stageName !== TorConnectStage.Start &&
+ subject.wrappedJSObject.changes.some(propertyName =>
+ propertyName.startsWith("bridges.")
+ )
+ ) {
+ // A change in bridge settings before we are bootstrapped, we reset
+ // the stage to Start to:
+ // + Cancel any ongoing bootstrap attempt. In particular, we
+ // definitely do not want to continue with an auto-bootstrap's
+ // temporary bridges if the settings have changed.
+ // + Reset the UI to be ready for normal bootstrapping in case the
+ // user returns to about:torconnect.
+ // See tor-browser#41921.
+ // NOTE: We do not reset in response to a change in the quickstart,
+ // firewall or proxy settings.
+ lazy.logger.warn(
+ "Resetting to Start stage since bridge settings changed"
+ );
+ // Rather than cancel and return to the pre-bootstrap stage, we
+ // explicitly cancel and return to the start stage.
+ this._makeStageRequest(TorConnectStage.Start);
+ }
+ break;
+ case NETWORK_LINK_TOPIC:
+ this._updateInternetStatus();
+ break;
+ case "intl:app-locales-changed":
+ // Unset the regionNames to use the new app locale when requested.
+ this._regionNames = null;
+ // Let consumers know that the list of names has changed.
+ Services.obs.notifyObservers(null, TorConnectTopics.RegionNamesChange);
+ break;
+ }
+ },
+
+ /**
+ * Set the user stage.
+ *
+ * @param {string} name - The name of the stage to move to.
+ */
+ _setStage(name) {
+ if (this._bootstrapAttempt) {
+ throw new Error(`Trying to set the stage to ${name} during a bootstrap`);
+ }
+
+ lazy.logger.info(`Entering stage ${name}`);
+ const prevState = this.state;
+ this._stageName = name;
+ this._bootstrappingStatus.hasWarning = false;
+ this._bootstrappingStatus.progress =
+ name === TorConnectStage.Bootstrapped ? 100 : 0;
+
+ Services.obs.notifyObservers(this.stage, TorConnectTopics.StageChange);
+
+ // TODO: Remove when all pages have switched to stage.
+ const newState = this.state;
+ if (prevState !== newState) {
+ Services.obs.notifyObservers(
+ { state: newState },
+ TorConnectTopics.StateChange
+ );
+ }
+
+ // Update the progress after the stage has changed.
+ this._notifyBootstrapProgress();
+ },
+
+ /*
+ Various getters
+ */
+
+ /**
+ * Whether TorConnect is enabled.
+ *
+ * @type {boolean}
+ */
+ get enabled() {
+ // FIXME: This is called before the TorProvider is ready.
+ // As a matter of fact, at the moment it is equivalent to the following
+ // line, but this might become a problem in the future.
+ return lazy.TorLauncherUtil.shouldStartAndOwnTor;
+ },
+
+ /**
+ * Whether bootstrapping can begin immediately once Tor Browser has been
+ * opened.
+ *
+ * @type {boolean}
+ */
+ get quickstart() {
+ return Services.prefs.getBoolPref(TorConnectPrefs.quickstart, false);
+ },
+
+ set quickstart(enabled) {
+ enabled = Boolean(enabled);
+ if (enabled === this.quickstart) {
+ return;
+ }
+ Services.prefs.setBoolPref(TorConnectPrefs.quickstart, enabled);
+ Services.obs.notifyObservers(null, TorConnectTopics.QuickstartChange);
+ },
+
+ get shouldShowTorConnect() {
+ // TorBrowser must control the daemon
+ return (
+ this.enabled &&
+ // if we have succesfully bootstraped, then no need to show TorConnect
+ this._stageName !== TorConnectStage.Bootstrapped
+ );
+ },
+
+ /**
+ * Whether we are in a stage that can lead into a "normal" bootstrapping
+ * request.
+ *
+ * The value may change with TorConnectTopics.StageChanged.
+ */
+ get canBeginNormalBootstrap() {
+ return (
+ this._stageName === TorConnectStage.Start ||
+ this._stageName === TorConnectStage.Offline
+ );
+ },
+
+ /**
+ * Whether we are in an error stage that can lead into the Bootstrapping
+ * stage. I.e. whether we can make an "auto" bootstrapping request.
+ *
+ * The value may change with TorConnectTopics.StageChanged.
+ */
+ get canBeginAutoBootstrap() {
+ return (
+ this._stageName === TorConnectStage.ChooseRegion ||
+ this._stageName === TorConnectStage.RegionNotFound ||
+ this._stageName === TorConnectStage.ConfirmRegion
+ );
+ },
+
+ // TODO: Remove when all pages have switched to "stage".
+ get state() {
+ // There is no "Error" stage, but about:torconnect relies on receiving the
+ // Error state to update its display. So we temporarily set the stage for a
+ // StateChange signal.
+ if (this._isErrorState) {
+ return TorConnectState.Error;
+ }
+ switch (this._stageName) {
+ case TorConnectStage.Disabled:
+ return TorConnectState.Disabled;
+ case TorConnectStage.Loading:
+ return TorConnectState.Initial;
+ case TorConnectStage.Start:
+ case TorConnectStage.Offline:
+ case TorConnectStage.ChooseRegion:
+ case TorConnectStage.RegionNotFound:
+ case TorConnectStage.ConfirmRegion:
+ case TorConnectStage.FinalError:
+ return TorConnectState.Configuring;
+ case TorConnectStage.Bootstrapping:
+ if (
+ this._bootstrapTrigger === TorConnectStage.Start ||
+ this._bootstrapTrigger === TorConnectStage.Offline
+ ) {
+ return TorConnectState.Bootstrapping;
+ }
+ return TorConnectState.AutoBootstrapping;
+ case TorConnectStage.Bootstrapped:
+ return TorConnectState.Bootstrapped;
+ }
+ lazy.logger.error(`Unknown state at stage ${this._stageName}`);
+ return null;
+ },
+
+ /**
+ * Get a map of all region codes and their localized names.
+ *
+ * @returns {object} - The region names in the current locale.
+ */
+ getRegionNames() {
+ if (!this._regionNames) {
+ this._regionNames = {};
+ const codes = Services.intl.getAvailableLocaleDisplayNames("region");
+ // Get the localised names, for the current app locale.
+ const names = Services.intl.getRegionDisplayNames(undefined, codes);
+ for (let i = 0; i < codes.length; i++) {
+ this._regionNames[codes[i]] = names[i];
+ }
+ }
+ return structuredClone(this._regionNames);
+ },
+
+ /**
+ * Whether the Bootstrapping process has ever failed, not including being
+ * cancelled or being offline.
+ *
+ * The value may change with TorConnectTopics.StageChanged.
+ *
+ * @type {boolean}
+ */
+ get potentiallyBlocked() {
+ return this._potentiallyBlocked;
+ },
+
+ /**
+ * Ensure that we are not disabled.
+ */
+ _ensureEnabled() {
+ if (!this.enabled || this._stageName === TorConnectStage.Disabled) {
+ throw new Error("Unexpected Disabled stage for user method");
+ }
+ },
+
+ /**
+ * Signal an error to listeners.
+ *
+ * @param {Error} error - The error.
+ */
+ _signalError(error) {
+ // TODO: Replace this method with _setError without any signalling when
+ // pages have switched to stage.
+ // Currently it simulates the old behaviour for about:torconnect.
+ lazy.logger.debug("Signalling error", error);
+
+ if (error instanceof lazy.TorBootstrapError) {
+ error = new TorConnectError(TorConnectError.BootstrapError, error);
+ } else if (!(error instanceof TorConnectError)) {
+ error = new TorConnectError(TorConnectError.ExternalError, error);
+ }
+ this._errorDetails = error;
+
+ // Temporarily set an error state for listeners.
+ // We send the Error signal before the "StateChange" signal.
+ // Expected on android `onBootstrapError` to set lastKnownError.
+ // Expected in about:torconnect to set the error codes and internet status
+ // *before* the StateChange signal.
+ this._isErrorState = true;
+ Services.obs.notifyObservers(error, TorConnectTopics.Error);
+ Services.obs.notifyObservers(
+ { state: this.state },
+ TorConnectTopics.StateChange
+ );
+ this._isErrorState = false;
+ },
+
+ /**
+ * Add simulation options to the bootstrap request.
+ *
+ * @param {BootstrapOptions} bootstrapOptions - The options to add to.
+ * @param {string} [regionCode] - The region code being used.
+ */
+ _addSimulateOptions(bootstrapOptions, regionCode) {
+ if (this.simulateBootstrapOptions.simulateCensorship) {
+ bootstrapOptions.simulateCensorship = true;
+ }
+ if (this.simulateBootstrapOptions.simulateDelay) {
+ bootstrapOptions.simulateDelay =
+ this.simulateBootstrapOptions.simulateDelay;
+ }
+ if (this.simulateBootstrapOptions.simulateMoatResponse) {
+ bootstrapOptions.simulateMoatResponse =
+ this.simulateBootstrapOptions.simulateMoatResponse;
+ }
+
+ const censorshipLevel = Services.prefs.getIntPref(
+ TorConnectPrefs.censorship_level,
+ 0
+ );
+ if (censorshipLevel > 0 && !bootstrapOptions.simulateDelay) {
+ bootstrapOptions.simulateDelay = 1500;
+ }
+ if (censorshipLevel === 1) {
+ // Bootstrap fails, but auto-bootstrap does not.
+ if (!regionCode) {
+ bootstrapOptions.simulateCensorship = true;
+ }
+ } else if (censorshipLevel === 2) {
+ // Bootstrap fails. Auto-bootstrap fails with ConfirmRegion when using
+ // auto-detect region, but succeeds otherwise.
+ if (!regionCode) {
+ bootstrapOptions.simulateCensorship = true;
+ }
+ if (regionCode === "automatic") {
+ bootstrapOptions.simulateCensorship = true;
+ bootstrapOptions.simulateMoatResponse = {
+ country: "fi",
+ bridgesList: [
+ { source: 0, builtin_type: "obfs4" },
+ { source: 0, builtin_type: "snowflake" },
+ ],
+ };
+ }
+ } else if (censorshipLevel === 3) {
+ // Bootstrap and auto-bootstrap fail.
+ bootstrapOptions.simulateCensorship = true;
+ bootstrapOptions.simulateMoatResponse = {
+ country: null,
+ bridgesList: [],
+ };
+ }
+ },
+
+ /**
+ * Confirm that a bootstrapping can take place, and whether the given values
+ * are valid.
+ *
+ * @param {string} [regionCode] - The region code passed in.
+ *
+ * @returns {boolean} whether bootstrapping can proceed.
+ */
+ _confirmBootstrapping(regionCode) {
+ this._ensureEnabled();
+
+ if (this._bootstrapAttempt) {
+ lazy.logger.warn(
+ "Already have an ongoing bootstrap attempt." +
+ ` Ignoring request with ${regionCode}.`
+ );
+ return false;
+ }
+
+ const currentStage = this._stageName;
+
+ if (regionCode) {
+ if (!this.canBeginAutoBootstrap) {
+ lazy.logger.warn(
+ `Cannot begin auto bootstrap in stage ${currentStage}`
+ );
+ return false;
+ }
+ if (
+ regionCode === "automatic" &&
+ currentStage !== TorConnectStage.ChooseRegion
+ ) {
+ lazy.logger.warn("Auto bootstrap is missing an explicit regionCode");
+ return false;
+ }
+ return true;
+ }
+
+ if (!this.canBeginNormalBootstrap) {
+ lazy.logger.warn(
+ `Cannot begin normal bootstrap in stage ${currentStage}`
+ );
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Begin a bootstrap attempt.
+ *
+ * @param {string} [regionCode] - An optional region code string to use, or
+ * "automatic" to automatically determine the region. If given, will start
+ * an auto-bootstrap attempt.
+ */
+ async beginBootstrapping(regionCode) {
+ await this._beginBootstrappingInternal(regionCode, false);
+ },
+
+ /**
+ * Begin a bootstrap attempt.
+ *
+ * @param {string} [regionCode] - An optional region code string to use, or
+ * "automatic" to automatically determine the region. If given, will start
+ * an auto-bootstrap attempt.
+ * @param {boolean} isQuickstart - Whether this was triggered by the
+ * quickstart option.
+ */
+ async _beginBootstrappingInternal(regionCode, isQuickstart) {
+ lazy.logger.debug("TorConnect.beginBootstrapping()");
+
+ if (!this._confirmBootstrapping(regionCode)) {
+ return;
+ }
+
+ const beginStage = this._stageName;
+ const bootstrapOptions = { regionCode };
+ const bootstrapAttempt = regionCode
+ ? new AutoBootstrapAttempt()
+ : new BootstrapAttempt();
+
+ this._addSimulateOptions(bootstrapOptions, regionCode);
+
+ // NOTE: The only `await` in this method is for `bootstrapAttempt.run`.
+ // Moreover, we returned early if `_bootstrapAttempt` was non-`null`.
+ // Therefore, the method is effectively "locked" by `_bootstrapAttempt`, so
+ // there should only ever be one caller at a time.
+
+ if (regionCode) {
+ // Set the default to what the user chose.
+ this._defaultRegion = regionCode;
+ } else {
+ // Reset the default region to show in the UI.
+ this._defaultRegion = "automatic";
+ }
+ this._requestedStage = null;
+ this._bootstrapTrigger = beginStage;
+ this._isQuickstart = isQuickstart;
+ this._setStage(TorConnectStage.Bootstrapping);
+ this._bootstrapAttempt = bootstrapAttempt;
+
+ let error = null;
+ let result = null;
+ try {
+ result = await bootstrapAttempt.run(progress => {
+ this._bootstrappingStatus.progress = progress;
+ lazy.logger.info(`Bootstrapping ${progress}% complete`);
+ this._notifyBootstrapProgress();
+ }, bootstrapOptions);
+ } catch (err) {
+ error = err;
+ }
+
+ const requestedStage = this._requestedStage;
+ this._requestedStage = null;
+ this._bootstrapTrigger = null;
+ this._isQuickstart = false;
+ this._bootstrapAttempt = null;
+
+ if (bootstrapAttempt.detectedRegion) {
+ this._defaultRegion = bootstrapAttempt.detectedRegion;
+ }
+
+ if (result === "complete") {
+ // Reset tryAgain, potentiallyBlocked and errorDetails in case the tor
+ // process exists later on.
+ this._tryAgain = false;
+ this._potentiallyBlocked = false;
+ this._errorDetails = null;
+ // Re-enable quickstart for future sessions.
+ Services.prefs.setBoolPref(TorConnectPrefs.prompt_at_startup, false);
+
+ if (requestedStage) {
+ lazy.logger.warn(
+ `Ignoring ${requestedStage} request since we are bootstrapped`
+ );
+ }
+ this._setStage(TorConnectStage.Bootstrapped);
+ Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete);
+
+ // Now call the postBootstrapped method. We do this after changing the
+ // stage to ensure that AutoBootstrapAttempt.postBootstrapped call to
+ // saveTemporaryBridges does not trigger SettingsChanged too early and
+ // cancel itself.
+ bootstrapAttempt.postBootstrapped();
+ return;
+ }
+
+ if (requestedStage) {
+ lazy.logger.debug("Ignoring bootstrap result", result, error);
+ this._setStage(requestedStage);
+ return;
+ }
+
+ if (error) {
+ lazy.logger.info("Bootstrap attempt error", error);
+ this._tryAgain = true;
+
+ if (error instanceof lazy.TorProviderInitError) {
+ // Treat like TorProviderTopics.ProcessExited. We expect a user
+ // notification when this happens.
+ // Treat a failure as a possibly broken configuration.
+ // So, prevent quickstart at the next start.
+ Services.prefs.setBoolPref(TorConnectPrefs.prompt_at_startup, true);
+ lazy.logger.info(
+ "Starting again since the tor provider failed to initialise"
+ );
+ this._setStage(TorConnectStage.Start);
+ return;
+ }
+
+ if (
+ (beginStage === TorConnectStage.Start ||
+ beginStage === TorConnectStage.Offline) &&
+ (this.internetStatus === InternetStatus.Offline || this.simulateOffline)
+ ) {
+ // If we are currently offline, we assume the bootstrap error is caused
+ // by a general internet connection problem, so we show an "Offline"
+ // stage instead.
+ // NOTE: In principle, we may determine that we are offline prior to the
+ // bootstrap being thrown, but we do not want to cancel a bootstrap
+ // attempt prematurely in case the offline state is intermittent or
+ // incorrect.
+ this._signalError(new TorConnectError(TorConnectError.Offline));
+ this._setStage(TorConnectStage.Offline);
+ return;
+ }
+
+ this._potentiallyBlocked = true;
+ // Disable quickstart until we have a successful bootstrap.
+ Services.prefs.setBoolPref(TorConnectPrefs.prompt_at_startup, true);
+
+ this._signalError(error);
+
+ let errorStage = TorConnectStage.FinalError;
+
+ switch (beginStage) {
+ case TorConnectStage.Start:
+ case TorConnectStage.Offline:
+ if (error instanceof lazy.TorBootstrapError) {
+ errorStage = TorConnectStage.ChooseRegion;
+ }
+ // Else, some other unexpected error type. Skip straight to the
+ // "FinalError". See tor-browser#43488.
+ break;
+ case TorConnectStage.ChooseRegion:
+ // TODO: Handle a Moat error of the type
+ // DomainFrontRequestNetworkError to show a different stage. See
+ // tor-browser#43569.
+ if (
+ regionCode === "automatic" &&
+ error instanceof TorConnectError &&
+ (error.code === TorConnectError.AllSettingsFailed ||
+ error.code === TorConnectError.CannotDetermineCountry ||
+ error.code === TorConnectError.NoSettingsForCountry)
+ ) {
+ // The automatic region failed.
+ errorStage = bootstrapAttempt.detectedRegion
+ ? TorConnectStage.ConfirmRegion
+ : TorConnectStage.RegionNotFound;
+ }
+ // Else, not automatic. Go straight to the final error since the user
+ // is unlikely to succeed re-selecting the same region and it would be
+ // unexpected for the user to select a different region.
+ // See tor-browser#42550.
+ // Else, some other error type. Skip straight to the "FinalError". See
+ // tor-browser#43488.
+ break;
+ }
+ this._setStage(errorStage);
+ return;
+ }
+
+ // Bootstrap was cancelled.
+ if (result !== "cancelled") {
+ lazy.logger.error(`Unexpected bootstrap result`, result);
+ }
+
+ // TODO: Remove this Offline hack when pages use "stage".
+ if (beginStage === TorConnectStage.Offline) {
+ // Re-send the "Offline" error to push the pages back to "Offline".
+ this._signalError(new TorConnectError(TorConnectError.Offline));
+ }
+
+ // Return to the previous stage.
+ this._setStage(beginStage);
+ },
+
+ /**
+ * Cancel an ongoing bootstrap attempt.
+ */
+ cancelBootstrapping() {
+ lazy.logger.debug("TorConnect.cancelBootstrapping()");
+
+ this._ensureEnabled();
+
+ if (!this._bootstrapAttempt) {
+ lazy.logger.warn("No bootstrap attempt to cancel");
+ return;
+ }
+
+ this._bootstrapAttempt.cancel();
+ },
+
+ /**
+ * Request the transition to the given stage.
+ *
+ * If we are bootstrapping, it will be cancelled and the stage will be
+ * transitioned to when it resolves. Otherwise, we will switch to the stage
+ * immediately.
+ *
+ * @param {string} stage - The stage to request.
+ * @param {boolean} [overrideBootstrapped=false] - Whether the request can
+ * override the "Bootstrapped" stage.
+ */
+ _makeStageRequest(stage, overrideBootstrapped = false) {
+ lazy.logger.debug(`Request for stage ${stage}`);
+
+ this._ensureEnabled();
+
+ if (stage === this._stageName) {
+ lazy.logger.info(`Ignoring request for current stage ${stage}`);
+ return;
+ }
+ if (
+ !overrideBootstrapped &&
+ this._stageName === TorConnectStage.Bootstrapped
+ ) {
+ lazy.logger.warn(`Cannot move to ${stage} when bootstrapped`);
+ return;
+ }
+ if (this._stageName === TorConnectStage.Loading) {
+ if (stage === TorConnectStage.Start) {
+ // Will transition to "Start" stage when loading completes.
+ lazy.logger.info("Still in the Loading stage");
+ } else {
+ lazy.logger.warn(`Cannot move to ${stage} when Loading`);
+ }
+ return;
+ }
+
+ if (!this._bootstrapAttempt) {
+ // Transition immediately.
+ this._setStage(stage);
+ return;
+ }
+
+ if (this._requestedStage === stage) {
+ lazy.logger.info(`Already requesting stage ${stage}`);
+ return;
+ }
+ if (this._requestedStage) {
+ lazy.logger.warn(
+ `Overriding request for ${this._requestedStage} with ${stage}`
+ );
+ }
+ // Move to stage *after* bootstrap completes.
+ this._requestedStage = stage;
+ this._bootstrapAttempt?.cancel();
+ },
+
+ /**
+ * Restart the TorConnect stage to the start.
+ */
+ startAgain() {
+ this._makeStageRequest(TorConnectStage.Start);
+ },
+
+ /**
+ * Set the stage to be "ChooseRegion".
+ */
+ chooseRegion() {
+ if (!this._potentiallyBlocked) {
+ lazy.logger.error("chooseRegion request before getting an error");
+ return;
+ }
+ // NOTE: The ChooseRegion stage needs _errorDetails to be displayed in
+ // about:torconnect. The _potentiallyBlocked condition should be
+ // sufficient to ensure this.
+ this._makeStageRequest(TorConnectStage.ChooseRegion);
+ },
+
+ /**
+ * Get the list of regions that Moat has settings for.
+ *
+ * @returns {string[]} - The list of region codes.
+ */
+ async getFrequentRegions() {
+ return this._moatRegionsPromise ?? [];
+ },
+};
diff --git a/toolkit/modules/TorSettings.sys.mjs b/toolkit/modules/TorSettings.sys.mjs
@@ -0,0 +1,1570 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
+ Lox: "resource://gre/modules/Lox.sys.mjs",
+ LoxTopics: "resource://gre/modules/Lox.sys.mjs",
+ TorParsers: "resource://gre/modules/TorParsers.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () => {
+ return console.createInstance({
+ maxLogLevelPref: "browser.torsettings.log_level",
+ prefix: "TorSettings",
+ });
+});
+
+/* TorSettings observer topics */
+export const TorSettingsTopics = Object.freeze({
+ Ready: "torsettings:ready",
+ SettingsChanged: "torsettings:settings-changed",
+ ApplyError: "torsettings:apply-error",
+});
+
+/* Prefs used to store settings in TorBrowser prefs */
+const TorSettingsPrefs = Object.freeze({
+ // NOTE: torbrowser.settings.quickstart.enabled used to be managed by
+ // TorSettings but was moved to TorConnect.quickstart in tor-browser#41921.
+ bridges: {
+ /* bool: does tor use bridges */
+ enabled: "torbrowser.settings.bridges.enabled",
+ /* int: See TorBridgeSource */
+ source: "torbrowser.settings.bridges.source",
+ /* string: output of crypto.randomUUID() */
+ lox_id: "torbrowser.settings.bridges.lox_id",
+ /* string: obfs4|meek|snowflake|etc */
+ builtin_type: "torbrowser.settings.bridges.builtin_type",
+ /* preference branch: each child branch should be a bridge string */
+ bridge_strings: "torbrowser.settings.bridges.bridge_strings",
+ },
+ proxy: {
+ /* bool: does tor use a proxy */
+ enabled: "torbrowser.settings.proxy.enabled",
+ /* See TorProxyType */
+ type: "torbrowser.settings.proxy.type",
+ /* string: proxy server address */
+ address: "torbrowser.settings.proxy.address",
+ /* int: [1,65535], proxy port */
+ port: "torbrowser.settings.proxy.port",
+ /* string: username */
+ username: "torbrowser.settings.proxy.username",
+ /* string: password */
+ password: "torbrowser.settings.proxy.password",
+ },
+ firewall: {
+ /* bool: does tor have a port allow list */
+ enabled: "torbrowser.settings.firewall.enabled",
+ /* string: comma-delimitted list of port numbers */
+ allowed_ports: "torbrowser.settings.firewall.allowed_ports",
+ },
+});
+
+export const TorBridgeSource = Object.freeze({
+ Invalid: -1,
+ BuiltIn: 0,
+ BridgeDB: 1,
+ UserProvided: 2,
+ Lox: 3,
+});
+
+export const TorProxyType = Object.freeze({
+ Invalid: -1,
+ Socks4: 0,
+ Socks5: 1,
+ HTTPS: 2,
+});
+
+/**
+ * Split a blob of bridge lines into an array with single lines.
+ * Lines are delimited by \r\n or \n and each bridge string can also optionally
+ * have 'bridge' at the beginning.
+ * We split the text by \r\n, we trim the lines, remove the bridge prefix.
+ *
+ * @param {string} bridgeLines The text with the lines
+ * @returns {string[]} An array where each bridge line is an item
+ */
+function splitBridgeLines(bridgeLines) {
+ // Split on the newline and for each bridge string: trim, remove starting
+ // 'bridge' string.
+ // Replace whitespace with standard " ".
+ // NOTE: We only remove the bridge string part if it is followed by a
+ // non-whitespace.
+ return bridgeLines.split(/\r?\n/).map(val =>
+ val
+ .trim()
+ .replace(/^bridge\s+(\S)/i, "$1")
+ .replace(/\s+/, " ")
+ );
+}
+
+/**
+ * @typedef {object} BridgeValidationResult
+ *
+ * @property {integer[]} errorLines - The lines that contain errors. Counting
+ * from 1.
+ * @property {boolean} empty - Whether the given string contains no bridges.
+ * @property {string[]} validBridges - The valid bridge lines found.
+ */
+/**
+ * Validate the given bridge lines.
+ *
+ * @param {string} bridgeLines - The bridge lines to validate, separated by
+ * newlines.
+ *
+ * @returns {BridgeValidationResult}
+ */
+export function validateBridgeLines(bridgeLines) {
+ let empty = true;
+ const errorLines = [];
+ const validBridges = [];
+ for (const [index, bridge] of splitBridgeLines(bridgeLines).entries()) {
+ if (!bridge) {
+ // Empty line.
+ continue;
+ }
+ empty = false;
+ try {
+ // TODO: Have a more comprehensive validation parser.
+ lazy.TorParsers.parseBridgeLine(bridge);
+ } catch {
+ errorLines.push(index + 1);
+ continue;
+ }
+ validBridges.push(bridge);
+ }
+ return { empty, errorLines, validBridges };
+}
+
+/**
+ * Return a shuffled (Fisher-Yates) copy of an array.
+ *
+ * @template T
+ * @param {T[]} array
+ * @returns {T[]}
+ */
+function arrayShuffle(array) {
+ array = [...array];
+ for (let i = array.length - 1; i > 0; --i) {
+ // number n such that 0.0 <= n < 1.0
+ const n = Math.random();
+ // integer j such that 0 <= j <= i
+ const j = Math.floor(n * (i + 1));
+
+ // swap values at indices i and j
+ const tmp = array[i];
+ array[i] = array[j];
+ array[j] = tmp;
+ }
+ return array;
+}
+
+/**
+ * @typedef {object} TorBridgeSettings
+ *
+ * Represents the Tor bridge settings.
+ *
+ * @property {boolean} enabled - Whether bridges are enabled.
+ * @property {integer} source - The source of bridges. One of the values in
+ * `TorBridgeSource`.
+ * @property {string} lox_id - The ID of the lox credentials to get bridges for.
+ * Or "" if not using a `Lox` `source`.
+ * @property {string} builtin_type - The name of the built-in bridge type. Or ""
+ * if not using a `BuiltIn` `source`.
+ * @property {string[]} bridge_strings - The bridge lines to be passed to the
+ * provider. Should be empty if and only if the `source` is `Invalid`.
+ */
+
+/**
+ * @typedef {object} TorProxySettings
+ *
+ * Represents the Tor proxy settings.
+ *
+ * @property {boolean} enabled - Whether the proxy should be enabled.
+ * @property {integer} type - The proxy type. One of the values in
+ * `TorProxyType`.
+ * @property {string} address - The proxy address, or "" if the proxy is not
+ * being used.
+ * @property {integer} port - The proxy port, or 0 if the proxy is not being
+ * used.
+ * @property {string} username - The proxy user name, or "" if this is not
+ * needed.
+ * @property {string} password - The proxy password, or "" if this is not
+ * needed.
+ */
+
+/**
+ * @typedef {object} TorFirewallSettings
+ *
+ * Represents the Tor firewall settings.
+ *
+ * @property {boolean} enabled - Whether the firewall settings should be
+ * enabled.
+ * @property {integer[]} allowed_ports - The list of ports that are allowed.
+ */
+
+/**
+ * @typedef {object} TorCombinedSettings
+ *
+ * A combination of Tor settings.
+ *
+ * @property {TorBridgeSettings} bridges - The bridge settings.
+ * @property {TorProxySettings} proxy - The proxy settings.
+ * @property {TorFirewallSettings} firewall - The firewall settings.
+ */
+
+/* TorSettings module */
+
+/**
+ * The implementation for the global `TorSettings` object.
+ */
+class TorSettingsImpl {
+ /**
+ * The default settings to use.
+ *
+ * @type {TorCombinedSettings}
+ */
+ #defaultSettings = {
+ bridges: {
+ enabled: false,
+ source: TorBridgeSource.Invalid,
+ lox_id: "",
+ builtin_type: "",
+ bridge_strings: [],
+ },
+ proxy: {
+ enabled: false,
+ type: TorProxyType.Invalid,
+ address: "",
+ port: 0,
+ username: "",
+ password: "",
+ },
+ firewall: {
+ enabled: false,
+ allowed_ports: [],
+ },
+ };
+
+ /**
+ * The underlying settings values.
+ *
+ * @type {TorCombinedSettings}
+ */
+ #settings = structuredClone(this.#defaultSettings);
+
+ /**
+ * The last successfully applied settings for the current `TorProvider`, if
+ * any.
+ *
+ * NOTE: Should only be written to within `#applySettings`.
+ *
+ * @type {{
+ * bridges: ?TorBridgeSettings,
+ * proxy: ?TorProxySettings,
+ * firewall: ?TorFirewallSettings
+ * }}
+ */
+ #successfulSettings = { bridges: null, proxy: null, firewall: null };
+
+ /**
+ * Whether temporary bridge settings have been applied to the current
+ * `TorProvider`.
+ *
+ * @type {boolean}
+ */
+ #temporaryBridgesApplied = false;
+
+ /**
+ * @typedef {TorSettingsApplyError}
+ *
+ * @property {boolean} canUndo - Whether the latest error can be "undone".
+ * When this is `false`, the TorProvider will be using its default values
+ * instead.
+ */
+
+ /**
+ * A summary of the latest failures to apply our settings, if any.
+ *
+ * NOTE: Should only be written to within `#applySettings`.
+ *
+ * @type {{
+ * bridges: ?TorSettingsApplyError,
+ * proxy: ?TorSettingsApplyError,
+ * firewall: ?TorSettingsApplyError,
+ * }}
+ */
+ #applyErrors = { bridges: null, proxy: null, firewall: null };
+
+ /**
+ * Get the latest failure for the given setting, if any.
+ *
+ * @param {string} group - The settings to get the error details for.
+ *
+ * @returns {?TorSettingsApplyError} - The error details, if any.
+ */
+ getApplyError(group) {
+ return structuredClone(this.#applyErrors[group]);
+ }
+
+ /**
+ * Temporary bridge settings to apply instead of #settings.bridges.
+ *
+ * @type {?TorBridgeSettings}
+ */
+ #temporaryBridgeSettings = null;
+
+ /**
+ * The recommended pluggable transport.
+ *
+ * @type {string}
+ */
+ #recommendedPT = "";
+
+ /**
+ * The bridge lines for built-in bridges.
+ * Keys are pluggable transports, and values are arrays of bridge lines.
+ *
+ * @type {object}
+ */
+ #builtinBridges = {};
+
+ /**
+ * A promise that resolves once we are initialized, or throws if there was an
+ * initialization error.
+ *
+ * @type {Promise}
+ */
+ #initializedPromise;
+ /**
+ * Resolve callback of the initializedPromise.
+ */
+ #initComplete;
+ /**
+ * Reject callback of the initializedPromise.
+ */
+ #initFailed;
+ /**
+ * Tell whether the initializedPromise has been resolved.
+ * We keep this additional member to avoid making everything async.
+ *
+ * @type {boolean}
+ */
+ #initialized = false;
+
+ /**
+ * Whether uninit cleanup has been called.
+ *
+ * @type {boolean}
+ */
+ #uninitCalled = false;
+
+ /**
+ * Whether Lox was initialized.
+ *
+ * @type {boolean}
+ */
+ #initializedLox = false;
+
+ /**
+ * Whether observers were initialized.
+ *
+ * @type {boolean}
+ */
+ #initializedObservers = false;
+
+ constructor() {
+ this.#initializedPromise = new Promise((resolve, reject) => {
+ this.#initComplete = resolve;
+ this.#initFailed = reject;
+ });
+
+ // Add some read-only getters for the #settings object.
+ // E.g. TorSetting.#settings.bridges.source is exposed publicly as
+ // TorSettings.bridges.source.
+ for (const groupname in this.#settings) {
+ const publicGroup = {};
+ for (const name in this.#settings[groupname]) {
+ // Public group only has a getter for the property.
+ Object.defineProperty(publicGroup, name, {
+ get: () => {
+ this.#checkIfInitialized();
+ return structuredClone(this.#settings[groupname][name]);
+ },
+ set: () => {
+ throw new Error(
+ `TorSettings.${groupname}.${name} cannot be set directly`
+ );
+ },
+ });
+ }
+ // The group object itself should not be writable.
+ Object.preventExtensions(publicGroup);
+ Object.defineProperty(this, groupname, {
+ writable: false,
+ value: publicGroup,
+ });
+ }
+ }
+
+ /**
+ * The proxy URI for the current settings, or `null` if no proxy is
+ * configured.
+ *
+ * @type {?string}
+ */
+ get proxyUri() {
+ const { type, address, port, username, password } = this.#settings.proxy;
+ switch (type) {
+ case TorProxyType.Socks4:
+ return `socks4a://${address}:${port}`;
+ case TorProxyType.Socks5:
+ if (username) {
+ return `socks5://${username}:${password}@${address}:${port}`;
+ }
+ return `socks5://${address}:${port}`;
+ case TorProxyType.HTTPS:
+ if (username) {
+ return `http://${username}:${password}@${address}:${port}`;
+ }
+ return `http://${address}:${port}`;
+ }
+ return null;
+ }
+
+ /**
+ * Verify a port number is within bounds.
+ *
+ * @param {integer} val - The value to verify.
+ * @returns {boolean} - Whether the port is within range.
+ */
+ validPort(val) {
+ return Number.isInteger(val) && val >= 1 && val <= 65535;
+ }
+
+ /**
+ * Verify that some SOCKS5 credentials are valid.
+ *
+ * @param {string} username - The SOCKS5 username.
+ * @param {string} password - The SOCKS5 password.
+ * @returns {boolean} - Whether the credentials are valid.
+ */
+ validSocks5Credentials(username, password) {
+ if (!username && !password) {
+ // Both empty is valid.
+ return true;
+ }
+ for (const val of [username, password]) {
+ if (typeof val !== "string") {
+ return false;
+ }
+ const byteLen = new TextEncoder().encode(val).length;
+ if (byteLen < 1 || byteLen > 255) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Test whether two arrays have equal members and order.
+ *
+ * @param {Array} val1 - The first array to test.
+ * @param {Array} val2 - The second array to compare against.
+ *
+ * @returns {boolean} - Whether the two arrays are equal.
+ */
+ #arrayEqual(val1, val2) {
+ if (val1.length !== val2.length) {
+ return false;
+ }
+ return val1.every((v, i) => v === val2[i]);
+ }
+
+ /**
+ * Return the bridge lines associated to a certain pluggable transport.
+ *
+ * @param {string} pt The pluggable transport to return the lines for
+ * @returns {string[]} The bridge lines in random order
+ */
+ #getBuiltinBridges(pt) {
+ return this.#builtinBridges[pt] ?? [];
+ }
+
+ /**
+ * Whether this module is enabled.
+ *
+ * @type {boolean}
+ */
+ get enabled() {
+ return lazy.TorLauncherUtil.shouldStartAndOwnTor;
+ }
+
+ /**
+ * Load or init our settings.
+ */
+ async init() {
+ if (this.#initialized) {
+ lazy.logger.warn("Called init twice.");
+ await this.#initializedPromise;
+ return;
+ }
+ try {
+ await this.#initInternal();
+ this.#initialized = true;
+ this.#initComplete();
+ Services.obs.notifyObservers(null, TorSettingsTopics.Ready);
+ } catch (e) {
+ this.#initFailed(e);
+ throw e;
+ }
+ }
+
+ /**
+ * The actual implementation of the initialization, which is wrapped to make
+ * it easier to update initializatedPromise.
+ */
+ async #initInternal() {
+ if (!this.enabled || this.#uninitCalled) {
+ // Nothing to do.
+ return;
+ }
+
+ try {
+ const req = await fetch("chrome://global/content/pt_config.json");
+ const config = await req.json();
+ lazy.logger.debug("Loaded pt_config.json", config);
+ if ("meek-azure" in config.bridges) {
+ // Convert the meek-azure name to meek. tor-browser#44068.
+ // NOTE: no need to convert recommendedDefault since it is not meek.
+ lazy.logger.debug("Converting pt_config type from meek-azure to meek");
+ config.bridges.meek = config.bridges["meek-azure"];
+ delete config.bridges["meek-azure"];
+ }
+ this.#recommendedPT = config.recommendedDefault;
+ this.#builtinBridges = config.bridges;
+ for (const type in this.#builtinBridges) {
+ // Shuffle so that Tor Browser users do not all try the built-in bridges
+ // in the same order.
+ // Only do this once per session. In particular, we don't re-shuffle if
+ // changeSettings is called with the same bridges.builtin_type value.
+ this.#builtinBridges[type] = arrayShuffle(this.#builtinBridges[type]);
+ }
+ } catch (e) {
+ lazy.logger.error("Could not load the built-in PT config.", e);
+ }
+
+ // `uninit` may have been called whilst we awaited pt_config.
+ if (this.#uninitCalled) {
+ lazy.logger.warn("unint was called before init completed.");
+ return;
+ }
+
+ // Initialize this before loading from prefs because we need Lox initialized
+ // before any calls to Lox.getBridges().
+ if (!lazy.TorLauncherUtil.isAndroid) {
+ try {
+ // Set as initialized before calling to ensure it is cleaned up by our
+ // `uninit` method.
+ this.#initializedLox = true;
+ await lazy.Lox.init();
+ } catch (e) {
+ lazy.logger.error("Could not initialize Lox.", e);
+ }
+ }
+
+ // `uninit` may have been called whilst we awaited Lox.init.
+ if (this.#uninitCalled) {
+ lazy.logger.warn("unint was called before init completed.");
+ return;
+ }
+
+ this.#loadFromPrefs();
+ // We do not pass on the loaded settings to the TorProvider yet. Instead
+ // TorProvider will ask for these once it has initialised.
+
+ Services.obs.addObserver(this, lazy.LoxTopics.UpdateBridges);
+ this.#initializedObservers = true;
+
+ lazy.logger.info("Ready");
+ }
+
+ /**
+ * Unload or uninit our settings.
+ */
+ async uninit() {
+ if (this.#uninitCalled) {
+ lazy.logger.warn("Called uninit twice");
+ return;
+ }
+
+ this.#uninitCalled = true;
+ // NOTE: We do not reset #initialized to false because we want it to remain
+ // in place for external callers, and we do not want `#initInternal` to be
+ // re-entered.
+
+ if (this.#initializedObservers) {
+ Services.obs.removeObserver(this, lazy.LoxTopics.UpdateBridges);
+ }
+ if (this.#initializedLox) {
+ await lazy.Lox.uninit();
+ }
+ }
+
+ observe(subject, topic) {
+ switch (topic) {
+ case lazy.LoxTopics.UpdateBridges:
+ if (
+ this.#settings.bridges.lox_id &&
+ this.#settings.bridges.source === TorBridgeSource.Lox
+ ) {
+ // Re-trigger the call to lazy.Lox.getBridges.
+ // FIXME: This can cancel a bootstrap. tor-browser#43991.
+ this.changeSettings({
+ bridges: {
+ source: TorBridgeSource.Lox,
+ lox_id: this.#settings.bridges.lox_id,
+ },
+ });
+ }
+ break;
+ }
+ }
+
+ /**
+ * Check whether the module is enabled and successfully initialized, and throw
+ * if it is not.
+ */
+ #checkIfInitialized() {
+ if (!this.enabled) {
+ throw new Error("TorSettings is not enabled");
+ }
+ if (!this.#initialized) {
+ lazy.logger.trace("Not initialized code path.");
+ throw new Error(
+ "TorSettings has not been initialized yet, or its initialization failed"
+ );
+ }
+ }
+
+ /**
+ * Tell whether TorSettings has been successfully initialized.
+ *
+ * @returns {boolean}
+ */
+ get initialized() {
+ return this.#initialized;
+ }
+
+ /**
+ * A promise that resolves once we are initialized, or throws if there was an
+ * initialization error.
+ *
+ * @type {Promise}
+ */
+ get initializedPromise() {
+ return this.#initializedPromise;
+ }
+
+ /**
+ * Load our settings from prefs.
+ */
+ #loadFromPrefs() {
+ lazy.logger.debug("loadFromPrefs()");
+
+ /* Bridges */
+ const bridges = {};
+ bridges.enabled = Services.prefs.getBoolPref(
+ TorSettingsPrefs.bridges.enabled,
+ false
+ );
+ bridges.source = Services.prefs.getIntPref(
+ TorSettingsPrefs.bridges.source,
+ TorBridgeSource.Invalid
+ );
+ switch (bridges.source) {
+ case TorBridgeSource.BridgeDB:
+ case TorBridgeSource.UserProvided:
+ bridges.bridge_strings = Services.prefs
+ .getBranch(TorSettingsPrefs.bridges.bridge_strings)
+ .getChildList("")
+ .map(pref =>
+ Services.prefs.getStringPref(
+ `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
+ )
+ );
+ break;
+ case TorBridgeSource.BuiltIn: {
+ // bridge_strings is set via builtin_type.
+ let builtinType = Services.prefs.getStringPref(
+ TorSettingsPrefs.bridges.builtin_type,
+ ""
+ );
+ if (builtinType === "meek-azure") {
+ lazy.logger.debug(
+ "Converting builtin-bridge setting value from meek-azure to meek"
+ );
+ builtinType = "meek";
+ // Store the new value.
+ Services.prefs.setStringPref(
+ TorSettingsPrefs.bridges.builtin_type,
+ builtinType
+ );
+ }
+ bridges.builtin_type = builtinType;
+ break;
+ }
+ case TorBridgeSource.Lox:
+ // bridge_strings is set via lox id.
+ bridges.lox_id = Services.prefs.getStringPref(
+ TorSettingsPrefs.bridges.lox_id,
+ ""
+ );
+ break;
+ }
+ try {
+ this.#fixupBridgeSettings(bridges);
+ this.#settings.bridges = bridges;
+ } catch (error) {
+ lazy.logger.error("Loaded bridge preferences failed", error);
+ // Keep the default #settings.bridges.
+ }
+
+ /* Proxy */
+ const proxy = {};
+ proxy.enabled = Services.prefs.getBoolPref(
+ TorSettingsPrefs.proxy.enabled,
+ false
+ );
+ if (proxy.enabled) {
+ proxy.type = Services.prefs.getIntPref(
+ TorSettingsPrefs.proxy.type,
+ TorProxyType.Invalid
+ );
+ proxy.address = Services.prefs.getStringPref(
+ TorSettingsPrefs.proxy.address,
+ ""
+ );
+ proxy.port = Services.prefs.getIntPref(TorSettingsPrefs.proxy.port, 0);
+ proxy.username = Services.prefs.getStringPref(
+ TorSettingsPrefs.proxy.username,
+ ""
+ );
+ proxy.password = Services.prefs.getStringPref(
+ TorSettingsPrefs.proxy.password,
+ ""
+ );
+ }
+ try {
+ this.#fixupProxySettings(proxy);
+ this.#settings.proxy = proxy;
+ } catch (error) {
+ lazy.logger.error("Loaded proxy preferences failed", error);
+ // Keep the default #settings.proxy.
+ }
+
+ /* Firewall */
+ const firewall = {};
+ firewall.enabled = Services.prefs.getBoolPref(
+ TorSettingsPrefs.firewall.enabled,
+ false
+ );
+ if (firewall.enabled) {
+ firewall.allowed_ports = Services.prefs
+ .getStringPref(TorSettingsPrefs.firewall.allowed_ports, "")
+ .split(",")
+ .filter(p => p.trim())
+ .map(p => parseInt(p, 10));
+ }
+ try {
+ this.#fixupFirewallSettings(firewall);
+ this.#settings.firewall = firewall;
+ } catch (error) {
+ lazy.logger.error("Loaded firewall preferences failed", error);
+ // Keep the default #settings.firewall.
+ }
+ }
+
+ /**
+ * Save our bridge settings.
+ */
+ #saveBridgeSettings() {
+ lazy.logger.debug("Saving bridge settings");
+
+ Services.prefs.setBoolPref(
+ TorSettingsPrefs.bridges.enabled,
+ this.#settings.bridges.enabled
+ );
+ Services.prefs.setIntPref(
+ TorSettingsPrefs.bridges.source,
+ this.#settings.bridges.source
+ );
+ Services.prefs.setStringPref(
+ TorSettingsPrefs.bridges.builtin_type,
+ this.#settings.bridges.builtin_type
+ );
+ Services.prefs.setStringPref(
+ TorSettingsPrefs.bridges.lox_id,
+ this.#settings.bridges.lox_id
+ );
+ // erase existing bridge strings
+ const bridgeBranchPrefs = Services.prefs
+ .getBranch(TorSettingsPrefs.bridges.bridge_strings)
+ .getChildList("");
+ bridgeBranchPrefs.forEach(pref => {
+ Services.prefs.clearUserPref(
+ `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
+ );
+ });
+ // write new ones
+ if (
+ this.#settings.bridges.source !== TorBridgeSource.Lox &&
+ this.#settings.bridges.source !== TorBridgeSource.BuiltIn
+ ) {
+ this.#settings.bridges.bridge_strings.forEach((string, index) => {
+ Services.prefs.setStringPref(
+ `${TorSettingsPrefs.bridges.bridge_strings}.${index}`,
+ string
+ );
+ });
+ }
+ }
+
+ /**
+ * Save our proxy settings.
+ */
+ #saveProxySettings() {
+ lazy.logger.debug("Saving proxy settings");
+
+ Services.prefs.setBoolPref(
+ TorSettingsPrefs.proxy.enabled,
+ this.#settings.proxy.enabled
+ );
+ if (this.#settings.proxy.enabled) {
+ Services.prefs.setIntPref(
+ TorSettingsPrefs.proxy.type,
+ this.#settings.proxy.type
+ );
+ Services.prefs.setStringPref(
+ TorSettingsPrefs.proxy.address,
+ this.#settings.proxy.address
+ );
+ Services.prefs.setIntPref(
+ TorSettingsPrefs.proxy.port,
+ this.#settings.proxy.port
+ );
+ Services.prefs.setStringPref(
+ TorSettingsPrefs.proxy.username,
+ this.#settings.proxy.username
+ );
+ Services.prefs.setStringPref(
+ TorSettingsPrefs.proxy.password,
+ this.#settings.proxy.password
+ );
+ } else {
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.type);
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.address);
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.port);
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.username);
+ Services.prefs.clearUserPref(TorSettingsPrefs.proxy.password);
+ }
+ }
+
+ /**
+ * Save our firewall settings.
+ */
+ #saveFirewallSettings() {
+ lazy.logger.debug("Saving firewall settings");
+
+ Services.prefs.setBoolPref(
+ TorSettingsPrefs.firewall.enabled,
+ this.#settings.firewall.enabled
+ );
+ if (this.#settings.firewall.enabled) {
+ Services.prefs.setStringPref(
+ TorSettingsPrefs.firewall.allowed_ports,
+ this.#settings.firewall.allowed_ports.join(",")
+ );
+ } else {
+ Services.prefs.clearUserPref(TorSettingsPrefs.firewall.allowed_ports);
+ }
+ }
+
+ /**
+ * A blocker promise for the #applySettings method.
+ *
+ * Ensures only one active caller to protect the #applyErrors and
+ * #successfulSettings properties.
+ *
+ * @type {?Promise}
+ */
+ #applySettingsTask = null;
+
+ /**
+ * Push our settings down to the tor provider.
+ *
+ * Even though this introduces a circular depdency, it makes the API nicer for
+ * frontend consumers.
+ *
+ * @param {object} apply - The list of settings to apply.
+ * @param {boolean} [apply.bridges] - Whether to apply our bridge settings.
+ * @param {boolean} [apply.proxy] - Whether to apply our proxy settings.
+ * @param {boolean} [apply.firewall] - Whether to apply our firewall settings.
+ * @param {boolean} [details] - Optional details.
+ * @param {boolean} [details.useTemporaryBridges] - Whether the caller wants
+ * to apply temporary bridges.
+ * @param {boolean} [details.newProvider] - Whether the caller is initialising
+ * a new `TorProvider`.
+ */
+ async #applySettings(apply, details) {
+ // Grab this provider before awaiting.
+ // In particular, if the provider is changed we do not want to switch to
+ // writing to the new instance.
+ const providerRef = this.#providerRef;
+ const provider = providerRef?.deref();
+ if (!provider) {
+ // Wait until setTorProvider is called.
+ lazy.logger.info("No TorProvider yet");
+ return;
+ }
+
+ // We only want one instance of #applySettings to be running at any given
+ // time, so we await the previous call first.
+ // Read and replace #applySettingsTask before we do any async operations.
+ // I.e. this is effectively an atomic read and replace.
+ const prevTask = this.#applySettingsTask;
+ let taskComplete;
+ ({ promise: this.#applySettingsTask, resolve: taskComplete } =
+ Promise.withResolvers());
+ await prevTask;
+
+ try {
+ let flush = false;
+ const errors = [];
+
+ // Test whether the provider is no longer running or has been replaced.
+ const providerRunning = () => {
+ return providerRef === this.#providerRef && provider.isRunning;
+ };
+
+ lazy.logger.debug("Passing on settings to the provider", apply, details);
+
+ if (details?.newProvider) {
+ // If we have a new provider we clear our successful settings.
+ // In particular, the user may have changed their settings several times
+ // whilst the tor process was not running. In the event of an
+ // "ApplyError", we want to correctly show to the user that they are now
+ // using default settings and we do not want to allow them to "undo"
+ // since the previous successful settings may be out of date.
+ // NOTE: We do not do this within `setTorProvider` since some other
+ // caller's `#applySettingsTask` may still be running and writing to
+ // these values when `setTorProvider` is called.
+ this.#successfulSettings.bridges = null;
+ this.#successfulSettings.proxy = null;
+ this.#successfulSettings.firewall = null;
+ this.#applyErrors.bridges = null;
+ this.#applyErrors.proxy = null;
+ this.#applyErrors.firewall = null;
+ // Temporary bridges are not applied to the new provider.
+ this.#temporaryBridgesApplied = false;
+ }
+
+ for (const group of ["bridges", "proxy", "firewall"]) {
+ if (!apply[group]) {
+ continue;
+ }
+
+ if (!providerRunning()) {
+ lazy.logger.info("The TorProvider is no longer running");
+ // Bail on this task since the old provider should not accept
+ // settings. setTorProvider will be called for the new provider and
+ // will already handle applying the same settings.
+ return;
+ }
+
+ let usingSettings = true;
+ if (group === "bridges") {
+ // Only record successes or failures when using the user settings.
+ if (this.#temporaryBridgeSettings && !details?.useTemporaryBridges) {
+ // #temporaryBridgeSettings were written whilst awaiting the
+ // previous task. Do nothing and allow applyTemporarySettings to
+ // apply the temporary bridges instead.
+ lazy.logger.info(
+ "Not apply bridges since temporary bridges were applied"
+ );
+ continue;
+ }
+ if (!this.#temporaryBridgeSettings && details?.useTemporaryBridges) {
+ // #temporaryBridgeSettings were cleared whilst awaiting the
+ // previous task. Do nothing and allow changeSettings or
+ // clearTemporaryBridges to apply the non-temporary bridges
+ // instead.
+ lazy.logger.info(
+ "Not apply temporary bridges since they were cleared"
+ );
+ continue;
+ }
+ usingSettings = !details?.useTemporaryBridges;
+ }
+
+ try {
+ switch (group) {
+ case "bridges": {
+ const bridges = structuredClone(
+ usingSettings
+ ? this.#settings.bridges
+ : this.#temporaryBridgeSettings
+ );
+
+ try {
+ await provider.writeBridgeSettings(bridges);
+ } catch (e) {
+ if (
+ usingSettings &&
+ this.#temporaryBridgesApplied &&
+ providerRunning()
+ ) {
+ lazy.logger.warn(
+ "Recovering to clear temporary bridges from the provider"
+ );
+ // The TorProvider is still using the temporary bridges. As a
+ // priority we want to try and restore the TorProvider to the
+ // state it was in prior to the temporary bridges being
+ // applied.
+ const prevBridges = structuredClone(
+ this.#successfulSettings.bridges ??
+ this.#defaultSettings.bridges
+ );
+ try {
+ await provider.writeBridgeSettings(prevBridges);
+ this.#temporaryBridgesApplied = false;
+ } catch (ex) {
+ lazy.logger.error(
+ "Failed to clear the temporary bridges from the provider",
+ ex
+ );
+ }
+ }
+ throw e;
+ }
+
+ if (usingSettings) {
+ this.#successfulSettings.bridges = bridges;
+ this.#temporaryBridgesApplied = false;
+ this.#applyErrors.bridges = null;
+ flush = true;
+ } else {
+ this.#temporaryBridgesApplied = true;
+ // Do not flush the temporary bridge settings until they are
+ // saved.
+ }
+ break;
+ }
+ case "proxy": {
+ const proxy = structuredClone(this.#settings.proxy);
+ await provider.writeProxySettings(proxy);
+ this.#successfulSettings.proxy = proxy;
+ this.#applyErrors.proxy = null;
+ flush = true;
+ break;
+ }
+ case "firewall": {
+ const firewall = structuredClone(this.#settings.firewall);
+ await provider.writeFirewallSettings(firewall);
+ this.#successfulSettings.firewall = firewall;
+ this.#applyErrors.firewall = null;
+ flush = true;
+ break;
+ }
+ }
+ } catch (e) {
+ // Store the error and throw later.
+ errors.push(e);
+ if (usingSettings && providerRunning()) {
+ // Record and signal the error.
+ // NOTE: We do not signal ApplyError when we fail to apply temporary
+ // bridges.
+ this.#applyErrors[group] = {
+ canUndo: Boolean(this.#successfulSettings[group]),
+ };
+ lazy.logger.debug(`Signalling new ApplyError for ${group}`);
+ Services.obs.notifyObservers(
+ { group },
+ TorSettingsTopics.ApplyError
+ );
+ }
+ }
+ }
+ if (flush && providerRunning()) {
+ provider.flushSettings();
+ }
+ if (errors.length) {
+ lazy.logger.error("Failed to apply settings", errors);
+ throw new Error(`Failed to apply settings. ${errors.join(". ")}.`);
+ }
+ } finally {
+ // Allow the next caller to proceed.
+ taskComplete();
+ }
+ }
+
+ /**
+ * Fixup the given bridges settings to fill in details, establish the correct
+ * types and clean up.
+ *
+ * May throw if there is an error in the given values.
+ *
+ * @param {object} bridges - The bridges settings to fix up.
+ */
+ #fixupBridgeSettings(bridges) {
+ if (!Object.values(TorBridgeSource).includes(bridges.source)) {
+ throw new Error(`Not a valid bridge source: "${bridges.source}"`);
+ }
+
+ if ("enabled" in bridges) {
+ bridges.enabled = Boolean(bridges.enabled);
+ }
+
+ // Set bridge_strings
+ switch (bridges.source) {
+ case TorBridgeSource.UserProvided:
+ case TorBridgeSource.BridgeDB:
+ // Only accept an Array for UserProvided and BridgeDB bridge_strings.
+ break;
+ case TorBridgeSource.BuiltIn:
+ bridges.builtin_type = String(bridges.builtin_type);
+ bridges.bridge_strings = this.#getBuiltinBridges(bridges.builtin_type);
+ break;
+ case TorBridgeSource.Lox:
+ bridges.lox_id = String(bridges.lox_id);
+ bridges.bridge_strings = lazy.Lox.getBridges(bridges.lox_id);
+ break;
+ case TorBridgeSource.Invalid:
+ bridges.bridge_strings = [];
+ break;
+ }
+
+ if (
+ !Array.isArray(bridges.bridge_strings) ||
+ bridges.bridge_strings.some(str => typeof str !== "string")
+ ) {
+ throw new Error("bridge_strings should be an Array of strings");
+ }
+
+ if (
+ bridges.source !== TorBridgeSource.Invalid &&
+ !bridges.bridge_strings?.length
+ ) {
+ throw new Error(
+ `Missing bridge_strings for bridge source ${bridges.source}`
+ );
+ }
+
+ if (bridges.source !== TorBridgeSource.BuiltIn) {
+ bridges.builtin_type = "";
+ }
+ if (bridges.source !== TorBridgeSource.Lox) {
+ bridges.lox_id = "";
+ }
+
+ if (bridges.source === TorBridgeSource.Invalid) {
+ bridges.enabled = false;
+ }
+ }
+
+ /**
+ * Fixup the given proxy settings to fill in details, establish the correct
+ * types and clean up.
+ *
+ * May throw if there is an error in the given values.
+ *
+ * @param {object} proxy - The proxy settings to fix up.
+ */
+ #fixupProxySettings(proxy) {
+ proxy.enabled = Boolean(proxy.enabled);
+ if (!proxy.enabled) {
+ proxy.type = TorProxyType.Invalid;
+ proxy.address = "";
+ proxy.port = 0;
+ proxy.username = "";
+ proxy.password = "";
+ return;
+ }
+
+ if (!Object.values(TorProxyType).includes(proxy.type)) {
+ throw new Error(`Invalid proxy type: ${proxy.type}`);
+ }
+ // Do not allow port value to be 0.
+ // Whilst Socks4Proxy, Socks5Proxy and HTTPSProxyPort allow you to pass in
+ // `<address>:0` this will select a default port. Our UI does not indicate
+ // that `0` maps to a different value, so we disallow it.
+ if (!this.validPort(proxy.port)) {
+ throw new Error(`Invalid proxy port: ${proxy.port}`);
+ }
+
+ switch (proxy.type) {
+ case TorProxyType.Socks4:
+ // Never use the username or password.
+ proxy.username = "";
+ proxy.password = "";
+ break;
+ case TorProxyType.Socks5:
+ if (!this.validSocks5Credentials(proxy.username, proxy.password)) {
+ throw new Error("Invalid SOCKS5 credentials");
+ }
+ break;
+ case TorProxyType.HTTPS:
+ // username and password are both optional.
+ break;
+ }
+
+ proxy.address = String(proxy.address);
+ proxy.username = String(proxy.username);
+ proxy.password = String(proxy.password);
+ }
+
+ /**
+ * Fixup the given firewall settings to fill in details, establish the correct
+ * types and clean up.
+ *
+ * May throw if there is an error in the given values.
+ *
+ * @param {object} firewall - The proxy settings to fix up.
+ */
+ #fixupFirewallSettings(firewall) {
+ firewall.enabled = Boolean(firewall.enabled);
+ if (!firewall.enabled) {
+ firewall.allowed_ports = [];
+ return;
+ }
+
+ if (!Array.isArray(firewall.allowed_ports)) {
+ throw new Error("allowed_ports should be an array of ports");
+ }
+ for (const port of firewall.allowed_ports) {
+ if (!this.validPort(port)) {
+ throw new Error(`Invalid firewall port: ${port}`);
+ }
+ }
+ // Remove duplicates
+ firewall.allowed_ports = [...new Set(firewall.allowed_ports)];
+ }
+
+ /**
+ * The current `TorProvider` instance we are using, if any.
+ *
+ * @type {?WeakRef<TorProvider>}
+ */
+ #providerRef = null;
+
+ /**
+ * Called whenever we have a new provider to send settings to.
+ *
+ * @param {TorProvider} provider - The provider to apply our settings to.
+ */
+ async setTorProvider(provider) {
+ lazy.logger.debug("Applying settings to new provider");
+ this.#checkIfInitialized();
+
+ // Use a WeakRef to not keep the TorProvider instance alive.
+ this.#providerRef = new WeakRef(provider);
+ // NOTE: We need the caller to pass in the TorProvider instance because
+ // TorProvider's initialisation waits for this method. In particular, we
+ // cannot await TorProviderBuilder.build since it would hang!
+ await this.#applySettings(
+ { bridges: true, proxy: true, firewall: true },
+ { newProvider: true }
+ );
+ }
+
+ /**
+ * Undo settings that have failed to be applied by restoring the last
+ * successfully applied settings instead.
+ *
+ * @param {string} group - The group to undo the settings for.
+ */
+ async undoFailedSettings(group) {
+ if (!this.#applyErrors[group]) {
+ lazy.logger.warn(
+ `${group} settings have already been successfully replaced.`
+ );
+ return;
+ }
+ if (!this.#successfulSettings[group]) {
+ // Unexpected.
+ lazy.logger.warn(
+ `Cannot undo ${group} settings since we have no successful settings.`
+ );
+ return;
+ }
+ await this.changeSettings({ [group]: this.#successfulSettings[group] });
+ }
+
+ /**
+ * Clear settings that have failed to be applied by using the default settings
+ * instead.
+ *
+ * @param {string} group - The group to clear the settings for.
+ */
+ async clearFailedSettings(group) {
+ if (!this.#applyErrors[group]) {
+ lazy.logger.warn(
+ `${group} settings have already been successfully replaced.`
+ );
+ return;
+ }
+ await this.changeSettings({ [group]: this.#defaultSettings[group] });
+ }
+
+ /**
+ * Change the Tor settings in use.
+ *
+ * It is possible to set all settings, or only some sections:
+ *
+ * + bridges.enabled can be set individually.
+ * + bridges.source can be set with a corresponding bridge specification for
+ * the source (bridge_strings, lox_id, builtin_type).
+ * + proxy settings can be set as a group.
+ * + firewall settings can be set a group.
+ *
+ * @param {object} newValues - The new setting values that should be changed.
+ * A subset of the `TorCombinedSettings` object.
+ */
+ async changeSettings(newValues) {
+ lazy.logger.debug("changeSettings()", newValues);
+ this.#checkIfInitialized();
+
+ // Make a structured clone since we change the object and may adopt some of
+ // the Array values.
+ newValues = structuredClone(newValues);
+
+ const completeSettings = structuredClone(this.#settings);
+ const changes = [];
+ const apply = {};
+
+ /**
+ * Change the given setting to a new value. Does nothing if the new value
+ * equals the old one, otherwise the change will be recorded in `changes`.
+ *
+ * @param {string} group - The group name for the property.
+ * @param {string} prop - The property name within the group.
+ * @param {any} value - The value to set.
+ * @param {Function?} equal - A method to test equality between the old and
+ * new value. Otherwise uses `===` to check equality.
+ */
+ const changeSetting = (group, prop, value, equal = null) => {
+ const currentValue = this.#settings[group][prop];
+ if (equal ? equal(currentValue, value) : currentValue === value) {
+ return;
+ }
+ completeSettings[group][prop] = value;
+ changes.push(`${group}.${prop}`);
+ // Apply these settings.
+ apply[group] = true;
+ };
+
+ if ("bridges" in newValues) {
+ if ("source" in newValues.bridges) {
+ this.#fixupBridgeSettings(newValues.bridges);
+ changeSetting("bridges", "source", newValues.bridges.source);
+ changeSetting(
+ "bridges",
+ "bridge_strings",
+ newValues.bridges.bridge_strings,
+ this.#arrayEqual
+ );
+ changeSetting("bridges", "lox_id", newValues.bridges.lox_id);
+ changeSetting(
+ "bridges",
+ "builtin_type",
+ newValues.bridges.builtin_type
+ );
+ } else if ("enabled" in newValues.bridges) {
+ // Don't need to fixup all the settings, just need to ensure that the
+ // enabled value is compatible with the current source.
+ newValues.bridges.enabled = Boolean(newValues.bridges.enabled);
+ if (
+ newValues.bridges.enabled &&
+ completeSettings.bridges.source === TorBridgeSource.Invalid
+ ) {
+ throw new Error("Cannot enable bridges without a bridge source.");
+ }
+ }
+ if ("enabled" in newValues.bridges) {
+ changeSetting("bridges", "enabled", newValues.bridges.enabled);
+ }
+
+ if (this.#temporaryBridgeSettings && apply.bridges) {
+ // A change in the bridges settings.
+ // We want to clear the temporary bridge settings to ensure that they
+ // cannot be used to overwrite these user-provided settings.
+ // See tor-browser#41921.
+ // NOTE: This should also trigger TorConnect to cancel any ongoing
+ // AutoBootstrap that would have otherwise used these settings.
+ this.#temporaryBridgeSettings = null;
+ lazy.logger.warn(
+ "Cleared temporary bridges since bridge settings were changed"
+ );
+ }
+ }
+
+ if ("proxy" in newValues) {
+ // proxy settings have to be set as a group.
+ this.#fixupProxySettings(newValues.proxy);
+ changeSetting("proxy", "enabled", Boolean(newValues.proxy.enabled));
+ changeSetting("proxy", "type", newValues.proxy.type);
+ changeSetting("proxy", "address", newValues.proxy.address);
+ changeSetting("proxy", "port", newValues.proxy.port);
+ changeSetting("proxy", "username", newValues.proxy.username);
+ changeSetting("proxy", "password", newValues.proxy.password);
+ }
+
+ if ("firewall" in newValues) {
+ // firewall settings have to be set as a group.
+ this.#fixupFirewallSettings(newValues.firewall);
+ changeSetting("firewall", "enabled", Boolean(newValues.firewall.enabled));
+ changeSetting(
+ "firewall",
+ "allowed_ports",
+ newValues.firewall.allowed_ports,
+ this.#arrayEqual
+ );
+ }
+
+ // No errors so far, so save and commit.
+ this.#settings = completeSettings;
+ // NOTE: We want to avoid overwriting saved preference values unless the
+ // user actually makes a change in their settings.
+ // In particular, if we fail to load a setting at startup due to a bug, the
+ // #settings object for that group will point to the #defaultSettings value
+ // instead. We do not want to write these #defaultSettings to the user's
+ // settings unless the user actually makes a change in one of the groups.
+ // E.g. we do not want a change in the proxy settings to overwrite the
+ // saved bridge settings. Hence, we only save the groups that have changes.
+ // See tor-browser#43766.
+ // NOTE: We could go more fine-grained and only save the preference values
+ // that actually change. E.g. only save the bridges.enabled pref when the
+ // user switches the toggle, and leave the bridges.bridge_strings as they
+ // are. However, at the time of implementation there is no known benefit to
+ // doing this, since the #defaultSettings will not allow for any changes
+ // that don't require changing the group entirely. E.g. to change
+ // bridges.enabled when starting with the #defaultSettings.bridges,
+ // bridges.bridge_strings must necessarily be set.
+ if (apply.bridges) {
+ this.#saveBridgeSettings();
+ }
+ if (apply.proxy) {
+ this.#saveProxySettings();
+ }
+ if (apply.firewall) {
+ this.#saveFirewallSettings();
+ }
+
+ if (changes.length) {
+ Services.obs.notifyObservers(
+ { changes },
+ TorSettingsTopics.SettingsChanged
+ );
+ }
+
+ lazy.logger.debug("setSettings result", this.#settings, changes);
+
+ if (apply.bridges || apply.proxy || apply.firewall) {
+ // After we have sent out the notifications for the changed settings and
+ // saved the preferences we send the new settings to TorProvider.
+ await this.#applySettings(apply);
+ }
+ }
+
+ /**
+ * Get a copy of all our settings.
+ *
+ * @returns {TorCombinedSettings} A copy of the current settings.
+ */
+ getSettings() {
+ lazy.logger.debug("getSettings()");
+ this.#checkIfInitialized();
+ return structuredClone(this.#settings);
+ }
+
+ /**
+ * Return an array with the pluggable transports for which we have at least a
+ * built-in bridge line.
+ *
+ * @returns {string[]} An array with PT identifiers
+ */
+ get builtinBridgeTypes() {
+ this.#checkIfInitialized();
+ const types = Object.keys(this.#builtinBridges);
+ const recommendedIndex = types.indexOf(this.#recommendedPT);
+ if (recommendedIndex > 0) {
+ types.splice(recommendedIndex, 1);
+ types.unshift(this.#recommendedPT);
+ }
+ return types;
+ }
+
+ /**
+ * Apply some Moat bridges temporarily.
+ *
+ * These bridges will not yet be saved to settings.
+ *
+ * @param {MoatBridges} bridges - The bridges to apply.
+ */
+ async applyTemporaryBridges(bridges) {
+ this.#checkIfInitialized();
+
+ if (
+ bridges.source !== TorBridgeSource.BuiltIn &&
+ bridges.source !== TorBridgeSource.BridgeDB
+ ) {
+ throw new Error(`Invalid bridge source ${bridges.source}`);
+ }
+
+ const bridgeSettings = {
+ enabled: true,
+ source: bridges.source,
+ builtin_type: String(bridges.builtin_type),
+ bridge_strings: structuredClone(bridges.bridge_strings),
+ };
+
+ this.#fixupBridgeSettings(bridgeSettings);
+
+ // After checks are complete, we commit them.
+ this.#temporaryBridgeSettings = bridgeSettings;
+
+ await this.#applySettings({ bridges: true }, { useTemporaryBridges: true });
+ }
+
+ /**
+ * Save to current temporary bridges to be permanent instead.
+ */
+ async saveTemporaryBridges() {
+ this.#checkIfInitialized();
+ if (!this.#temporaryBridgeSettings) {
+ lazy.logger.warn("No temporary bridges to save");
+ return;
+ }
+ const bridgeSettings = this.#temporaryBridgeSettings;
+ this.#temporaryBridgeSettings = null;
+ await this.changeSettings({ bridges: bridgeSettings });
+ }
+
+ /**
+ * Clear the current temporary bridges.
+ */
+ async clearTemporaryBridges() {
+ this.#checkIfInitialized();
+ if (!this.#temporaryBridgeSettings) {
+ lazy.logger.debug("No temporary bridges to clear");
+ return;
+ }
+ this.#temporaryBridgeSettings = null;
+ await this.#applySettings({ bridges: true });
+ }
+}
+
+export const TorSettings = new TorSettingsImpl();
diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build
@@ -155,6 +155,7 @@ EXTRA_JS_MODULES += [
"AsyncPrefs.sys.mjs",
"Bech32Decode.sys.mjs",
"BinarySearch.sys.mjs",
+ "BridgeDB.sys.mjs",
"BrowserTelemetryUtils.sys.mjs",
"BrowserUtils.sys.mjs",
"CanonicalJSON.sys.mjs",
@@ -164,6 +165,7 @@ EXTRA_JS_MODULES += [
"ContentDOMReference.sys.mjs",
"CreditCard.sys.mjs",
"DeferredTask.sys.mjs",
+ "DomainFrontedRequests.sys.mjs",
"E10SUtils.sys.mjs",
"EventEmitter.sys.mjs",
"FileUtils.sys.mjs",
@@ -188,6 +190,7 @@ EXTRA_JS_MODULES += [
"LayoutUtils.sys.mjs",
"Log.sys.mjs",
"LogManager.sys.mjs",
+ "Moat.sys.mjs",
"NewTabUtils.sys.mjs",
"NLP.sys.mjs",
"ObjectUtils.sys.mjs",
@@ -209,6 +212,8 @@ EXTRA_JS_MODULES += [
"Sqlite.sys.mjs",
"SubDialog.sys.mjs",
"Timer.sys.mjs",
+ "TorConnect.sys.mjs",
+ "TorSettings.sys.mjs",
"TorStrings.sys.mjs",
"Troubleshoot.sys.mjs",
"UpdateUtils.sys.mjs",