Skip to content
Snippets Groups Projects
Verified Commit be408b39 authored by Pier Angelo Vendrame's avatar Pier Angelo Vendrame :jack_o_lantern:
Browse files

Bug 40933: Add tor-launcher functionality

parent 1549cab4
No related branches found
No related tags found
1 merge request!543Tor Browser 12.5a 102.8.0esr rebase
Showing
with 2859 additions and 0 deletions
......@@ -234,6 +234,7 @@
@RESPATH@/browser/chrome/browser.manifest
@RESPATH@/chrome/pdfjs.manifest
@RESPATH@/chrome/pdfjs/*
@RESPATH@/components/tor-launcher.manifest
@RESPATH@/chrome/toolkit@JAREXT@
@RESPATH@/chrome/toolkit.manifest
#ifdef MOZ_GTK
......
......@@ -75,6 +75,7 @@ DIRS += [
"thumbnails",
"timermanager",
"tooltiptext",
"tor-launcher",
"typeaheadfind",
"utils",
"url-classifier",
......
"use strict";
var EXPORTED_SYMBOLS = ["TorBootstrapRequest", "TorTopics"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { setTimeout, clearTimeout } = ChromeUtils.import(
"resource://gre/modules/Timer.jsm"
);
const { TorProtocolService } = ChromeUtils.import(
"resource://gre/modules/TorProtocolService.jsm"
);
const { TorLauncherUtil } = ChromeUtils.import(
"resource://gre/modules/TorLauncherUtil.jsm"
);
/* tor-launcher observer topics */
const TorTopics = Object.freeze({
BootstrapStatus: "TorBootstrapStatus",
BootstrapError: "TorBootstrapError",
LogHasWarnOrErr: "TorLogHasWarnOrErr",
});
// modeled after XMLHttpRequest
// nicely encapsulates the observer register/unregister logic
class TorBootstrapRequest {
constructor() {
// number of ms to wait before we abandon the bootstrap attempt
// a value of 0 implies we never wait
this.timeout = 0;
// callbacks for bootstrap process status updates
this.onbootstrapstatus = (progress, status) => {};
this.onbootstrapcomplete = () => {};
this.onbootstraperror = (message, details) => {};
// internal resolve() method for bootstrap
this._bootstrapPromiseResolve = null;
this._bootstrapPromise = null;
this._timeoutID = null;
}
observe(subject, topic, data) {
const obj = subject?.wrappedJSObject;
switch (topic) {
case TorTopics.BootstrapStatus: {
const progress = obj.PROGRESS;
const status = TorLauncherUtil.getLocalizedBootstrapStatus(obj, "TAG");
if (this.onbootstrapstatus) {
this.onbootstrapstatus(progress, status);
}
if (progress === 100) {
if (this.onbootstrapcomplete) {
this.onbootstrapcomplete();
}
this._bootstrapPromiseResolve(true);
clearTimeout(this._timeoutID);
}
break;
}
case TorTopics.BootstrapError: {
console.info("TorBootstrapRequest: observerd TorBootstrapError", obj);
this._stop(obj?.message, obj?.details);
break;
}
}
}
// resolves 'true' if bootstrap succeeds, false otherwise
bootstrap() {
if (this._bootstrapPromise) {
return this._bootstrapPromise;
}
this._bootstrapPromise = new Promise((resolve, reject) => {
this._bootstrapPromiseResolve = resolve;
// register ourselves to listen for bootstrap events
Services.obs.addObserver(this, TorTopics.BootstrapStatus);
Services.obs.addObserver(this, TorTopics.BootstrapError);
// optionally cancel bootstrap after a given timeout
if (this.timeout > 0) {
this._timeoutID = setTimeout(async () => {
this._timeoutID = null;
// TODO: Translate, if really used
await this._stop(
"Tor Bootstrap process timed out",
`Bootstrap attempt abandoned after waiting ${this.timeout} ms`
);
}, this.timeout);
}
// wait for bootstrapping to begin and maybe handle error
TorProtocolService.connect().catch(err => {
this._stop(err.message, "");
});
}).finally(() => {
// and remove ourselves once bootstrap is resolved
Services.obs.removeObserver(this, TorTopics.BootstrapStatus);
Services.obs.removeObserver(this, TorTopics.BootstrapError);
this._bootstrapPromise = null;
});
return this._bootstrapPromise;
}
async cancel() {
await this._stop();
}
// Internal implementation. Do not use directly, but call cancel, instead.
async _stop(message, details) {
// first stop our bootstrap timeout before handling the error
if (this._timeoutID !== null) {
clearTimeout(this._timeoutID);
this._timeoutID = null;
}
// stopBootstrap never throws
await TorProtocolService.stopBootstrap();
if (this.onbootstraperror && message) {
this.onbootstraperror(message, details);
}
this._bootstrapPromiseResolve(false);
}
}
// Copyright (c) 2022, The Tor Project, Inc.
// See LICENSE for licensing information.
"use strict";
/*************************************************************************
* Tor Launcher Util JS Module
*************************************************************************/
var EXPORTED_SYMBOLS = ["TorLauncherUtil"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const kPropBundleURI = "chrome://torbutton/locale/torlauncher.properties";
const kPropNamePrefix = "torlauncher.";
const kIPCDirPrefName = "extensions.torlauncher.tmp_ipc_dir";
let gStringBundle = null;
class TorFile {
// The nsIFile to be returned
file = null;
// A relative or absolute path that will determine file
path = null;
pathIsRelative = false;
// If true, path is ignored
useAppDir = false;
isIPC = false;
checkIPCPathLen = true;
static _isFirstIPCPathRequest = true;
static _isUserDataOutsideOfAppDir = undefined;
static _dataDir = null;
static _appDir = null;
constructor(aTorFileType, aCreate) {
this.fileType = aTorFileType;
this.getFromPref();
this.getIPC();
// No preference and no pre-determined IPC path: use a default path.
if (!this.file && !this.path) {
this.getDefault();
}
if (!this.file && this.path) {
this.pathToFile();
}
if (this.file && !this.file.exists() && !this.isIPC && aCreate) {
this.createFile();
}
this.normalize();
}
getFile() {
return this.file;
}
getFromPref() {
const prefName = `extensions.torlauncher.${this.fileType}_path`;
this.path = Services.prefs.getCharPref(prefName, "");
if (this.path) {
const re = TorLauncherUtil.isWindows ? /^[A-Za-z]:\\/ : /^\//;
this.isRelativePath = !re.test(this.path);
// always try to use path if provided in pref
this.checkIPCPathLen = false;
}
}
getIPC() {
const isControlIPC = this.fileType === "control_ipc";
const isSOCKSIPC = this.fileType === "socks_ipc";
this.isIPC = isControlIPC || isSOCKSIPC;
const kControlIPCFileName = "control.socket";
const kSOCKSIPCFileName = "socks.socket";
this.ipcFileName = isControlIPC ? kControlIPCFileName : kSOCKSIPCFileName;
this.extraIPCPathLen = this.isSOCKSIPC ? 2 : 0;
// Do not do anything else if this.path has already been populated with the
// _path preference for this file type (or if we are not looking for an IPC
// file).
if (this.path || !this.isIPC) {
return;
}
// If this is the first request for an IPC path during this browser
// session, remove the old temporary directory. This helps to keep /tmp
// clean if the browser crashes or is killed.
if (TorFile._isFirstIPCPathRequest) {
TorLauncherUtil.cleanupTempDirectories();
TorFile._isFirstIPCPathRequest = false;
} else {
// FIXME: Do we really need a preference? Or can we save it in a static
// member?
// Retrieve path for IPC objects (it may have already been determined).
const ipcDirPath = Services.prefs.getCharPref(kIPCDirPrefName, "");
if (ipcDirPath) {
// We have already determined where IPC objects will be placed.
this.file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
this.file.initWithPath(ipcDirPath);
this.file.append(this.ipcFileName);
this.checkIPCPathLen = false; // already checked.
return;
}
}
// If XDG_RUNTIME_DIR is set, use it as the base directory for IPC
// objects (e.g., Unix domain sockets) -- assuming it is not too long.
const env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
if (!env.exists("XDG_RUNTIME_DIR")) {
return;
}
const ipcDir = this.createUniqueIPCDir(env.get("XDG_RUNTIME_DIR"));
if (ipcDir) {
const f = ipcDir.clone();
f.append(this.ipcFileName);
if (this.isIPCPathLengthOK(f.path, this.extraIPCPathLen)) {
this.file = f;
this.checkIPCPathLen = false; // no need to check again.
// Store directory path so it can be reused for other IPC objects
// and so it can be removed during exit.
Services.prefs.setCharPref(kIPCDirPrefName, ipcDir.path);
} else {
// too long; remove the directory that we just created.
ipcDir.remove(false);
}
}
}
// This block is used for the TorBrowser-Data/ case.
getDefault() {
let torPath = "";
let dataDir = "";
// FIXME: TOR_BROWSER_DATA_OUTSIDE_APP_DIR is used only on macOS at the
// moment. In Linux and Windows it might not work anymore.
// We might simplify the code here, if we get rid of this macro.
// Also, we allow specifying directly a relative path, for a portable mode.
// Anyway, that macro is also available in AppConstants.
if (TorFile.isUserDataOutsideOfAppDir) {
if (TorLauncherUtil.isMac) {
torPath = "Contents/MacOS/Tor";
} else {
torPath = "TorBrowser/Tor";
}
} else {
torPath = "Tor";
dataDir = "Data/";
}
switch (this.fileType) {
case "tor":
this.path = `${torPath}/tor`;
if (TorLauncherUtil.isWindows) {
this.path += ".exe";
}
break;
case "torrc-defaults":
this.path = TorLauncherUtil.isMac
? "Contents/Resources/TorBrowser/Tor"
: `${dataDir}Tor`;
this.path += "/torrc-defaults";
break;
case "torrc":
this.path = `${dataDir}Tor/torrc`;
break;
case "tordatadir":
this.path = `${dataDir}Tor`;
break;
case "toronionauthdir":
this.path = `${dataDir}Tor/onion-auth`;
break;
case "pt-profiles-dir":
this.path = TorFile.isUserDataOutsideOfAppDir
? "Tor/PluggableTransports"
: `${dataDir}Browser`;
break;
case "pt-startup-dir":
if (TorLauncherUtil.isMac && TorFile.isUserDataOutsideOfAppDir) {
this.path = "Contents/MacOS/Tor";
} else {
this.file = TorFile.appDir.clone();
return;
}
break;
default:
if (!TorLauncherUtil.isWindows && this.isIPC) {
this.path = "Tor/" + this.ipcFileName;
break;
}
throw new Error("Unknown file type");
}
if (TorLauncherUtil.isWindows) {
this.path = this.path.replaceAll("/", "\\");
}
this.isRelativePath = true;
}
pathToFile() {
if (TorLauncherUtil.isWindows) {
this.path = this.path.replaceAll("/", "\\");
}
// Turn 'path' into an absolute path when needed.
if (this.isRelativePath) {
const isUserData =
this.fileType !== "tor" &&
this.fileType !== "pt-startup-dir" &&
this.fileType !== "torrc-defaults";
if (TorFile.isUserDataOutsideOfAppDir) {
let baseDir = isUserData ? TorFile.dataDir : TorFile.appDir;
this.file = baseDir.clone();
} else {
this.file = TorFile.appDir.clone();
this.file.append("TorBrowser");
}
this.file.appendRelativePath(this.path);
} else {
this.file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
this.file.initWithPath(this.path);
}
}
createFile() {
if (
"tordatadir" == this.fileType ||
"toronionauthdir" == this.fileType ||
"pt-profiles-dir" == this.fileType
) {
this.file.create(this.file.DIRECTORY_TYPE, 0o700);
} else {
this.file.create(this.file.NORMAL_FILE_TYPE, 0o600);
}
}
// If the file exists or an IPC object was requested, normalize the path
// and return a file object. The control and SOCKS IPC objects will be
// created by tor.
normalize() {
if (!this.file.exists() && !this.isIPC) {
throw new Error(`${this.fileType} file not found: ${this.file.path}`);
}
try {
this.file.normalize();
} catch (e) {
console.warn("Normalization of the path failed", e);
}
// Ensure that the IPC path length is short enough for use by the
// operating system. If not, create and use a unique directory under
// /tmp for all IPC objects. The created directory path is stored in
// a preference so it can be reused for other IPC objects and so it
// can be removed during exit.
if (
this.isIPC &&
this.checkIPCPathLen &&
!this.isIPCPathLengthOK(this.file.path, this.extraIPCPathLen)
) {
this.file = this.createUniqueIPCDir("/tmp");
if (!this.file) {
throw new Error("failed to create unique directory under /tmp");
}
Services.prefs.setCharPref(kIPCDirPrefName, this.file.path);
this.file.append(this.ipcFileName);
}
}
// Return true if aPath is short enough to be used as an IPC object path,
// e.g., for a Unix domain socket path. aExtraLen is the "delta" necessary
// to accommodate other IPC objects that have longer names; it is used to
// account for "control.socket" vs. "socks.socket" (we want to ensure that
// all IPC objects are placed in the same parent directory unless the user
// has set prefs or env vars to explicitly specify the path for an object).
// We enforce a maximum length of 100 because all operating systems allow
// at least 100 characters for Unix domain socket paths.
isIPCPathLengthOK(aPath, aExtraLen) {
const kMaxIPCPathLen = 100;
return aPath && aPath.length + aExtraLen <= kMaxIPCPathLen;
}
// Returns an nsIFile or null if a unique directory could not be created.
createUniqueIPCDir(aBasePath) {
try {
const d = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
d.initWithPath(aBasePath);
d.append("Tor");
d.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
return d;
} catch (e) {
console.error(`createUniqueIPCDir failed for ${aBasePath}: `, e);
return null;
}
}
static get isUserDataOutsideOfAppDir() {
if (this._isUserDataOutsideOfAppDir === undefined) {
// Determine if we are using a "side-by-side" data model by checking
// whether the user profile is outside of the app directory.
try {
const profDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
this._isUserDataOutsideOfAppDir = !this.appDir.contains(profDir);
} catch (e) {
this._isUserDataOutsideOfAppDir = false;
}
}
return this._isUserDataOutsideOfAppDir;
}
// Returns an nsIFile that points to the application directory.
static get appDir() {
if (!this._appDir) {
let topDir = Services.dirsvc.get("CurProcD", Ci.nsIFile);
// On Linux and Windows, we want to return the Browser/ directory.
// Because topDir ("CurProcD") points to Browser/browser on those
// platforms, we need to go up one level.
// On Mac OS, we want to return the TorBrowser.app/ directory.
// Because topDir points to Contents/Resources/browser on Mac OS,
// we need to go up 3 levels.
let tbbBrowserDepth = TorLauncherUtil.isMac ? 3 : 1;
while (tbbBrowserDepth > 0) {
let didRemove = topDir.leafName != ".";
topDir = topDir.parent;
if (didRemove) {
tbbBrowserDepth--;
}
}
this._appDir = topDir;
}
return this._appDir;
}
// Returns an nsIFile that points to the TorBrowser-Data/ directory.
// This function is only used when isUserDataOutsideOfAppDir === true.
// May throw.
static get dataDir() {
if (!this._dataDir) {
const profDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
this._dataDir = profDir.parent.parent;
}
return this._dataDir;
}
}
const TorLauncherUtil = Object.freeze({
get isMac() {
return Services.appinfo.OS === "Darwin";
},
get isWindows() {
return Services.appinfo.OS === "WINNT";
},
// Returns true if user confirms; false if not.
showConfirm(aParentWindow, aMsg, aDefaultButtonLabel, aCancelButtonLabel) {
if (!aParentWindow) {
aParentWindow = Services.wm.getMostRecentWindow("navigator:browser");
}
const ps = Services.prompt;
const title = this.getLocalizedString("error_title");
const btnFlags =
ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
ps.BUTTON_POS_0_DEFAULT +
ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING;
const notUsed = { value: false };
const btnIndex = ps.confirmEx(
aParentWindow,
title,
aMsg,
btnFlags,
aDefaultButtonLabel,
aCancelButtonLabel,
null,
null,
notUsed
);
return btnIndex === 0;
},
// Localized Strings
// TODO: Switch to fluent also these ones.
// "torlauncher." is prepended to aStringName.
getLocalizedString(aStringName) {
if (!aStringName) {
return aStringName;
}
try {
const key = kPropNamePrefix + aStringName;
return this._stringBundle.GetStringFromName(key);
} catch (e) {}
return aStringName;
},
// "torlauncher." is prepended to aStringName.
getFormattedLocalizedString(aStringName, aArray, aLen) {
if (!aStringName || !aArray) {
return aStringName;
}
try {
const key = kPropNamePrefix + aStringName;
return this._stringBundle.formatStringFromName(key, aArray, aLen);
} catch (e) {}
return aStringName;
},
getLocalizedStringForError(aNSResult) {
for (let prop in Cr) {
if (Cr[prop] === aNSResult) {
const key = "nsresult." + prop;
const rv = this.getLocalizedString(key);
if (rv !== key) {
return rv;
}
return prop; // As a fallback, return the NS_ERROR... name.
}
}
return undefined;
},
getLocalizedBootstrapStatus(aStatusObj, aKeyword) {
if (!aStatusObj || !aKeyword) {
return "";
}
let result;
let fallbackStr;
if (aStatusObj[aKeyword]) {
let val = aStatusObj[aKeyword].toLowerCase();
let key;
if (aKeyword === "TAG") {
// The bootstrap status tags in tagMap below are used by Tor
// versions prior to 0.4.0.x. We map each one to the tag that will
// produce the localized string that is the best fit.
const tagMap = {
conn_dir: "conn",
handshake_dir: "onehop_create",
conn_or: "enough_dirinfo",
handshake_or: "ap_conn",
};
if (val in tagMap) {
val = tagMap[val];
}
key = "bootstrapStatus." + val;
fallbackStr = aStatusObj.SUMMARY;
} else if (aKeyword === "REASON") {
if (val === "connectreset") {
val = "connectrefused";
}
key = "bootstrapWarning." + val;
fallbackStr = aStatusObj.WARNING;
}
result = TorLauncherUtil.getLocalizedString(key);
if (result === key) {
result = undefined;
}
}
if (!result) {
result = fallbackStr;
}
if (aKeyword === "REASON" && aStatusObj.HOSTADDR) {
result += " - " + aStatusObj.HOSTADDR;
}
return result ? result : "";
},
get shouldStartAndOwnTor() {
const kPrefStartTor = "extensions.torlauncher.start_tor";
try {
const kBrowserToolboxPort = "MOZ_BROWSER_TOOLBOX_PORT";
const kEnvSkipLaunch = "TOR_SKIP_LAUNCH";
const env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
if (env.exists(kBrowserToolboxPort)) {
return false;
}
if (env.exists(kEnvSkipLaunch)) {
const value = parseInt(env.get(kEnvSkipLaunch));
return isNaN(value) || !value;
}
} catch (e) {}
return Services.prefs.getBoolPref(kPrefStartTor, true);
},
get shouldShowNetworkSettings() {
try {
const kEnvForceShowNetConfig = "TOR_FORCE_NET_CONFIG";
const env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
if (env.exists(kEnvForceShowNetConfig)) {
const value = parseInt(env.get(kEnvForceShowNetConfig));
return !isNaN(value) && value;
}
} catch (e) {}
return true;
},
get shouldOnlyConfigureTor() {
const kPrefOnlyConfigureTor = "extensions.torlauncher.only_configure_tor";
try {
const kEnvOnlyConfigureTor = "TOR_CONFIGURE_ONLY";
const env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
if (env.exists(kEnvOnlyConfigureTor)) {
const value = parseInt(env.get(kEnvOnlyConfigureTor));
return !isNaN(value) && value;
}
} catch (e) {}
return Services.prefs.getBoolPref(kPrefOnlyConfigureTor, false);
},
// Returns an nsIFile.
// If aTorFileType is "control_ipc" or "socks_ipc", aCreate is ignored
// and there is no requirement that the IPC object exists.
// For all other file types, null is returned if the file does not exist
// and it cannot be created (it will be created if aCreate is true).
getTorFile(aTorFileType, aCreate) {
if (!aTorFileType) {
return null;
}
try {
const torFile = new TorFile(aTorFileType, aCreate);
return torFile.getFile();
} catch (e) {
console.error(`getTorFile: cannot get ${aTorFileType}`, e);
}
return null; // File not found or error (logged above).
},
cleanupTempDirectories() {
const dirPath = Services.prefs.getCharPref(kIPCDirPrefName, "");
try {
Services.prefs.clearUserPref(kIPCDirPrefName);
} catch (e) {}
try {
if (dirPath) {
const f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
f.initWithPath(dirPath);
if (f.exists()) {
f.remove(false);
}
}
} catch (e) {
console.warn("Could not remove the IPC directory", e);
}
},
get _stringBundle() {
if (!gStringBundle) {
gStringBundle = Services.strings.createBundle(kPropBundleURI);
}
return gStringBundle;
},
});
// Copyright (c) 2022, The Tor Project, Inc.
"use strict";
var EXPORTED_SYMBOLS = ["TorMonitorService"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { clearTimeout, setTimeout } = ChromeUtils.import(
"resource://gre/modules/Timer.jsm"
);
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { TorParsers, TorStatuses } = ChromeUtils.import(
"resource://gre/modules/TorParsers.jsm"
);
const { TorProcess } = ChromeUtils.import(
"resource://gre/modules/TorProcess.jsm"
);
const { TorLauncherUtil } = ChromeUtils.import(
"resource://gre/modules/TorLauncherUtil.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"controller",
"resource://torbutton/modules/tor-control-port.js"
);
// TODO: Write a helper to create these logs
XPCOMUtils.defineLazyGetter(this, "logger", () => {
const { ConsoleAPI } = ChromeUtils.import(
"resource://gre/modules/Console.jsm"
);
// TODO: Use a preference to set the log level.
const consoleOptions = {
// maxLogLevel: "warn",
maxLogLevel: "all",
prefix: "TorMonitorService",
};
return new ConsoleAPI(consoleOptions);
});
const Preferences = Object.freeze({
PromptAtStartup: "extensions.torlauncher.prompt_at_startup",
});
const TorTopics = Object.freeze({
BootstrapError: "TorBootstrapError",
HasWarnOrErr: "TorLogHasWarnOrErr",
ProcessExited: "TorProcessExited",
ProcessIsReady: "TorProcessIsReady",
ProcessRestarted: "TorProcessRestarted",
});
const ControlConnTimings = Object.freeze({
initialDelayMS: 25, // Wait 25ms after the process has started, before trying to connect
maxRetryMS: 10000, // Retry at most every 10 seconds
timeoutMS: 5 * 60 * 1000, // Wait at most 5 minutes for tor to start
});
/**
* This service monitors an existing Tor instance, or starts one, if needed, and
* then starts monitoring it.
*
* This is the service which should be queried to know information about the
* status of the bootstrap, the logs, etc...
*/
const TorMonitorService = {
_connection: null,
_eventsToMonitor: Object.freeze(["STATUS_CLIENT", "NOTICE", "WARN", "ERR"]),
_torLog: [], // Array of objects with date, type, and msg properties.
_startTimeout: null,
_isBootstrapDone: false,
_bootstrapErrorOccurred: false,
_lastWarningPhase: null,
_lastWarningReason: null,
_torProcess: null,
_inited: false,
// Public methods
// Starts Tor, if needed, and starts monitoring for events
init() {
if (this._inited) {
return;
}
this._inited = true;
if (this.ownsTorDaemon) {
this._controlTor();
} else {
logger.info(
"Not starting the event monitor, as we do not own the Tor daemon."
);
}
logger.debug("TorMonitorService initialized");
},
// Closes the connection that monitors for events.
// When Tor is started by Tor Browser, it is configured to exit when the
// control connection is closed. Therefore, as a matter of facts, calling this
// function also makes the child Tor instance stop.
uninit() {
if (this._torProcess) {
this._torProcess.forget();
this._torProcess.onExit = null;
this._torProcess.onRestart = null;
this._torProcess = null;
}
this._shutDownEventMonitor();
},
async retrieveBootstrapStatus() {
if (!this._connection) {
throw new Error("Event monitor connection not available");
}
// TODO: Unify with TorProtocolService.sendCommand and put everything in the
// reviewed torbutton replacement.
const cmd = "GETINFO";
const key = "status/bootstrap-phase";
let reply = await this._connection.sendCommand(`${cmd} ${key}`);
if (!reply) {
throw new Error("We received an empty reply");
}
// A typical reply looks like:
// 250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
// 250 OK
reply = TorParsers.parseCommandResponse(reply);
if (!TorParsers.commandSucceeded(reply)) {
throw new Error(`${cmd} failed`);
}
reply = TorParsers.parseReply(cmd, key, reply);
if (reply.lineArray) {
this._processBootstrapStatus(reply.lineArray[0], true);
}
},
// Returns captured log message as a text string (one message per line).
getLog() {
return this._torLog
.map(logObj => {
const timeStr = logObj.date
.toISOString()
.replace("T", " ")
.replace("Z", "");
return `${timeStr} [${logObj.type}] ${logObj.msg}`;
})
.join(TorLauncherUtil.isWindows ? "\r\n" : "\n");
},
// true if we launched and control tor, false if using system tor
get ownsTorDaemon() {
return TorLauncherUtil.shouldStartAndOwnTor;
},
get isBootstrapDone() {
return this._isBootstrapDone;
},
get bootstrapErrorOccurred() {
return this._bootstrapErrorOccurred;
},
clearBootstrapError() {
this._bootstrapErrorOccurred = false;
this._lastWarningPhase = null;
this._lastWarningReason = null;
},
// This should be used for debug only
setBootstrapError() {
this._bootstrapErrorOccurred = true;
},
get isRunning() {
return !!this._connection;
},
// Private methods
async _startProcess() {
// TorProcess should be instanced once, then always reused and restarted
// only through the prompt it exposes when the controlled process dies.
if (!this._torProcess) {
this._torProcess = new TorProcess();
this._torProcess.onExit = () => {
this._shutDownEventMonitor();
Services.obs.notifyObservers(null, TorTopics.ProcessExited);
};
this._torProcess.onRestart = async () => {
this._shutDownEventMonitor();
await this._controlTor();
Services.obs.notifyObservers(null, TorTopics.ProcessRestarted);
};
}
// Already running, but we did not start it
if (this._torProcess.isRunning) {
return false;
}
try {
await this._torProcess.start();
if (this._torProcess.isRunning) {
logger.info("tor started");
}
} catch (e) {
// TorProcess already logs the error.
this._bootstrapErrorOccurred = true;
this._lastWarningPhase = "startup";
this._lastWarningReason = e.toString();
}
return this._torProcess.isRunning;
},
async _controlTor() {
if (!this._torProcess?.isRunning && !(await this._startProcess())) {
logger.error("Tor not running, not starting to monitor it.");
return;
}
let delayMS = ControlConnTimings.initialDelayMS;
const callback = async () => {
if (await this._startEventMonitor()) {
this.retrieveBootstrapStatus().catch(e => {
logger.warn("Could not get the initial bootstrap status", e);
});
// FIXME: TorProcess is misleading here. We should use a topic related
// to having a control port connection, instead.
Services.obs.notifyObservers(null, TorTopics.ProcessIsReady);
logger.info(`Notified ${TorTopics.ProcessIsReady}`);
// We reset this here hoping that _shutDownEventMonitor can interrupt
// the current monitor, either by calling clearTimeout and preventing it
// from starting, or by closing the control port connection.
if (this._startTimeout === null) {
logger.warn("Someone else reset _startTimeout!");
}
this._startTimeout = null;
} else if (
Date.now() - this._torProcessStartTime >
ControlConnTimings.timeoutMS
) {
let s = TorLauncherUtil.getLocalizedString("tor_controlconn_failed");
this._bootstrapErrorOccurred = true;
this._lastWarningPhase = "startup";
this._lastWarningReason = s;
logger.info(s);
if (this._startTimeout === null) {
logger.warn("Someone else reset _startTimeout!");
}
this._startTimeout = null;
} else {
delayMS *= 2;
if (delayMS > ControlConnTimings.maxRetryMS) {
delayMS = ControlConnTimings.maxRetryMS;
}
this._startTimeout = setTimeout(() => {
logger.debug(`Control port not ready, waiting ${delayMS / 1000}s.`);
callback();
}, delayMS);
}
};
// Check again, in the unfortunate case in which the execution was alrady
// queued, but was waiting network code.
if (this._startTimeout === null) {
this._startTimeout = setTimeout(callback, delayMS);
} else {
logger.error("Possible race? Refusing to start the timeout again");
}
},
async _startEventMonitor() {
if (this._connection) {
return true;
}
let conn;
try {
const avoidCache = true;
conn = await controller(avoidCache);
} catch (e) {
logger.error("Cannot open a control port connection", e);
if (conn) {
try {
conn.close();
} catch (e) {
logger.error(
"Also, the connection is not null but cannot be closed",
e
);
}
}
return false;
}
// TODO: optionally monitor INFO and DEBUG log messages.
let reply = await conn.sendCommand(
"SETEVENTS " + this._eventsToMonitor.join(" ")
);
reply = TorParsers.parseCommandResponse(reply);
if (!TorParsers.commandSucceeded(reply)) {
logger.error("SETEVENTS failed");
conn.close();
return false;
}
// FIXME: At the moment it is not possible to start the event monitor
// when we do start the tor process. So, does it make sense to keep this
// control?
if (this._torProcess) {
this._torProcess.connectionWorked();
}
if (!TorLauncherUtil.shouldOnlyConfigureTor) {
try {
await this._takeTorOwnership(conn);
} catch (e) {
logger.warn("Could not take ownership of the Tor daemon", e);
}
}
this._connection = conn;
this._waitForEventData();
return true;
},
// Try to become the primary controller (TAKEOWNERSHIP).
async _takeTorOwnership(conn) {
const takeOwnership = "TAKEOWNERSHIP";
let reply = await conn.sendCommand(takeOwnership);
reply = TorParsers.parseCommandResponse(reply);
if (!TorParsers.commandSucceeded(reply)) {
logger.warn("Take ownership failed");
} else {
const resetConf = "RESETCONF __OwningControllerProcess";
reply = await conn.sendCommand(resetConf);
reply = TorParsers.parseCommandResponse(reply);
if (!TorParsers.commandSucceeded(reply)) {
logger.warn("Clear owning controller process failed");
}
}
},
_waitForEventData() {
if (!this._connection) {
return;
}
logger.debug("Start watching events:", this._eventsToMonitor);
let replyObj = {};
for (const torEvent of this._eventsToMonitor) {
this._connection.watchEvent(
torEvent,
null,
line => {
if (!line) {
return;
}
logger.debug("Event response: ", line);
const isComplete = TorParsers.parseReplyLine(line, replyObj);
if (isComplete) {
this._processEventReply(replyObj);
replyObj = {};
}
},
true
);
}
},
_processEventReply(aReply) {
if (aReply._parseError || !aReply.lineArray.length) {
return;
}
if (aReply.statusCode !== TorStatuses.EventNotification) {
logger.warn("Unexpected event status code:", aReply.statusCode);
return;
}
// TODO: do we need to handle multiple lines?
const s = aReply.lineArray[0];
const idx = s.indexOf(" ");
if (idx === -1) {
return;
}
const eventType = s.substring(0, idx);
const msg = s.substring(idx + 1).trim();
if (eventType === "STATUS_CLIENT") {
this._processBootstrapStatus(msg, false);
return;
} else if (!this._eventsToMonitor.includes(eventType)) {
logger.debug(`Dropping unlistened event ${eventType}`);
return;
}
if (eventType === "WARN" || eventType === "ERR") {
// Notify so that Copy Log can be enabled.
Services.obs.notifyObservers(null, TorTopics.HasWarnOrErr);
}
const now = new Date();
const maxEntries = Services.prefs.getIntPref(
"extensions.torlauncher.max_tor_log_entries",
1000
);
if (maxEntries > 0 && this._torLog.length >= maxEntries) {
this._torLog.splice(0, 1);
}
this._torLog.push({ date: now, type: eventType, msg });
const logString = `Tor ${eventType}: ${msg}`;
logger.info(logString);
},
// Process a bootstrap status to update the current state, and broadcast it
// to TorBootstrapStatus observers.
// If aSuppressErrors is true, errors are ignored. This is used when we
// are handling the response to a "GETINFO status/bootstrap-phase" command.
_processBootstrapStatus(aStatusMsg, aSuppressErrors) {
const statusObj = TorParsers.parseBootstrapStatus(aStatusMsg);
if (!statusObj) {
return;
}
// Notify observers
statusObj.wrappedJSObject = statusObj;
Services.obs.notifyObservers(statusObj, "TorBootstrapStatus");
if (statusObj.PROGRESS === 100) {
this._isBootstrapDone = true;
this._bootstrapErrorOccurred = false;
try {
Services.prefs.setBoolPref(Preferences.PromptAtStartup, false);
} catch (e) {
logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
}
return;
}
this._isBootstrapDone = false;
if (
statusObj.TYPE === "WARN" &&
statusObj.RECOMMENDATION !== "ignore" &&
!aSuppressErrors
) {
this._notifyBootstrapError(statusObj);
}
},
_notifyBootstrapError(statusObj) {
this._bootstrapErrorOccurred = true;
try {
Services.prefs.setBoolPref(Preferences.PromptAtStartup, true);
} catch (e) {
logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
}
const phase = TorLauncherUtil.getLocalizedBootstrapStatus(statusObj, "TAG");
const reason = TorLauncherUtil.getLocalizedBootstrapStatus(
statusObj,
"REASON"
);
const details = TorLauncherUtil.getFormattedLocalizedString(
"tor_bootstrap_failed_details",
[phase, reason],
2
);
logger.error(
`Tor bootstrap error: [${statusObj.TAG}/${statusObj.REASON}] ${details}`
);
if (
statusObj.TAG !== this._lastWarningPhase ||
statusObj.REASON !== this._lastWarningReason
) {
this._lastWarningPhase = statusObj.TAG;
this._lastWarningReason = statusObj.REASON;
const message = TorLauncherUtil.getLocalizedString(
"tor_bootstrap_failed"
);
Services.obs.notifyObservers(
{ message, details },
TorTopics.BootstrapError
);
}
},
_shutDownEventMonitor() {
this._connection?.close();
this._connection = null;
if (this._startTimeout !== null) {
clearTimeout(this._startTimeout);
this._startTimeout = null;
}
this._isBootstrapDone = false;
this.clearBootstrapError();
},
};
// Copyright (c) 2022, The Tor Project, Inc.
"use strict";
var EXPORTED_SYMBOLS = ["TorParsers", "TorStatuses"];
const TorStatuses = Object.freeze({
OK: 250,
EventNotification: 650,
});
const TorParsers = Object.freeze({
commandSucceeded(aReply) {
return aReply?.statusCode === TorStatuses.OK;
},
// parseReply() understands simple GETCONF and GETINFO replies.
parseReply(aCmd, aKey, aReply) {
if (!aCmd || !aKey || !aReply) {
return [];
}
const lcKey = aKey.toLowerCase();
const prefix = lcKey + "=";
const prefixLen = prefix.length;
const tmpArray = [];
for (const line of aReply.lineArray) {
var lcLine = line.toLowerCase();
if (lcLine === lcKey) {
tmpArray.push("");
} else if (lcLine.indexOf(prefix) !== 0) {
console.warn(`Unexpected ${aCmd} response: ${line}`);
} else {
try {
let s = this.unescapeString(line.substring(prefixLen));
tmpArray.push(s);
} catch (e) {
console.warn(
`Error while unescaping the response of ${aCmd}: ${line}`,
e
);
}
}
}
aReply.lineArray = tmpArray;
return aReply;
},
// Returns false if more lines are needed. The first time, callers
// should pass an empty aReplyObj.
// Parsing errors are indicated by aReplyObj._parseError = true.
parseReplyLine(aLine, aReplyObj) {
if (!aLine || !aReplyObj) {
return false;
}
if (!("_parseError" in aReplyObj)) {
aReplyObj.statusCode = 0;
aReplyObj.lineArray = [];
aReplyObj._parseError = false;
}
if (aLine.length < 4) {
console.error("Unexpected response: ", aLine);
aReplyObj._parseError = true;
return true;
}
// TODO: handle + separators (data)
aReplyObj.statusCode = parseInt(aLine.substring(0, 3), 10);
const s = aLine.length < 5 ? "" : aLine.substring(4);
// Include all lines except simple "250 OK" ones.
if (aReplyObj.statusCode !== TorStatuses.OK || s !== "OK") {
aReplyObj.lineArray.push(s);
}
return aLine.charAt(3) === " ";
},
// Split aStr at spaces, accounting for quoted values.
// Returns an array of strings.
splitReplyLine(aStr) {
// Notice: the original function did not check for escaped quotes.
return aStr
.split('"')
.flatMap((token, index) => {
const inQuotedStr = index % 2 === 1;
return inQuotedStr ? `"${token}"` : token.split(" ");
})
.filter(s => s);
},
// Helper function for converting a raw controller response into a parsed object.
parseCommandResponse(reply) {
if (!reply) {
return {};
}
const lines = reply.split("\r\n");
const rv = {};
for (const line of lines) {
if (this.parseReplyLine(line, rv) || rv._parseError) {
break;
}
}
return rv;
},
// If successful, returns a JS object with these fields:
// status.TYPE -- "NOTICE" or "WARN"
// status.PROGRESS -- integer
// status.TAG -- string
// status.SUMMARY -- string
// status.WARNING -- string (optional)
// status.REASON -- string (optional)
// status.COUNT -- integer (optional)
// status.RECOMMENDATION -- string (optional)
// status.HOSTADDR -- string (optional)
// Returns null upon failure.
parseBootstrapStatus(aStatusMsg) {
if (!aStatusMsg || !aStatusMsg.length) {
return null;
}
let sawBootstrap = false;
const statusObj = {};
statusObj.TYPE = "NOTICE";
// The following code assumes that this is a one-line response.
for (const tokenAndVal of this.splitReplyLine(aStatusMsg)) {
let token, val;
const idx = tokenAndVal.indexOf("=");
if (idx < 0) {
token = tokenAndVal;
} else {
token = tokenAndVal.substring(0, idx);
try {
val = TorParsers.unescapeString(tokenAndVal.substring(idx + 1));
} catch (e) {
console.debug("Could not parse the token value", e);
}
if (!val) {
// skip this token/value pair.
continue;
}
}
switch (token) {
case "BOOTSTRAP":
sawBootstrap = true;
break;
case "WARN":
case "NOTICE":
case "ERR":
statusObj.TYPE = token;
break;
case "COUNT":
case "PROGRESS":
statusObj[token] = parseInt(val, 10);
break;
default:
statusObj[token] = val;
break;
}
}
if (!sawBootstrap) {
if (statusObj.TYPE === "NOTICE") {
console.info(aStatusMsg);
} else {
console.warn(aStatusMsg);
}
return null;
}
return statusObj;
},
// 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
.replace("\\", "\\\\")
.replace('"', '\\"')
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
.replace(/[^\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;
},
});
"use strict";
var EXPORTED_SYMBOLS = ["TorProcess"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { Subprocess } = ChromeUtils.import(
"resource://gre/modules/Subprocess.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"TorProtocolService",
"resource://gre/modules/TorProtocolService.jsm"
);
const { TorLauncherUtil } = ChromeUtils.import(
"resource://gre/modules/TorLauncherUtil.jsm"
);
const { TorParsers } = ChromeUtils.import(
"resource://gre/modules/TorParsers.jsm"
);
const TorProcessStatus = Object.freeze({
Unknown: 0,
Starting: 1,
Running: 2,
Exited: 3,
});
// Logger adapted from CustomizableUI.jsm
XPCOMUtils.defineLazyGetter(this, "logger", () => {
const { ConsoleAPI } = ChromeUtils.import(
"resource://gre/modules/Console.jsm"
);
// TODO: Use a preference to set the log level.
const consoleOptions = {
maxLogLevel: "info",
prefix: "TorProcess",
};
return new ConsoleAPI(consoleOptions);
});
class TorProcess {
_exeFile = null;
_dataDir = null;
_args = [];
_subprocess = null;
_status = TorProcessStatus.Unknown;
_torProcessStartTime = null; // JS Date.now()
_didConnectToTorControlPort = false; // Have we ever made a connection?
onExit = null;
onRestart = null;
get status() {
return this._status;
}
get isRunning() {
return (
this._status === TorProcessStatus.Starting ||
this._status === TorProcessStatus.Running
);
}
async start() {
if (this._subprocess) {
return;
}
await this._fixupTorrc();
this._status = TorProcessStatus.Unknown;
try {
this._makeArgs();
this._addControlPortArg();
this._addSocksPortArg();
const pid = Services.appinfo.processID;
if (pid !== 0) {
this._args.push("__OwningControllerProcess");
this._args.push("" + pid);
}
if (TorLauncherUtil.shouldShowNetworkSettings) {
this._args.push("DisableNetwork");
this._args.push("1");
}
this._status = TorProcessStatus.Starting;
this._didConnectToTorControlPort = false;
// 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",
};
if (TorLauncherUtil.isMac) {
// On macOS, we specify pluggable transport relative to the tor
// executable.
options.workdir = this._exeFile.parent.path;
}
this._subprocess = await Subprocess.call(options);
this._dumpStdout();
this._watchProcess();
this._status = TorProcessStatus.Running;
this._torProcessStartTime = Date.now();
} catch (e) {
this._status = TorProcessStatus.Exited;
this._subprocess = null;
logger.error("startTor error:", e);
throw e;
}
}
// 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;
}
// The owner of the process can use this function to tell us that they
// successfully connected to the control port. This information will be used
// only to decide which text to show in the confirmation dialog if tor exits.
connectionWorked() {
this._didConnectToTorControlPort = true;
}
async _dumpStdout() {
let string;
while (
this._subprocess &&
(string = await this._subprocess.stdout.readString())
) {
dump(string);
}
}
async _watchProcess() {
const watched = this._subprocess;
if (!watched) {
return;
}
try {
const { exitCode } = await watched.wait();
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();
}
}
_processExitedUnexpectedly() {
this._subprocess = null;
this._status = TorProcessStatus.Exited;
// TODO: Move this logic somewhere else?
let s;
if (!this._didConnectToTorControlPort) {
// tor might be misconfigured, becauser we could never connect to it
const key = "tor_exited_during_startup";
s = TorLauncherUtil.getLocalizedString(key);
} else {
// tor exited suddenly, so configuration should be okay
s =
TorLauncherUtil.getLocalizedString("tor_exited") +
"\n\n" +
TorLauncherUtil.getLocalizedString("tor_exited2");
}
logger.info(s);
const defaultBtnLabel = TorLauncherUtil.getLocalizedString("restart_tor");
let cancelBtnLabel = "OK";
try {
const kSysBundleURI = "chrome://global/locale/commonDialogs.properties";
const sysBundle = Services.strings.createBundle(kSysBundleURI);
cancelBtnLabel = sysBundle.GetStringFromName(cancelBtnLabel);
} catch (e) {
logger.warn("Could not localize the cancel button", e);
}
const restart = TorLauncherUtil.showConfirm(
null,
s,
defaultBtnLabel,
cancelBtnLabel
);
if (restart) {
this.start().then(() => {
if (this.onRestart) {
this.onRestart();
}
});
} else if (this.onExit) {
this.onExit();
}
}
_makeArgs() {
// Ideally, we would cd to the Firefox application directory before
// starting tor (but we don't know how to do that). Instead, we
// rely on the TBB launcher to start Firefox from the right place.
// Get the Tor data directory first so it is created before we try to
// construct paths to files that will be inside it.
this._exeFile = TorLauncherUtil.getTorFile("tor", false);
const torrcFile = TorLauncherUtil.getTorFile("torrc", true);
this._dataDir = TorLauncherUtil.getTorFile("tordatadir", true);
const onionAuthDir = TorLauncherUtil.getTorFile("toronionauthdir", true);
const hashedPassword = TorProtocolService.torGetPassword(true);
let detailsKey;
if (!this._exeFile) {
detailsKey = "tor_missing";
} else if (!torrcFile) {
detailsKey = "torrc_missing";
} else if (!this._dataDir) {
detailsKey = "datadir_missing";
} else if (!onionAuthDir) {
detailsKey = "onionauthdir_missing";
} else if (!hashedPassword) {
detailsKey = "password_hash_missing";
}
if (detailsKey) {
const details = TorLauncherUtil.getLocalizedString(detailsKey);
const key = "unable_to_start_tor";
const err = TorLauncherUtil.getFormattedLocalizedString(
key,
[details],
1
);
throw new Error(err);
}
const torrcDefaultsFile = TorLauncherUtil.getTorFile(
"torrc-defaults",
false
);
// The geoip and geoip6 files are in the same directory as torrc-defaults.
const geoipFile = torrcDefaultsFile.clone();
geoipFile.leafName = "geoip";
const geoip6File = torrcDefaultsFile.clone();
geoip6File.leafName = "geoip6";
this._args = [];
if (torrcDefaultsFile) {
this._args.push("--defaults-torrc");
this._args.push(torrcDefaultsFile.path);
}
this._args.push("-f");
this._args.push(torrcFile.path);
this._args.push("DataDirectory");
this._args.push(this._dataDir.path);
this._args.push("ClientOnionAuthDir");
this._args.push(onionAuthDir.path);
this._args.push("GeoIPFile");
this._args.push(geoipFile.path);
this._args.push("GeoIPv6File");
this._args.push(geoip6File.path);
this._args.push("HashedControlPassword");
this._args.push(hashedPassword);
}
_addControlPortArg() {
// Include a ControlPort argument to support switching between
// a TCP port and an IPC port (e.g., a Unix domain socket). We
// include a "+__" prefix so that (1) this control port is added
// to any control ports that the user has defined in their torrc
// file and (2) it is never written to torrc.
let controlPortArg;
const controlIPCFile = TorProtocolService.torGetControlIPCFile();
const controlPort = TorProtocolService.torGetControlPort();
if (controlIPCFile) {
controlPortArg = this._ipcPortArg(controlIPCFile);
} else if (controlPort) {
controlPortArg = "" + controlPort;
}
if (controlPortArg) {
this._args.push("+__ControlPort");
this._args.push(controlPortArg);
}
}
_addSocksPortArg() {
// Include a SocksPort argument to support switching between
// a TCP port and an IPC port (e.g., a Unix domain socket). We
// include a "+__" prefix so that (1) this SOCKS port is added
// to any SOCKS ports that the user has defined in their torrc
// file and (2) it is never written to torrc.
const socksPortInfo = TorProtocolService.torGetSOCKSPortInfo();
if (socksPortInfo) {
let socksPortArg;
if (socksPortInfo.ipcFile) {
socksPortArg = this._ipcPortArg(socksPortInfo.ipcFile);
} else if (socksPortInfo.host && socksPortInfo.port != 0) {
socksPortArg = socksPortInfo.host + ":" + socksPortInfo.port;
}
if (socksPortArg) {
let socksPortFlags = Services.prefs.getCharPref(
"extensions.torlauncher.socks_port_flags",
"IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth"
);
if (socksPortFlags) {
socksPortArg += " " + socksPortFlags;
}
this._args.push("+__SocksPort");
this._args.push(socksPortArg);
}
}
}
// Return a ControlPort or SocksPort argument for aIPCFile (an nsIFile).
// The result is unix:/path or unix:"/path with spaces" with appropriate
// C-style escaping within the path portion.
_ipcPortArg(aIPCFile) {
return "unix:" + TorParsers.escapeString(aIPCFile.path);
}
async _fixupTorrc() {
// If we have not already done so, remove any ControlPort and SocksPort
// lines from the user's torrc file that may conflict with the arguments
// we plan to pass when starting tor.
// See bugs 20761 and 22283.
const kTorrcFixupVersion = 2;
const kTorrcFixupPref = "extensions.torlauncher.torrc_fixup_version";
if (Services.prefs.getIntPref(kTorrcFixupPref, 0) > kTorrcFixupVersion) {
return true;
}
let torrcFile = TorLauncherUtil.getTorFile("torrc", true);
if (!torrcFile) {
// No torrc file; nothing to fixup.
return true;
}
torrcFile = torrcFile.path;
let torrcStr;
try {
torrcStr = await IOUtils.readUTF8(torrcFile);
} catch (e) {
logger.error(`Could not read ${torrcFile}:`, e);
return false;
}
if (!torrcStr.length) {
return true;
}
const controlIPCFile = TorProtocolService.torGetControlIPCFile();
const controlPort = TorProtocolService.torGetControlPort();
const socksPortInfo = TorProtocolService.torGetSOCKSPortInfo();
const valueIsUnixDomainSocket = aValue => {
// Handle several cases:
// "unix:/path options"
// unix:"/path" options
// unix:/path options
if (aValue.startsWith('"')) {
aValue = TorParsers.unescapeString(aValue);
}
return aValue.startsWith("unix:");
};
const valueContainsPort = (aValue, aPort) => {
// Check for a match, ignoring "127.0.0.1" and "localhost" prefixes.
let val = TorParsers.unescapeString(aValue);
const pieces = val.split(":");
if (
pieces.length >= 2 &&
(pieces[0] === "127.0.0.1" || pieces[0].toLowerCase() === "localhost")
) {
val = pieces[1];
}
return aPort === parseInt(val);
};
let removedLinesCount = 0;
const revisedLines = [];
const lines = this._joinContinuedTorrcLines(torrcStr);
lines.forEach(aLine => {
let removeLine = false;
// Look for "+ControlPort value" or "ControlPort value", skipping leading
// whitespace and ignoring case.
let matchResult = aLine.match(/\s*\+*controlport\s+(.*)/i);
if (matchResult) {
removeLine = valueIsUnixDomainSocket(matchResult[1]);
if (!removeLine && !controlIPCFile) {
removeLine = valueContainsPort(matchResult[1], controlPort);
}
} else if (socksPortInfo) {
// Look for "+SocksPort value" or "SocksPort value", skipping leading
// whitespace and ignoring case.
matchResult = aLine.match(/\s*\+*socksport\s+(.*)/i);
if (matchResult) {
removeLine = valueIsUnixDomainSocket(matchResult[1]);
if (!removeLine && !socksPortInfo.ipcFile) {
removeLine = valueContainsPort(matchResult[1], socksPortInfo.port);
}
}
}
if (removeLine) {
++removedLinesCount;
logger.info(`fixupTorrc: removing ${aLine}`);
} else {
revisedLines.push(aLine);
}
});
if (removedLinesCount > 0) {
const data = new TextEncoder().encode(revisedLines.join("\n"));
try {
await IOUtils.write(torrcFile, data, {
tmpPath: torrcFile + ".tmp",
});
} catch (e) {
logger.error(`Failed to overwrite file ${torrcFile}:`, e);
return false;
}
logger.info(
`fixupTorrc: removed ${removedLinesCount} configuration options`
);
}
Services.prefs.setIntPref(kTorrcFixupPref, kTorrcFixupVersion);
return true;
}
// Split aTorrcStr into lines, joining continued lines.
_joinContinuedTorrcLines(aTorrcStr) {
const lines = [];
const rawLines = aTorrcStr.split("\n");
let isContinuedLine = false;
let tmpLine;
rawLines.forEach(aLine => {
let len = aLine.length;
// Strip trailing CR if present.
if (len > 0 && aLine.substr(len - 1) === "\r") {
--len;
aLine = aLine.substr(0, len);
}
// Check for a continued line. This is indicated by a trailing \ or, if
// we are already within a continued line sequence, a trailing comment.
if (len > 0 && aLine.substr(len - 1) === "\\") {
--len;
aLine = aLine.substr(0, len);
// If this is the start of a continued line and it only contains a
// keyword (i.e., no spaces are present), append a space so that
// the keyword will be recognized (as it is by tor) after we join
// the pieces of the continued line into one line.
if (!isContinuedLine && !aLine.includes(" ")) {
aLine += " ";
}
isContinuedLine = true;
} else if (isContinuedLine) {
if (!len) {
isContinuedLine = false;
} else {
// Check for a comment. According to tor's doc/torrc_format.txt,
// comments do not terminate a sequence of continued lines.
let idx = aLine.indexOf("#");
if (idx < 0) {
isContinuedLine = false; // Not a comment; end continued line.
} else {
// Remove trailing comment from continued line. The continued
// line sequence continues.
aLine = aLine.substr(0, idx);
}
}
}
if (isContinuedLine) {
if (tmpLine) {
tmpLine += aLine;
} else {
tmpLine = aLine;
}
} else if (tmpLine) {
lines.push(tmpLine + aLine);
tmpLine = undefined;
} else {
lines.push(aLine);
}
});
return lines;
}
}
This diff is collapsed.
"use strict";
var EXPORTED_SYMBOLS = ["TorStartupService"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
// We will use the modules only when the profile is loaded, so prefer lazy
// loading
ChromeUtils.defineModuleGetter(
this,
"TorLauncherUtil",
"resource://gre/modules/TorLauncherUtil.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"TorMonitorService",
"resource://gre/modules/TorMonitorService.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"TorProtocolService",
"resource://gre/modules/TorProtocolService.jsm"
);
/* 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.
class TorStartupService {
_defaultPreferencesAreLoaded = false;
observe(aSubject, aTopic, aData) {
if (aTopic === BrowserTopics.ProfileAfterChange && !gInited) {
this._init();
} else if (aTopic === BrowserTopics.QuitApplicationGranted) {
this._uninit();
}
}
async _init() {
Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
// Starts TorProtocolService first, because it configures the controller
// factory, too.
await TorProtocolService.init();
TorMonitorService.init();
gInited = true;
}
_uninit() {
Services.obs.removeObserver(this, BrowserTopics.QuitApplicationGranted);
// Close any helper connection first...
TorProtocolService.uninit();
// ... and only then closes the event monitor connection, which will cause
// Tor to stop.
TorMonitorService.uninit();
TorLauncherUtil.cleanupTempDirectories();
}
}
Classes = [
{
"cid": "{df46c65d-be2b-4d16-b280-69733329eecf}",
"contract_ids": [
"@torproject.org/tor-startup-service;1"
],
"jsm": "resource://gre/modules/TorStartupService.jsm",
"constructor": "TorStartupService",
},
]
EXTRA_JS_MODULES += [
"TorBootstrapRequest.jsm",
"TorLauncherUtil.jsm",
"TorMonitorService.jsm",
"TorParsers.jsm",
"TorProcess.jsm",
"TorProtocolService.jsm",
"TorStartupService.jsm",
]
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.
Finish editing this message first!
Please register or to comment