Skip to content
Snippets Groups Projects
Commit f1ebdb05 authored by Pier Angelo Vendrame's avatar Pier Angelo Vendrame :jack_o_lantern: Committed by morgan
Browse files

Bug 40933: Add tor-launcher functionality

Bug 41926: Reimplement the control port
parent 4e9014aa
Branches
Tags
2 merge requests!1202Bug_43099: 2024 YEC Strings,!1136Bug 43085: Rebased alpha onto 128.2.0esr
Showing
with 4159 additions and 0 deletions
......@@ -93,6 +93,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs",
TabUnloader: "resource:///modules/TabUnloader.sys.mjs",
TelemetryUtils: "resource://gre/modules/TelemetryUtils.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
UIState: "resource://services-sync/UIState.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
......@@ -1995,6 +1996,8 @@ BrowserGlue.prototype = {
lazy.DoHController.init();
lazy.TorProviderBuilder.firstWindowLoaded();
ClipboardPrivacy.startup();
this._firstWindowTelemetry(aWindow);
......
......
......@@ -213,6 +213,7 @@
@RESPATH@/browser/chrome/browser.manifest
@RESPATH@/chrome/pdfjs.manifest
@RESPATH@/chrome/pdfjs/*
@RESPATH@/components/tor-launcher.manifest
@RESPATH@/chrome/torbutton.manifest
@RESPATH@/chrome/torbutton/*
@RESPATH@/chrome/toolkit@JAREXT@
......
......
......@@ -78,6 +78,7 @@ DIRS += [
"thumbnails",
"timermanager",
"tooltiptext",
"tor-launcher",
"typeaheadfind",
"utils",
"url-classifier",
......
......
import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});
const log = console.createInstance({
maxLogLevel: "Info",
prefix: "TorBootstrapRequest",
});
/**
* This class encapsulates the observer register/unregister logic to provide an
* XMLHttpRequest-like API to bootstrap tor.
* TODO: Remove this class, and move its logic inside the TorProvider.
*/
export class TorBootstrapRequest {
// number of ms to wait before we abandon the bootstrap attempt
// a value of 0 implies we never wait
timeout = 0;
// callbacks for bootstrap process status updates
onbootstrapstatus = (_progress, _status) => {};
onbootstrapcomplete = () => {};
onbootstraperror = _error => {};
// internal resolve() method for bootstrap
#bootstrapPromiseResolve = null;
#bootstrapPromise = null;
#timeoutID = null;
observe(subject, topic) {
const obj = subject?.wrappedJSObject;
switch (topic) {
case lazy.TorProviderTopics.BootstrapStatus: {
const progress = obj.PROGRESS;
if (this.onbootstrapstatus) {
const status = obj.TAG;
this.onbootstrapstatus(progress, status);
}
if (progress === 100) {
if (this.onbootstrapcomplete) {
this.onbootstrapcomplete();
}
this.#bootstrapPromiseResolve(true);
clearTimeout(this.#timeoutID);
this.#timeoutID = null;
}
break;
}
case lazy.TorProviderTopics.BootstrapError: {
log.info("TorBootstrapRequest: observerd TorBootstrapError", obj);
const error = new Error(obj.summary);
Object.assign(error, obj);
this.#stop(error);
break;
}
}
}
// resolves 'true' if bootstrap succeeds, false otherwise
bootstrap() {
if (this.#bootstrapPromise) {
return this.#bootstrapPromise;
}
this.#bootstrapPromise = new Promise(resolve => {
this.#bootstrapPromiseResolve = resolve;
// register ourselves to listen for bootstrap events
Services.obs.addObserver(this, lazy.TorProviderTopics.BootstrapStatus);
Services.obs.addObserver(this, lazy.TorProviderTopics.BootstrapError);
// optionally cancel bootstrap after a given timeout
if (this.timeout > 0) {
this.#timeoutID = setTimeout(() => {
this.#timeoutID = null;
this.#stop(
new Error(
`Bootstrap attempt abandoned after waiting ${this.timeout} ms`
)
);
}, this.timeout);
}
// Wait for bootstrapping to begin and maybe handle error.
// Notice that we do not resolve the promise here in case of success, but
// we do it from the BootstrapStatus observer.
lazy.TorProviderBuilder.build()
.then(provider => provider.connect())
.catch(err => {
this.#stop(err);
});
}).finally(() => {
// and remove ourselves once bootstrap is resolved
Services.obs.removeObserver(this, lazy.TorProviderTopics.BootstrapStatus);
Services.obs.removeObserver(this, lazy.TorProviderTopics.BootstrapError);
this.#bootstrapPromise = null;
});
return this.#bootstrapPromise;
}
async cancel() {
await this.#stop();
}
// Internal implementation. Do not use directly, but call cancel, instead.
async #stop(error) {
// first stop our bootstrap timeout before handling the error
if (this.#timeoutID !== null) {
clearTimeout(this.#timeoutID);
this.#timeoutID = null;
}
let provider;
try {
provider = await lazy.TorProviderBuilder.build();
} catch {
// This was probably the error that lead to stop in the first place.
// No need to continue propagating it.
}
try {
await provider?.stopBootstrap();
} catch (e) {
console.error("Failed to stop the bootstrap.", e);
if (!error) {
error = e;
}
}
if (this.onbootstraperror && error) {
this.onbootstraperror(error);
}
this.#bootstrapPromiseResolve(false);
}
}
This diff is collapsed.
This diff is collapsed.
// Copyright (c) 2022, The Tor Project, Inc.
export const TorStatuses = Object.freeze({
OK: 250,
EventNotification: 650,
});
export const TorParsers = Object.freeze({
// Escape non-ASCII characters for use within the Tor Control protocol.
// Based on Vidalia's src/common/stringutil.cpp:string_escape().
// Returns the new string.
escapeString(aStr) {
// Just return if all characters are printable ASCII excluding SP, ", and #
const kSafeCharRE = /^[\x21\x24-\x7E]*$/;
if (!aStr || kSafeCharRE.test(aStr)) {
return aStr;
}
const escaped = aStr
.replaceAll("\\", "\\\\")
.replaceAll('"', '\\"')
.replaceAll("\n", "\\n")
.replaceAll("\r", "\\r")
.replaceAll("\t", "\\t")
.replaceAll(/[^\x20-\x7e]+/g, text => {
const encoder = new TextEncoder();
return Array.from(
encoder.encode(text),
ch => "\\x" + ch.toString(16)
).join("");
});
return `"${escaped}"`;
},
// Unescape Tor Control string aStr (removing surrounding "" and \ escapes).
// Based on Vidalia's src/common/stringutil.cpp:string_unescape().
// Returns the unescaped string. Throws upon failure.
// Within Torbutton, the file modules/utils.js also contains a copy of
// _strUnescape().
unescapeString(aStr) {
if (
!aStr ||
aStr.length < 2 ||
aStr[0] !== '"' ||
aStr[aStr.length - 1] !== '"'
) {
return aStr;
}
// Regular expression by Tim Pietzcker
// https://stackoverflow.com/a/15569588
if (!/^(?:[^"\\]|\\.|"(?:\\.|[^"\\])*")*$/.test(aStr)) {
throw new Error('Unescaped " within string');
}
const matchUnicode = /^(\\x[0-9A-Fa-f]{2}|\\[0-7]{3})+/;
let rv = "";
let lastAdded = 1;
let bs;
while ((bs = aStr.indexOf("\\", lastAdded)) !== -1) {
rv += aStr.substring(lastAdded, bs);
// We always increment lastAdded, because we will either add something, or
// ignore the backslash.
lastAdded = bs + 2;
if (lastAdded === aStr.length) {
// The string ends with \", which is illegal
throw new Error("Missing character after \\");
}
switch (aStr[bs + 1]) {
case "n":
rv += "\n";
break;
case "r":
rv += "\r";
break;
case "t":
rv += "\t";
break;
case '"':
case "\\":
rv += aStr[bs + 1];
break;
default:
aStr.substring(bs).replace(matchUnicode, sequence => {
const bytes = [];
for (let i = 0; i < sequence.length; i += 4) {
if (sequence[i + 1] === "x") {
bytes.push(parseInt(sequence.substring(i + 2, i + 4), 16));
} else {
bytes.push(parseInt(sequence.substring(i + 1, i + 4), 8));
}
}
lastAdded = bs + sequence.length;
const decoder = new TextDecoder();
rv += decoder.decode(new Uint8Array(bytes));
return "";
});
// We have already incremented lastAdded, which means we ignore the
// backslash, and we will do something at the next one.
break;
}
}
rv += aStr.substring(lastAdded, aStr.length - 1);
return rv;
},
parseBridgeLine(line) {
if (!line) {
return null;
}
const re =
/\s*(?:(?<transport>\S+)\s+)?(?<addr>[0-9a-fA-F\.\[\]\:]+:\d{1,5})(?:\s+(?<id>[0-9a-fA-F]{40}))?(?:\s+(?<args>.+))?/;
const match = re.exec(line);
if (!match) {
throw new Error(`Invalid bridge line: ${line}.`);
}
const bridge = match.groups;
if (!bridge.transport) {
bridge.transport = "vanilla";
}
return bridge;
},
});
/* 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 } from "resource://gre/modules/Timer.sys.mjs";
import { Subprocess } from "resource://gre/modules/Subprocess.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
TorParsers: "resource://gre/modules/TorParsers.sys.mjs",
});
const TorProcessStatus = Object.freeze({
Unknown: 0,
Starting: 1,
Running: 2,
Exited: 3,
});
const logger = console.createInstance({
maxLogLevel: "Info",
prefix: "TorProcess",
});
/**
* This class can be used to start a tor daemon instance and receive
* notifications when it exits.
* It will automatically convert the settings objects into the appropriate
* command line arguments.
*
* It does not offer a way to stop a process because it is supposed to exit
* automatically when the owning control port connection is closed.
*/
export class TorProcess {
#controlSettings;
#socksSettings;
#exeFile = null;
#dataDir = null;
#args = [];
#subprocess = null;
#status = TorProcessStatus.Unknown;
onExit = _exitCode => {};
constructor(controlSettings, socksSettings) {
if (
controlSettings &&
!controlSettings.password?.length &&
!controlSettings.cookieFilePath
) {
throw new Error("Unauthenticated control port is not supported");
}
const checkPort = port =>
port === undefined ||
(Number.isInteger(port) && port > 0 && port < 65535);
if (!checkPort(controlSettings?.port)) {
throw new Error("Invalid control port");
}
if (!checkPort(socksSettings.port)) {
throw new Error("Invalid port specified for the SOCKS port");
}
this.#controlSettings = { ...controlSettings };
const ipcFileToString = file =>
"unix:" + lazy.TorParsers.escapeString(file.path);
if (controlSettings.ipcFile) {
this.#controlSettings.ipcFile = ipcFileToString(controlSettings.ipcFile);
}
this.#socksSettings = { ...socksSettings };
if (socksSettings.ipcFile) {
this.#socksSettings.ipcFile = ipcFileToString(socksSettings.ipcFile);
}
}
get isRunning() {
return (
this.#status === TorProcessStatus.Starting ||
this.#status === TorProcessStatus.Running
);
}
async start() {
if (this.#subprocess) {
return;
}
this.#status = TorProcessStatus.Unknown;
try {
this.#makeArgs();
this.#addControlPortArgs();
this.#addSocksPortArg();
const pid = Services.appinfo.processID;
if (pid !== 0) {
this.#args.push("__OwningControllerProcess", pid.toString());
}
if (lazy.TorLauncherUtil.shouldShowNetworkSettings) {
this.#args.push("DisableNetwork", "1");
}
this.#status = TorProcessStatus.Starting;
// useful for simulating slow tor daemon launch
const kPrefTorDaemonLaunchDelay = "extensions.torlauncher.launch_delay";
const launchDelay = Services.prefs.getIntPref(
kPrefTorDaemonLaunchDelay,
0
);
if (launchDelay > 0) {
await new Promise(resolve => setTimeout(() => resolve(), launchDelay));
}
logger.debug(`Starting ${this.#exeFile.path}`, this.#args);
const options = {
command: this.#exeFile.path,
arguments: this.#args,
stderr: "stdout",
workdir: lazy.TorLauncherUtil.getTorFile("pt-startup-dir", false).path,
};
this.#subprocess = await Subprocess.call(options);
this.#status = TorProcessStatus.Running;
} catch (e) {
this.#status = TorProcessStatus.Exited;
this.#subprocess = null;
logger.error("startTor error:", e);
throw e;
}
// Do not await the following functions, as they will return only when the
// process exits.
this.#dumpStdout();
this.#watchProcess();
}
// Forget about a process.
//
// Instead of killing the tor process, we rely on the TAKEOWNERSHIP feature
// to shut down tor when we close the control port connection.
//
// Previously, we sent a SIGNAL HALT command to the tor control port,
// but that caused hangs upon exit in the Firefox 24.x based browser.
// Apparently, Firefox does not like to process socket I/O while
// quitting if the browser did not finish starting up (e.g., when
// someone presses the Quit button on our Network Settings window
// during startup).
//
// Still, before closing the owning connection, this class should forget about
// the process, so that future notifications will be ignored.
forget() {
this.#subprocess = null;
this.#status = TorProcessStatus.Exited;
}
async #dumpStdout() {
let string;
while (
this.#subprocess &&
(string = await this.#subprocess.stdout.readString())
) {
dump(string);
}
}
async #watchProcess() {
const watched = this.#subprocess;
if (!watched) {
return;
}
let processExitCode;
try {
const { exitCode } = await watched.wait();
processExitCode = exitCode;
if (watched !== this.#subprocess) {
logger.debug(`A Tor process exited with code ${exitCode}.`);
} else if (exitCode) {
logger.warn(`The watched Tor process exited with code ${exitCode}.`);
} else {
logger.info("The Tor process exited.");
}
} catch (e) {
logger.error("Failed to watch the tor process", e);
}
if (watched === this.#subprocess) {
this.#processExitedUnexpectedly(processExitCode);
}
}
#processExitedUnexpectedly(exitCode) {
this.#subprocess = null;
this.#status = TorProcessStatus.Exited;
logger.warn("Tor exited suddenly.");
this.onExit(exitCode);
}
#makeArgs() {
this.#exeFile = lazy.TorLauncherUtil.getTorFile("tor", false);
if (!this.#exeFile) {
throw new Error("Could not find the tor binary.");
}
const torrcFile = lazy.TorLauncherUtil.getTorFile("torrc", true);
if (!torrcFile) {
// FIXME: Is this still a fatal error?
throw new Error("Could not find the torrc.");
}
// Get the Tor data directory first so it is created before we try to
// construct paths to files that will be inside it.
this.#dataDir = lazy.TorLauncherUtil.getTorFile("tordatadir", true);
if (!this.#dataDir) {
throw new Error("Could not find the tor data directory.");
}
const onionAuthDir = lazy.TorLauncherUtil.getTorFile(
"toronionauthdir",
true
);
if (!onionAuthDir) {
throw new Error("Could not find the tor onion authentication directory.");
}
this.#args = [];
this.#args.push("-f", torrcFile.path);
this.#args.push("DataDirectory", this.#dataDir.path);
this.#args.push("ClientOnionAuthDir", onionAuthDir.path);
// TODO: Create this starting from pt_config.json (tor-browser#42357).
const torrcDefaultsFile = lazy.TorLauncherUtil.getTorFile(
"torrc-defaults",
false
);
if (torrcDefaultsFile) {
this.#args.push("--defaults-torrc", torrcDefaultsFile.path);
// The geoip and geoip6 files are in the same directory as torrc-defaults.
// TODO: Change TorFile to return the generic path to these files to make
// them independent from the torrc-defaults.
const geoipFile = torrcDefaultsFile.clone();
geoipFile.leafName = "geoip";
this.#args.push("GeoIPFile", geoipFile.path);
const geoip6File = torrcDefaultsFile.clone();
geoip6File.leafName = "geoip6";
this.#args.push("GeoIPv6File", geoip6File.path);
} else {
logger.warn(
"torrc-defaults was not found, some functionalities will be disabled."
);
}
}
/**
* Add all the arguments related to the control port.
* We use the + prefix so that the the port is added to any other port already
* defined in the torrc, and the __ prefix so that it is never written to
* torrc.
*/
#addControlPortArgs() {
if (!this.#controlSettings) {
return;
}
let controlPortArg;
if (this.#controlSettings.ipcFile) {
controlPortArg = this.#controlSettings.ipcFile;
} else if (this.#controlSettings.port) {
controlPortArg = this.#controlSettings.host
? `${this.#controlSettings.host}:${this.#controlSettings.port}`
: this.#controlSettings.port.toString();
}
if (controlPortArg) {
this.#args.push("+__ControlPort", controlPortArg);
}
if (this.#controlSettings.password?.length) {
this.#args.push(
"HashedControlPassword",
this.#hashPassword(this.#controlSettings.password)
);
}
if (this.#controlSettings.cookieFilePath) {
this.#args.push("CookieAuthentication", "1");
this.#args.push("CookieAuthFile", this.#controlSettings.cookieFilePath);
}
}
/**
* Add the argument related to the control port.
* We use the + prefix so that the the port is added to any other port already
* defined in the torrc, and the __ prefix so that it is never written to
* torrc.
*/
#addSocksPortArg() {
let socksPortArg;
if (this.#socksSettings.ipcFile) {
socksPortArg = this.#socksSettings.ipcFile;
} else if (this.#socksSettings.port != 0) {
socksPortArg = this.#socksSettings.host
? `${this.#socksSettings.host}:${this.#socksSettings.port}`
: this.#socksSettings.port.toString();
}
if (socksPortArg) {
const socksPortFlags = Services.prefs.getCharPref(
"extensions.torlauncher.socks_port_flags",
"IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth"
);
if (socksPortFlags) {
socksPortArg += " " + socksPortFlags;
}
this.#args.push("+__SocksPort", socksPortArg);
}
}
/**
* Hash a password to then pass it to Tor as a command line argument.
* Based on Vidalia's TorSettings::hashPassword().
*
* @param {Uint8Array} password The password, as an array of bytes
* @returns {string} The hashed password
*/
#hashPassword(password) {
// The password has already been checked by the caller.
// Generate a random, 8 byte salt value.
const salt = Array.from(crypto.getRandomValues(new Uint8Array(8)));
// Run through the S2K algorithm and convert to a string.
const toHex = v => v.toString(16).padStart(2, "0");
const arrayToHex = aArray => aArray.map(toHex).join("");
const kCodedCount = 96;
const hashVal = this.#cryptoSecretToKey(
Array.from(password),
salt,
kCodedCount
);
return "16:" + arrayToHex(salt) + toHex(kCodedCount) + arrayToHex(hashVal);
}
/**
* Generates and return a hash of a password by following the iterated and
* salted S2K algorithm (see RFC 2440 section 3.6.1.3).
* See also https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/control-spec.txt#L3824.
* #cryptoSecretToKey() is similar to Vidalia's crypto_secret_to_key().
*
* @param {Array} password The password to hash, as an array of bytes
* @param {Array} salt The salt to use for the hash, as an array of bytes
* @param {number} codedCount The counter, coded as specified in RFC 2440
* @returns {Array} The hash of the password, as an array of bytes
*/
#cryptoSecretToKey(password, salt, codedCount) {
const inputArray = salt.concat(password);
// Subtle crypto only has the final digest, and does not allow incremental
// updates.
const hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
Ci.nsICryptoHash
);
hasher.init(hasher.SHA1);
const kEXPBIAS = 6;
let count = (16 + (codedCount & 15)) << ((codedCount >> 4) + kEXPBIAS);
while (count > 0) {
if (count > inputArray.length) {
hasher.update(inputArray, inputArray.length);
count -= inputArray.length;
} else {
const finalArray = inputArray.slice(0, count);
hasher.update(finalArray, finalArray.length);
count = 0;
}
}
return hasher
.finish(false)
.split("")
.map(b => b.charCodeAt(0));
}
}
/* 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, {
EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
});
const logger = console.createInstance({
maxLogLevel: "Info",
prefix: "TorProcessAndroid",
});
const TorOutgoingEvents = Object.freeze({
start: "GeckoView:Tor:StartTor",
stop: "GeckoView:Tor:StopTor",
});
// The events we will listen to
const TorIncomingEvents = Object.freeze({
started: "GeckoView:Tor:TorStarted",
startFailed: "GeckoView:Tor:TorStartFailed",
exited: "GeckoView:Tor:TorExited",
});
/**
* This class allows to start a tor process on Android devices.
*
* GeckoView does not include the facilities to start processes from JavaScript,
* therefore the actual implementation is written in Java, and this is just
* plumbing code over the global EventDispatcher.
*/
export class TorProcessAndroid {
/**
* The handle the Java counterpart uses to refer to the process we started.
* We use it to filter the exit events and make sure they refer to the daemon
* we are interested in.
*/
#processHandle = null;
/**
* The promise resolver we call when the Java counterpart sends the event that
* tor has started.
*/
#startResolve = null;
/**
* The promise resolver we call when the Java counterpart sends the event that
* it failed to start tor.
*/
#startReject = null;
onExit = () => {};
get isRunning() {
return !!this.#processHandle;
}
async start() {
// Generate the handle on the JS side so that it's ready in case it takes
// less to start the process than to propagate the success.
this.#processHandle = crypto.randomUUID();
logger.info(`Starting new process with handle ${this.#processHandle}`);
// Let's declare it immediately, so that the Java side can do its stuff in
// an async manner and we avoid possible race conditions (at most we await
// an already resolved/rejected promise.
const startEventPromise = new Promise((resolve, reject) => {
this.#startResolve = resolve;
this.#startReject = reject;
});
lazy.EventDispatcher.instance.registerListener(
this,
Object.values(TorIncomingEvents)
);
let config;
try {
config = await lazy.EventDispatcher.instance.sendRequestForResult({
type: TorOutgoingEvents.start,
handle: this.#processHandle,
});
logger.debug("Sent the start event.");
} catch (e) {
this.forget();
throw e;
}
await startEventPromise;
return config;
}
forget() {
// Processes usually exit when we close the control port connection to them.
logger.trace(`Forgetting process ${this.#processHandle}`);
lazy.EventDispatcher.instance.sendRequestForResult({
type: TorOutgoingEvents.stop,
handle: this.#processHandle,
});
logger.debug("Sent the start event.");
this.#processHandle = null;
lazy.EventDispatcher.instance.unregisterListener(
this,
Object.values(TorIncomingEvents)
);
}
onEvent(event, data, _callback) {
if (data?.handle !== this.#processHandle) {
logger.debug(`Ignoring event ${event} with another handle`, data);
return;
}
logger.info(`Received an event ${event}`, data);
switch (event) {
case TorIncomingEvents.started:
this.#startResolve();
break;
case TorIncomingEvents.startFailed:
this.#startReject(new Error(data.error));
break;
case TorIncomingEvents.exited:
this.forget();
if (this.#startReject !== null) {
this.#startReject();
}
this.onExit(data.status);
break;
}
}
}
This diff is collapsed.
/* 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",
TorProvider: "resource://gre/modules/TorProvider.sys.mjs",
});
export const TorProviderTopics = Object.freeze({
ProcessExited: "TorProcessExited",
BootstrapStatus: "TorBootstrapStatus",
BootstrapError: "TorBootstrapError",
TorLog: "TorLog",
HasWarnOrErr: "TorLogHasWarnOrErr",
BridgeChanged: "TorBridgeChanged",
CircuitCredentialsMatched: "TorCircuitCredentialsMatched",
});
export const TorProviders = Object.freeze({
none: 0,
tor: 1,
});
/**
* The factory to get a Tor provider.
* Currently we support only TorProvider, i.e., the one that interacts with
* C-tor through the control port protocol.
*/
export class TorProviderBuilder {
/**
* A promise with the instance of the provider that we are using.
*
* @type {Promise<TorProvider>?}
*/
static #provider = null;
/**
* The observer that checks when the tor process exits, and reinitializes the
* provider.
*
* @type {Function}
*/
static #exitObserver = null;
/**
* Tell whether the browser UI is ready.
* We ignore any errors until it is because we cannot show them.
*
* @type {boolean}
*/
static #uiReady = false;
/**
* Initialize the provider of choice.
* Even though initialization is asynchronous, we do not expect the caller to
* await this method. The reason is that any call to build() will wait the
* initialization anyway (and re-throw any initialization error).
*/
static async init() {
switch (this.providerType) {
case TorProviders.tor:
await this.#initTorProvider();
break;
case TorProviders.none:
lazy.TorLauncherUtil.setProxyConfiguration(
lazy.TorLauncherUtil.getPreferredSocksConfiguration()
);
break;
default:
console.error(`Unknown tor provider ${this.providerType}.`);
break;
}
}
static async #initTorProvider() {
if (!this.#exitObserver) {
this.#exitObserver = this.#torExited.bind(this);
Services.obs.addObserver(
this.#exitObserver,
TorProviderTopics.ProcessExited
);
}
try {
const old = await this.#provider;
old?.uninit();
} catch {}
this.#provider = new Promise((resolve, reject) => {
const provider = new lazy.TorProvider();
provider
.init()
.then(() => resolve(provider))
.catch(reject);
});
await this.#provider;
}
static uninit() {
this.#provider?.then(provider => {
provider.uninit();
this.#provider = null;
});
if (this.#exitObserver) {
Services.obs.removeObserver(
this.#exitObserver,
TorProviderTopics.ProcessExited
);
this.#exitObserver = null;
}
}
/**
* Build a provider.
* This method will wait for the system to be initialized, and allows you to
* catch also any initialization errors.
*/
static async build() {
if (!this.#provider && this.providerType === TorProviders.none) {
throw new Error(
"Tor Browser has been configured to use only the proxy functionalities."
);
} else if (!this.#provider) {
throw new Error(
"The provider has not been initialized or already uninitialized."
);
}
return this.#provider;
}
/**
* Check if the provider has been succesfully initialized when the first
* browser window is shown.
* This is a workaround we need because ideally we would like the tor process
* to start as soon as possible, to avoid delays in the about:torconnect page,
* but we should modify TorConnect and about:torconnect to handle this case
* there with a better UX.
*/
static async firstWindowLoaded() {
// FIXME: Just integrate this with the about:torconnect or about:tor UI.
if (
!lazy.TorLauncherUtil.shouldStartAndOwnTor ||
this.providerType !== TorProviders.tor
) {
// If we are not managing the Tor daemon we cannot restart it, so just
// early return.
return;
}
let running = false;
try {
const provider = await this.#provider;
// The initialization might have succeeded, but so far we have ignored any
// error notification. So, check that the process has not exited after the
// provider has been initialized successfully, but the UI was not ready
// yet.
running = provider.isRunning;
} catch {
// Not even initialized, running is already false.
}
while (!running && lazy.TorLauncherUtil.showRestartPrompt(true)) {
try {
await this.#initTorProvider();
running = true;
} catch {}
}
// The user might have canceled the restart, but at this point the UI is
// ready in any case.
this.#uiReady = true;
}
static async #torExited() {
if (!this.#uiReady) {
console.warn(
`Seen ${TorProviderTopics.ProcessExited}, but not doing anything because the UI is not ready yet.`
);
return;
}
while (lazy.TorLauncherUtil.showRestartPrompt(false)) {
try {
await this.#initTorProvider();
break;
} catch {}
}
}
/**
* Return the provider chosen by the user.
* This function checks the TOR_PROVIDER environment variable and if it is a
* known provider, it returns its associated value.
* Otherwise, if it is not valid, the C tor implementation is chosen as the
* default one.
*
* @returns {number} An entry from TorProviders
*/
static get providerType() {
// TODO: Add a preference to permanently save this without and avoid always
// using an environment variable.
let provider = TorProviders.tor;
const kEnvName = "TOR_PROVIDER";
if (
Services.env.exists(kEnvName) &&
Services.env.get(kEnvName) in TorProviders
) {
provider = TorProviders[Services.env.get(kEnvName)];
}
return provider;
}
}
const lazy = {};
// We will use the modules only when the profile is loaded, so prefer lazy
// loading
ChromeUtils.defineESModuleGetters(lazy, {
TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});
/* Browser observer topis */
const BrowserTopics = Object.freeze({
ProfileAfterChange: "profile-after-change",
QuitApplicationGranted: "quit-application-granted",
});
let gInited = false;
/**
* This class is registered as an observer, and will be instanced automatically
* by Firefox.
* When it observes profile-after-change, it initializes whatever is needed to
* launch Tor.
*/
export class TorStartupService {
observe(aSubject, aTopic) {
if (aTopic === BrowserTopics.ProfileAfterChange && !gInited) {
this.#init();
} else if (aTopic === BrowserTopics.QuitApplicationGranted) {
this.#uninit();
}
}
#init() {
Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
// 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();
gInited = true;
}
#uninit() {
Services.obs.removeObserver(this, BrowserTopics.QuitApplicationGranted);
lazy.TorProviderBuilder.uninit();
lazy.TorLauncherUtil.cleanupTempDirectories();
}
}
Classes = [
{
"cid": "{df46c65d-be2b-4d16-b280-69733329eecf}",
"contract_ids": [
"@torproject.org/tor-startup-service;1"
],
"esModule": "resource://gre/modules/TorStartupService.sys.mjs",
"constructor": "TorStartupService",
},
]
EXTRA_JS_MODULES += [
"TorBootstrapRequest.sys.mjs",
"TorControlPort.sys.mjs",
"TorLauncherUtil.sys.mjs",
"TorParsers.sys.mjs",
"TorProcess.sys.mjs",
"TorProcessAndroid.sys.mjs",
"TorProvider.sys.mjs",
"TorProviderBuilder.sys.mjs",
"TorStartupService.sys.mjs",
]
XPCOM_MANIFESTS += [
"components.conf",
]
EXTRA_COMPONENTS += [
"tor-launcher.manifest",
]
category profile-after-change TorStartupService @torproject.org/tor-startup-service;1
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment