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

fixup! Bug 40933: Add tor-launcher functionality

Make TorProtocolService an ES class, and change _ with actual private
stuff.
parent 9722ca26
No related branches found
No related tags found
1 merge request!716Bug 40938 (part 2): Merge TorProtocolService and TorMonitorService
......@@ -8,12 +8,6 @@ import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs"
const lazy = {};
ChromeUtils.defineModuleGetter(
lazy,
"FileUtils",
"resource://gre/modules/FileUtils.jsm"
);
ChromeUtils.defineModuleGetter(
lazy,
"TorMonitorService",
......@@ -22,6 +16,7 @@ ChromeUtils.defineModuleGetter(
ChromeUtils.defineESModuleGetters(lazy, {
controller: "resource://gre/modules/TorControlPort.sys.mjs",
configureControlPortModule: "resource://gre/modules/TorControlPort.sys.mjs",
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
});
const TorTopics = Object.freeze({
......@@ -56,52 +51,52 @@ const logger = new ConsoleAPI({
// reply.statusCode -- integer, e.g., 250
// reply.lineArray -- an array of strings returned by tor
// For GetConf calls, the aKey prefix is removed from the lineArray strings.
export const TorProtocolService = {
_inited: false,
class TorProvider {
#inited = false;
// Maintain a map of tor settings set by Tor Browser so that we don't
// repeatedly set the same key/values over and over.
// This map contains string keys to primitives or array values.
_settingsCache: new Map(),
#settingsCache = new Map();
_controlPort: null,
_controlHost: null,
_controlIPCFile: null, // An nsIFile if using IPC for control port.
_controlPassword: null, // JS string that contains hex-encoded password.
_SOCKSPortInfo: null, // An object that contains ipcFile, host, port.
#controlPort = null;
#controlHost = null;
#controlIPCFile = null; // An nsIFile if using IPC for control port.
#controlPassword = null; // JS string that contains hex-encoded password.
#SOCKSPortInfo = null; // An object that contains ipcFile, host, port.
_controlConnection: null, // This is cached and reused.
_connectionQueue: [],
#controlConnection = null; // This is cached and reused.
#connectionQueue = [];
// Public methods
async init() {
if (this._inited) {
if (this.#inited) {
return;
}
this._inited = true;
this.#inited = true;
Services.obs.addObserver(this, TorTopics.ProcessExited);
Services.obs.addObserver(this, TorTopics.ProcessRestarted);
await this._setSockets();
await this.#setSockets();
logger.debug("TorProtocolService initialized");
},
}
uninit() {
Services.obs.removeObserver(this, TorTopics.ProcessExited);
Services.obs.removeObserver(this, TorTopics.ProcessRestarted);
this._closeConnection();
},
this.#closeConnection();
}
observe(subject, topic, data) {
if (topic === TorTopics.ProcessExited) {
this._closeConnection();
this.#closeConnection();
} else if (topic === TorTopics.ProcessRestarted) {
this._reconnect();
this.#reconnect();
}
}
},
// takes a Map containing tor settings
// throws on error
......@@ -109,14 +104,14 @@ export const TorProtocolService = {
// only write settings that have changed
const newSettings = Array.from(aSettingsObj).filter(([setting, value]) => {
// make sure we have valid data here
this._assertValidSetting(setting, value);
this.#assertValidSetting(setting, value);
if (!this._settingsCache.has(setting)) {
if (!this.#settingsCache.has(setting)) {
// no cached setting, so write
return true;
}
const cachedValue = this._settingsCache.get(setting);
const cachedValue = this.#settingsCache.get(setting);
if (value === cachedValue) {
return false;
} else if (Array.isArray(value) && Array.isArray(cachedValue)) {
......@@ -142,21 +137,21 @@ export const TorProtocolService = {
// save settings to cache after successfully writing to Tor
for (const [setting, value] of newSettings) {
this._settingsCache.set(setting, value);
this.#settingsCache.set(setting, value);
}
}
}
},
async readStringArraySetting(aSetting) {
const value = await this._readSetting(aSetting);
this._settingsCache.set(aSetting, value);
const value = await this.#readSetting(aSetting);
this.#settingsCache.set(aSetting, value);
return value;
},
}
// writes current tor settings to disk
async flushSettings() {
await this.sendCommand("SAVECONF");
},
}
async connect() {
const kTorConfKeyDisableNetwork = "DisableNetwork";
......@@ -166,7 +161,7 @@ export const TorProtocolService = {
await this.sendCommand("SAVECONF");
lazy.TorMonitorService.clearBootstrapError();
lazy.TorMonitorService.retrieveBootstrapStatus();
},
}
async stopBootstrap() {
// Tell tor to disable use of the network; this should stop the bootstrap
......@@ -181,11 +176,11 @@ export const TorProtocolService = {
// so we do not await this. We just want to be notified when the bootstrap
// status is actually updated through observers.
lazy.TorMonitorService.retrieveBootstrapStatus();
},
}
async newnym() {
return this.sendCommand("SIGNAL NEWNYM");
},
}
// Ask tor which ports it is listening to for SOCKS connections.
// At the moment this is used only in TorCheckService.
......@@ -194,7 +189,7 @@ export const TorProtocolService = {
const keyword = "net/listeners/socks";
const response = await this.sendCommand(cmd, keyword);
return TorParsers.parseReply(cmd, keyword, response);
},
}
async getBridges() {
// Ideally, we would not need this function, because we should be the one
......@@ -203,10 +198,10 @@ export const TorProtocolService = {
// is the most reliable way of getting the configured bridges, at the
// moment. Also, we are using this for the circuit display, which should
// work also when we are not configuring the tor daemon, but just using it.
return this._withConnection(conn => {
return this.#withConnection(conn => {
return conn.getConf("bridge");
});
},
}
/**
* Returns tha data about a relay or a bridge.
......@@ -215,7 +210,7 @@ export const TorProtocolService = {
* @returns {NodeData}
*/
async getNodeInfo(id) {
return this._withConnection(async conn => {
return this.#withConnection(async conn => {
const node = {
fingerprint: id,
ipAddrs: [],
......@@ -259,62 +254,62 @@ export const TorProtocolService = {
}
return node;
});
},
}
async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) {
return this._withConnection(conn => {
return this.#withConnection(conn => {
return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent);
});
},
}
async onionAuthRemove(hsAddress) {
return this._withConnection(conn => {
return this.#withConnection(conn => {
return conn.onionAuthRemove(hsAddress);
});
},
}
async onionAuthViewKeys() {
return this._withConnection(conn => {
return this.#withConnection(conn => {
return conn.onionAuthViewKeys();
});
},
}
// TODO: transform the following 4 functions in getters.
// Returns Tor password string or null if an error occurs.
torGetPassword() {
return this._controlPassword;
},
return this.#controlPassword;
}
torGetControlIPCFile() {
return this._controlIPCFile?.clone();
},
return this.#controlIPCFile?.clone();
}
torGetControlPort() {
return this._controlPort;
},
return this.#controlPort;
}
torGetSOCKSPortInfo() {
return this._SOCKSPortInfo;
},
return this.#SOCKSPortInfo;
}
get torControlPortInfo() {
const info = {
password: this._controlPassword,
password: this.#controlPassword,
};
if (this._controlIPCFile) {
info.ipcFile = this._controlIPCFile?.clone();
if (this.#controlIPCFile) {
info.ipcFile = this.#controlIPCFile?.clone();
}
if (this._controlPort) {
info.host = this._controlHost;
info.port = this._controlPort;
if (this.#controlPort) {
info.host = this.#controlHost;
info.port = this.#controlPort;
}
return info;
},
}
get torSOCKSPortInfo() {
return this._SOCKSPortInfo;
},
return this.#SOCKSPortInfo;
}
// Public, but called only internally
......@@ -326,7 +321,7 @@ export const TorProtocolService = {
let timeout = 250;
let reply;
while (leftConnAttempts-- > 0) {
const response = await this._trySend(cmd, args, leftConnAttempts == 0);
const response = await this.#trySend(cmd, args, leftConnAttempts === 0);
if (response.connected) {
reply = response.reply;
break;
......@@ -360,7 +355,7 @@ export const TorProtocolService = {
}
return reply;
},
}
// Perform a SETCONF command.
// aSettingsObj should be a JavaScript object with keys (property values)
......@@ -398,39 +393,39 @@ export const TorProtocolService = {
}
await this.sendCommand("SETCONF", args.join(" "));
},
}
// Public, never called?
async readBoolSetting(aSetting) {
let value = await this._readBoolSetting(aSetting);
this._settingsCache.set(aSetting, value);
let value = await this.#readBoolSetting(aSetting);
this.#settingsCache.set(aSetting, value);
return value;
},
}
async readStringSetting(aSetting) {
let value = await this._readStringSetting(aSetting);
this._settingsCache.set(aSetting, value);
let value = await this.#readStringSetting(aSetting);
this.#settingsCache.set(aSetting, value);
return value;
},
}
// Private
async _setSockets() {
async #setSockets() {
try {
const isWindows = TorLauncherUtil.isWindows;
// Determine how Tor Launcher will connect to the Tor control port.
// Environment variables get top priority followed by preferences.
if (!isWindows && Services.env.exists("TOR_CONTROL_IPC_PATH")) {
const ipcPath = Services.env.get("TOR_CONTROL_IPC_PATH");
this._controlIPCFile = new lazy.FileUtils.File(ipcPath);
this.#controlIPCFile = new lazy.FileUtils.File(ipcPath);
} else {
// Check for TCP host and port environment variables.
if (Services.env.exists("TOR_CONTROL_HOST")) {
this._controlHost = Services.env.get("TOR_CONTROL_HOST");
this.#controlHost = Services.env.get("TOR_CONTROL_HOST");
}
if (Services.env.exists("TOR_CONTROL_PORT")) {
this._controlPort = parseInt(
this.#controlPort = parseInt(
Services.env.get("TOR_CONTROL_PORT"),
10
);
......@@ -442,20 +437,20 @@ export const TorProtocolService = {
"extensions.torlauncher.control_port_use_ipc",
false
);
if (!this._controlHost && !this._controlPort && useIPC) {
this._controlIPCFile = TorLauncherUtil.getTorFile(
if (!this.#controlHost && !this.#controlPort && useIPC) {
this.#controlIPCFile = TorLauncherUtil.getTorFile(
"control_ipc",
false
);
} else {
if (!this._controlHost) {
this._controlHost = Services.prefs.getCharPref(
if (!this.#controlHost) {
this.#controlHost = Services.prefs.getCharPref(
"extensions.torlauncher.control_host",
"127.0.0.1"
);
}
if (!this._controlPort) {
this._controlPort = Services.prefs.getIntPref(
if (!this.#controlPort) {
this.#controlPort = Services.prefs.getIntPref(
"extensions.torlauncher.control_port",
9151
);
......@@ -465,46 +460,46 @@ export const TorProtocolService = {
// Populate _controlPassword so it is available when starting tor.
if (Services.env.exists("TOR_CONTROL_PASSWD")) {
this._controlPassword = Services.env.get("TOR_CONTROL_PASSWD");
this.#controlPassword = Services.env.get("TOR_CONTROL_PASSWD");
} else if (Services.env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
// TODO: test this code path (TOR_CONTROL_COOKIE_AUTH_FILE).
const cookiePath = Services.env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
if (cookiePath) {
this._controlPassword = await this._readAuthenticationCookie(
this.#controlPassword = await this.#readAuthenticationCookie(
cookiePath
);
}
}
if (!this._controlPassword) {
this._controlPassword = this._generateRandomPassword();
if (!this.#controlPassword) {
this.#controlPassword = this.#generateRandomPassword();
}
this._SOCKSPortInfo = TorLauncherUtil.getPreferredSocksConfiguration();
TorLauncherUtil.setProxyConfiguration(this._SOCKSPortInfo);
this.#SOCKSPortInfo = TorLauncherUtil.getPreferredSocksConfiguration();
TorLauncherUtil.setProxyConfiguration(this.#SOCKSPortInfo);
// Set the global control port info parameters.
lazy.configureControlPortModule(
this._controlIPCFile,
this._controlHost,
this._controlPort,
this._controlPassword
this.#controlIPCFile,
this.#controlHost,
this.#controlPort,
this.#controlPassword
);
} catch (e) {
logger.error("Failed to get environment variables", e);
}
},
}
_assertValidSettingKey(aSetting) {
#assertValidSettingKey(aSetting) {
// ensure the 'key' is a string
if (typeof aSetting !== "string") {
throw new Error(
`Expected setting of type string but received ${typeof aSetting}`
);
}
},
}
_assertValidSetting(aSetting, aValue) {
this._assertValidSettingKey(aSetting);
#assertValidSetting(aSetting, aValue) {
this.#assertValidSettingKey(aSetting);
switch (typeof aValue) {
case "boolean":
case "string":
......@@ -528,29 +523,29 @@ export const TorProtocolService = {
`Invalid object type received for setting '${aSetting}'`
);
}
},
}
// Perform a GETCONF command.
async _readSetting(aSetting) {
this._assertValidSettingKey(aSetting);
async #readSetting(aSetting) {
this.#assertValidSettingKey(aSetting);
const cmd = "GETCONF";
let reply = await this.sendCommand(cmd, aSetting);
return TorParsers.parseReply(cmd, aSetting, reply);
},
}
async _readStringSetting(aSetting) {
let lineArray = await this._readSetting(aSetting);
async #readStringSetting(aSetting) {
let lineArray = await this.#readSetting(aSetting);
if (lineArray.length !== 1) {
throw new Error(
`Expected an array with length 1 but received array of length ${lineArray.length}`
);
}
return lineArray[0];
},
}
async _readBoolSetting(aSetting) {
const value = this._readStringSetting(aSetting);
async #readBoolSetting(aSetting) {
const value = this.#readStringSetting(aSetting);
switch (value) {
case "0":
return false;
......@@ -559,16 +554,16 @@ export const TorProtocolService = {
default:
throw new Error(`Expected boolean (1 or 0) but received '${value}'`);
}
},
}
async _trySend(cmd, args, rethrow) {
async #trySend(cmd, args, rethrow) {
let connected = false;
let reply;
let leftAttempts = 2;
while (leftAttempts-- > 0) {
let conn;
try {
conn = await this._getConnection();
conn = await this.#getConnection();
} catch (e) {
logger.error("Cannot get a connection to the control port", e);
if (leftAttempts == 0 && rethrow) {
......@@ -584,105 +579,105 @@ export const TorProtocolService = {
reply = await conn.sendCommand(cmd + (args ? " " + args : ""));
if (reply) {
// Return for reuse.
this._returnConnection();
this.#returnConnection();
} else {
// Connection is bad.
logger.warn(
"sendCommand returned an empty response, taking the connection as broken and closing it."
);
this._closeConnection();
this.#closeConnection();
}
} catch (e) {
logger.error(`Cannot send the command ${cmd}`, e);
this._closeConnection();
this.#closeConnection();
if (leftAttempts == 0 && rethrow) {
throw e;
}
}
}
return { connected, reply };
},
}
// Opens an authenticated connection, sets it to this._controlConnection, and
// Opens an authenticated connection, sets it to this.#controlConnection, and
// return it.
async _getConnection() {
if (!this._controlConnection) {
this._controlConnection = await lazy.controller();
async #getConnection() {
if (!this.#controlConnection) {
this.#controlConnection = await lazy.controller();
}
if (this._controlConnection.inUse) {
if (this.#controlConnection.inUse) {
await new Promise((resolve, reject) =>
this._connectionQueue.push({ resolve, reject })
this.#connectionQueue.push({ resolve, reject })
);
} else {
this._controlConnection.inUse = true;
this.#controlConnection.inUse = true;
}
return this.#controlConnection;
}
return this._controlConnection;
},
_returnConnection() {
if (this._connectionQueue.length) {
this._connectionQueue.shift().resolve();
#returnConnection() {
if (this.#connectionQueue.length) {
this.#connectionQueue.shift().resolve();
} else {
this._controlConnection.inUse = false;
this.#controlConnection.inUse = false;
}
}
},
async _withConnection(func) {
async #withConnection(func) {
// TODO: Make more robust?
const conn = await this._getConnection();
const conn = await this.#getConnection();
try {
return await func(conn);
} finally {
this._returnConnection();
this.#returnConnection();
}
}
},
// If aConn is omitted, the cached connection is closed.
_closeConnection() {
if (this._controlConnection) {
#closeConnection() {
if (this.#controlConnection) {
logger.info("Closing the control connection");
this._controlConnection.close();
this._controlConnection = null;
this.#controlConnection.close();
this.#controlConnection = null;
}
for (const promise of this._connectionQueue) {
for (const promise of this.#connectionQueue) {
promise.reject("Connection closed");
}
this._connectionQueue = [];
},
this.#connectionQueue = [];
}
async _reconnect() {
this._closeConnection();
const conn = await this._getConnection();
async #reconnect() {
this.#closeConnection();
const conn = await this.#getConnection();
logger.debug("Reconnected to the control port.");
this._returnConnection(conn);
},
this.#returnConnection(conn);
}
async _readAuthenticationCookie(aPath) {
async #readAuthenticationCookie(aPath) {
const bytes = await IOUtils.read(aPath);
return Array.from(bytes, b => this._toHex(b, 2)).join("");
},
return Array.from(bytes, b => this.#toHex(b, 2)).join("");
}
// Returns a random 16 character password, hex-encoded.
_generateRandomPassword() {
#generateRandomPassword() {
// Similar to Vidalia's crypto_rand_string().
const kPasswordLen = 16;
const kMinCharCode = "!".charCodeAt(0);
const kMaxCharCode = "~".charCodeAt(0);
let pwd = "";
for (let i = 0; i < kPasswordLen; ++i) {
const val = this._cryptoRandInt(kMaxCharCode - kMinCharCode + 1);
const val = this.#cryptoRandInt(kMaxCharCode - kMinCharCode + 1);
if (val < 0) {
logger.error("_cryptoRandInt() failed");
return null;
}
pwd += this._toHex(kMinCharCode + val, 2);
pwd += this.#toHex(kMinCharCode + val, 2);
}
return pwd;
},
}
// Returns -1 upon failure.
_cryptoRandInt(aMax) {
#cryptoRandInt(aMax) {
// Based on tor's crypto_rand_int().
const maxUInt = 0xffffffff;
if (aMax <= 0 || aMax > maxUInt) {
......@@ -697,9 +692,11 @@ export const TorProtocolService = {
val = uint32[0];
}
return val % aMax;
},
}
_toHex(aValue, aMinLen) {
#toHex(aValue, aMinLen) {
return aValue.toString(16).padStart(aMinLen, "0");
},
};
}
}
export const TorProtocolService = new TorProvider();
/* 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, {
TorMonitorService: "resource://gre/modules/TorMonitorService.sys.mjs",
TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
});
export class TorProviderBuilder {
static async init() {
await lazy.TorProtocolService.init();
lazy.TorMonitorService.init();
}
static uninit() {
// Close any helper connection first...
lazy.TorProtocolService.uninit();
// ... and only then closes the event monitor connection, which will cause
// Tor to stop.
lazy.TorMonitorService.uninit();
}
// TODO: Switch to an async build?
static build() {
return lazy.TorProtocolService;
}
}
......@@ -5,8 +5,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
TorMonitorService: "resource://gre/modules/TorMonitorService.sys.mjs",
TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});
ChromeUtils.defineModuleGetter(
......@@ -33,24 +32,18 @@ let gInited = false;
// When it observes profile-after-change, it initializes whatever is needed to
// launch Tor.
export class TorStartupService {
_defaultPreferencesAreLoaded = false;
observe(aSubject, aTopic, aData) {
if (aTopic === BrowserTopics.ProfileAfterChange && !gInited) {
this._init();
this.#init();
} else if (aTopic === BrowserTopics.QuitApplicationGranted) {
this._uninit();
this.#uninit();
}
}
async _init() {
async #init() {
Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
// Starts TorProtocolService first, because it configures the controller
// factory, too.
await lazy.TorProtocolService.init();
lazy.TorMonitorService.init();
await lazy.TorProviderBuilder.init();
lazy.TorSettings.init();
lazy.TorConnect.init();
......@@ -59,17 +52,11 @@ export class TorStartupService {
gInited = true;
}
_uninit() {
#uninit() {
Services.obs.removeObserver(this, BrowserTopics.QuitApplicationGranted);
lazy.TorDomainIsolator.uninit();
// Close any helper connection first...
lazy.TorProtocolService.uninit();
// ... and only then closes the event monitor connection, which will cause
// Tor to stop.
lazy.TorMonitorService.uninit();
lazy.TorProviderBuilder.uninit();
lazy.TorLauncherUtil.cleanupTempDirectories();
}
}
......@@ -7,6 +7,7 @@ EXTRA_JS_MODULES += [
"TorParsers.sys.mjs",
"TorProcess.sys.mjs",
"TorProtocolService.sys.mjs",
"TorProviderBuilder.sys.mjs",
"TorStartupService.sys.mjs",
]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment