Loading toolkit/components/tor-launcher/TorControlPort.sys.mjs +128 −40 Original line number Diff line number Diff line Loading @@ -309,20 +309,22 @@ export class TorController { #commandQueue = []; /** * The callbacks for handling async notifications. * The event handler. * * @type {Map<string, EventCallback>} * @type {TorEventHandler} */ #eventCallbacks = new Map(); #eventHandler; /** * Connect to a control port over a Unix socket. * Not available on Windows. * * @param {nsIFile} ipcFile The path to the Unix socket to connect to * @param {TorEventHandler} eventHandler The event handler to use for * asynchronous notifications */ static fromIpcFile(ipcFile) { return new TorController(AsyncSocket.fromIpcFile(ipcFile)); static fromIpcFile(ipcFile, eventHandler) { return new TorController(AsyncSocket.fromIpcFile(ipcFile), eventHandler); } /** Loading @@ -330,9 +332,14 @@ export class TorController { * * @param {string} host The hostname to connect to * @param {number} port The port to connect the to * @param {TorEventHandler} eventHandler The event handler to use for * asynchronous notifications */ static fromSocketAddress(host, port) { return new TorController(AsyncSocket.fromSocketAddress(host, port)); static fromSocketAddress(host, port, eventHandler) { return new TorController( AsyncSocket.fromSocketAddress(host, port), eventHandler ); } /** Loading @@ -343,9 +350,12 @@ export class TorController { * * @private * @param {AsyncSocket} socket The socket to use * @param {TorEventHandler} eventHandler The event handler to use for * asynchronous notifications */ constructor(socket) { constructor(socket, eventHandler) { this.#socket = socket; this.#eventHandler = eventHandler; this.#startMessagePump(); } Loading Loading @@ -449,24 +459,6 @@ export class TorController { } } /** * Re-route an event message to the notification dispatcher. * * @param {string} message The message received on the control port. It should * starts with `"650" SP`. */ #handleNotification(message) { const maybeType = message.match(/^650\s+(?<type>\S+)/); const callback = this.#eventCallbacks.get(maybeType?.groups.type); if (callback) { try { callback(message); } catch (e) { console.error("An event watcher threw", e); } } } /** * Read messages on the socket and routed them to a dispatcher until the * socket is open or some error happens (including the underlying socket being Loading @@ -479,11 +471,18 @@ export class TorController { // condition becoming false. while (this.#socket) { const message = await this.#readMessage(); try { if (message.startsWith("650")) { this.#handleNotification(message); } else { this.#handleCommandReply(message); } } catch (err) { // E.g., if a notification handler fails. Without this internal // try/catch we risk of closing the connection while not actually // needed. console.error("Caught an exception while handling a message", err); } } } catch (err) { try { Loading Loading @@ -955,18 +954,76 @@ export class TorController { } /** * Watches for a particular type of asynchronous event. * Notice: we only observe `"650" SP...` events, currently (no `650+...` or * `650-...` events). * Also, you need to enable the events in the control port with SETEVENTS, * first. * Parse an asynchronous event and pass the data to the relative handler. * Only single-line messages are currently supported. * * @param {string} type The event type to catch * @param {EventCallback} callback The callback that will handle the event * @param {string} message The message received on the control port. It should * starts with `"650" SP`. */ watchEvent(type, callback) { this.#expectString(type, "type"); this.#eventCallbacks.set(type, callback); #handleNotification(message) { if (!this.#eventHandler) { return; } const data = message.match(/^650\s+(?<type>\S+)\s*(?<data>.*)?/); if (!data) { return; } switch (data.groups.type) { case "STATUS_CLIENT": let status; try { status = this.#parseBootstrapStatus(data.groups.data); } catch { // Probably, a non bootstrap client status break; } this.#eventHandler.onBootstrapStatus(status); break; case "CIRC": const builtEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(,?\$([0-9a-fA-F]{40})(?:~[a-zA-Z0-9]{1,19})?)+)/.exec( data.groups.data ); const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec( data.groups.data ); if (builtEvent) { const fp = /\$([0-9a-fA-F]{40})/g; const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g => g[1].toUpperCase() ); // In some cases, we might already receive SOCKS credentials in the // line. However, this might be a problem with onion services: we get // also a 4-hop circuit that we likely do not want to show to the // user, especially because it is used only temporarily, and it would // need a technical explaination. // const credentials = this.#parseCredentials(data.groups.data); this.#eventHandler.onCircuitBuilt(builtEvent.groups.ID, nodes); } else if (closedEvent) { this.#eventHandler.onCircuitClosed(closedEvent.groups.ID); } break; case "STREAM": const succeeedEvent = /^(?<StreamID>[a-zA-Z0-9]{1,16})\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec( data.groups.data ); if (succeeedEvent) { const credentials = this.#parseCredentials(data.groups.data); this.#eventHandler.onStreamSucceeded( succeeedEvent.groups.StreamID, succeeedEvent.groups.CircuitID, credentials?.username ?? null, credentials?.password ?? null ); } break; case "NOTICE": case "WARN": case "ERR": this.#eventHandler.onLogMessage(data.groups.type, data.groups.data); break; } } // Other helpers Loading Loading @@ -1011,6 +1068,24 @@ export class TorController { } } /** * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and * SOCKS_PASSWORD. * * @param {string} line The circ or stream line to check * @returns {object?} The credentials, or null if not found */ #parseCredentials(line) { const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line); const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line); return username && password ? { username: TorParsers.unescapeString(username[1]), password: TorParsers.unescapeString(password[1]), } : null; } /** * Return an object with all the matches that are in the form `key="value"` or * `key=value`. The values will be unescaped, but no additional parsing will Loading @@ -1030,3 +1105,16 @@ export class TorController { ); } } /** * The event handler interface. * The controller owner can implement this methods to receive asynchronous * notifications from the controller. */ export class TorEventHandler { onBootstrapStatus(status) {} onLogMessage(message) {} onCircuitBuilt(id, nodes) {} onCircuitClosed(id) {} onStreamSucceeded(streamId, circuitId, username, password) {} } toolkit/components/tor-launcher/TorParsers.sys.mjs +0 −165 Original line number Diff line number Diff line Loading @@ -6,171 +6,6 @@ export const TorStatuses = Object.freeze({ }); export 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 || !aReply.lineArray?.length) { 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 ); } } } return tmpArray; }, // 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. Loading toolkit/components/tor-launcher/TorProvider.sys.mjs +20 −142 Original line number Diff line number Diff line Loading @@ -6,10 +6,6 @@ import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs"; import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs"; import { TorParsers, TorStatuses, } from "resource://gre/modules/TorParsers.sys.mjs"; import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs"; const lazy = {}; Loading Loading @@ -449,14 +445,7 @@ export class TorProvider { }; this.#torProcess.onRestart = async () => { logger.info("Restarting the tor process"); try { this.#controlConnection.close(); } catch (e) { logger.warn( "Error when closing the previos control port on restart", e ); } this.#closeConnection(); this.#isBootstrapDone = false; this.#lastWarning = {}; this.#circuits.clear(); Loading Loading @@ -597,24 +586,20 @@ export class TorProvider { } async #setupEvents() { // We always liten to these events, because they are needed for the circuit // We always listen to these events, because they are needed for the circuit // display. this.#eventHandlers = new Map([ ["CIRC", this.#processCircEvent.bind(this)], ["STREAM", this.#processStreamEvent.bind(this)], ]); const events = ["CIRC", "STREAM"]; if (this.ownsTorDaemon) { // When we own the tor daemon, we listen to more events, that are used // for about:torconnect or for showing the logs in the settings page. this.#eventHandlers.set( "STATUS_CLIENT", this.#processStatusClient.bind(this) events.push("STATUS_CLIENT", "NOTICE", "WARN", "ERR"); // Do not await on the first bootstrap status retrieval, and do not // propagate its errors. this.#controlConnection .getBootstrapPhase() .then(status => this.#processBootstrapStatus(status, false)) .catch(e => logger.error("Failed to get the first bootstrap status", e) ); this.#eventHandlers.set("NOTICE", this.#processLog.bind(this)); this.#eventHandlers.set("WARN", this.#processLog.bind(this)); this.#eventHandlers.set("ERR", this.#processLog.bind(this)); } const events = Array.from(this.#eventHandlers.keys()); try { logger.debug(`Setting events: ${events.join(" ")}`); await this.#controlConnection.setEvents(events); Loading @@ -623,10 +608,6 @@ export class TorProvider { "We could not enable all the events we need. Tor Browser's functionalities might be reduced.", e ); return; } for (const [type, callback] of this.#eventHandlers.entries()) { this.#monitorEvent(type, callback); } } Loading @@ -641,12 +622,14 @@ export class TorProvider { let controlPort; if (this.#controlPortSettings.ipcFile) { controlPort = lazy.TorController.fromIpcFile( this.#controlPortSettings.ipcFile this.#controlPortSettings.ipcFile, this ); } else { controlPort = lazy.TorController.fromSocketAddress( this.#controlPortSettings.host, this.#controlPortSettings.port this.#controlPortSettings.port, this ); } try { Loading Loading @@ -681,6 +664,10 @@ export class TorProvider { logger.error("Failed to close the control port connection", e); } this.#controlConnection = null; } else { logger.trace( "Requested to close an already closed control port connection" ); } } Loading Loading @@ -901,113 +888,4 @@ export class TorProvider { TorProviderTopics.StreamSucceeded ); } // TODO: These are all parsing functions that should be moved to // TorControlPort. #eventHandlers = null; #monitorEvent(type, callback) { logger.info(`Watching events of type ${type}.`); let replyObj = {}; this.#controlConnection.watchEvent(type, line => { if (!line) { return; } logger.debug("Event response: ", line); const isComplete = TorParsers.parseReplyLine(line, replyObj); if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) { return; } const reply = replyObj; replyObj = {}; if (reply.statusCode !== TorStatuses.EventNotification) { logger.error("Unexpected event status code:", reply.statusCode); return; } if (!reply.lineArray[0].startsWith(`${type} `)) { logger.error("Wrong format for the first line:", reply.lineArray[0]); return; } reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1); try { callback(type, reply.lineArray); } catch (e) { logger.error("Exception while handling an event", reply, e); } }); } #processStatusClient(_type, lines) { const statusObj = TorParsers.parseBootstrapStatus(lines[0]); if (!statusObj) { // No `BOOTSTRAP` in the line return; } this.onBootstrapStatus(statusObj); } #processLog(type, lines) { this.onLogMessage(type, lines.join("\n")); } async #processCircEvent(_type, lines) { const builtEvent = /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec( lines[0] ); const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]); if (builtEvent) { const fp = /\$([0-9a-fA-F]{40})/g; const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g => g[1].toUpperCase() ); // In some cases, we might already receive SOCKS credentials in the line. // However, this might be a problem with onion services: we get also a // 4-hop circuit that we likely do not want to show to the user, // especially because it is used only temporarily, and it would need a // technical explaination. // const credentials = this.#parseCredentials(lines[0]); this.onCircuitBuilt(builtEvent.groups.CircuitID, nodes); } else if (closedEvent) { this.onCircuitClosed(closedEvent.groups.ID); } } #processStreamEvent(_type, lines) { const succeeedEvent = /^(?<StreamID>[a-zA-Z0-9]){1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec( lines[0] ); if (!succeeedEvent) { return; } const credentials = this.#parseCredentials(lines[0]); if (credentials !== null) { this.onStreamSucceeded( succeeedEvent.groups.StreamID, succeeedEvent.groups.CircuitID, credentials.username, credentials.password ); } } /** * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and * SOCKS_PASSWORD. * * @param {string} line The circ or stream line to check * @returns {object?} The credentials, or null if not found */ #parseCredentials(line) { const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line); const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line); return username && password ? { username: TorParsers.unescapeString(username[1]), password: TorParsers.unescapeString(password[1]), } : null; } } Loading
toolkit/components/tor-launcher/TorControlPort.sys.mjs +128 −40 Original line number Diff line number Diff line Loading @@ -309,20 +309,22 @@ export class TorController { #commandQueue = []; /** * The callbacks for handling async notifications. * The event handler. * * @type {Map<string, EventCallback>} * @type {TorEventHandler} */ #eventCallbacks = new Map(); #eventHandler; /** * Connect to a control port over a Unix socket. * Not available on Windows. * * @param {nsIFile} ipcFile The path to the Unix socket to connect to * @param {TorEventHandler} eventHandler The event handler to use for * asynchronous notifications */ static fromIpcFile(ipcFile) { return new TorController(AsyncSocket.fromIpcFile(ipcFile)); static fromIpcFile(ipcFile, eventHandler) { return new TorController(AsyncSocket.fromIpcFile(ipcFile), eventHandler); } /** Loading @@ -330,9 +332,14 @@ export class TorController { * * @param {string} host The hostname to connect to * @param {number} port The port to connect the to * @param {TorEventHandler} eventHandler The event handler to use for * asynchronous notifications */ static fromSocketAddress(host, port) { return new TorController(AsyncSocket.fromSocketAddress(host, port)); static fromSocketAddress(host, port, eventHandler) { return new TorController( AsyncSocket.fromSocketAddress(host, port), eventHandler ); } /** Loading @@ -343,9 +350,12 @@ export class TorController { * * @private * @param {AsyncSocket} socket The socket to use * @param {TorEventHandler} eventHandler The event handler to use for * asynchronous notifications */ constructor(socket) { constructor(socket, eventHandler) { this.#socket = socket; this.#eventHandler = eventHandler; this.#startMessagePump(); } Loading Loading @@ -449,24 +459,6 @@ export class TorController { } } /** * Re-route an event message to the notification dispatcher. * * @param {string} message The message received on the control port. It should * starts with `"650" SP`. */ #handleNotification(message) { const maybeType = message.match(/^650\s+(?<type>\S+)/); const callback = this.#eventCallbacks.get(maybeType?.groups.type); if (callback) { try { callback(message); } catch (e) { console.error("An event watcher threw", e); } } } /** * Read messages on the socket and routed them to a dispatcher until the * socket is open or some error happens (including the underlying socket being Loading @@ -479,11 +471,18 @@ export class TorController { // condition becoming false. while (this.#socket) { const message = await this.#readMessage(); try { if (message.startsWith("650")) { this.#handleNotification(message); } else { this.#handleCommandReply(message); } } catch (err) { // E.g., if a notification handler fails. Without this internal // try/catch we risk of closing the connection while not actually // needed. console.error("Caught an exception while handling a message", err); } } } catch (err) { try { Loading Loading @@ -955,18 +954,76 @@ export class TorController { } /** * Watches for a particular type of asynchronous event. * Notice: we only observe `"650" SP...` events, currently (no `650+...` or * `650-...` events). * Also, you need to enable the events in the control port with SETEVENTS, * first. * Parse an asynchronous event and pass the data to the relative handler. * Only single-line messages are currently supported. * * @param {string} type The event type to catch * @param {EventCallback} callback The callback that will handle the event * @param {string} message The message received on the control port. It should * starts with `"650" SP`. */ watchEvent(type, callback) { this.#expectString(type, "type"); this.#eventCallbacks.set(type, callback); #handleNotification(message) { if (!this.#eventHandler) { return; } const data = message.match(/^650\s+(?<type>\S+)\s*(?<data>.*)?/); if (!data) { return; } switch (data.groups.type) { case "STATUS_CLIENT": let status; try { status = this.#parseBootstrapStatus(data.groups.data); } catch { // Probably, a non bootstrap client status break; } this.#eventHandler.onBootstrapStatus(status); break; case "CIRC": const builtEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(,?\$([0-9a-fA-F]{40})(?:~[a-zA-Z0-9]{1,19})?)+)/.exec( data.groups.data ); const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec( data.groups.data ); if (builtEvent) { const fp = /\$([0-9a-fA-F]{40})/g; const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g => g[1].toUpperCase() ); // In some cases, we might already receive SOCKS credentials in the // line. However, this might be a problem with onion services: we get // also a 4-hop circuit that we likely do not want to show to the // user, especially because it is used only temporarily, and it would // need a technical explaination. // const credentials = this.#parseCredentials(data.groups.data); this.#eventHandler.onCircuitBuilt(builtEvent.groups.ID, nodes); } else if (closedEvent) { this.#eventHandler.onCircuitClosed(closedEvent.groups.ID); } break; case "STREAM": const succeeedEvent = /^(?<StreamID>[a-zA-Z0-9]{1,16})\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec( data.groups.data ); if (succeeedEvent) { const credentials = this.#parseCredentials(data.groups.data); this.#eventHandler.onStreamSucceeded( succeeedEvent.groups.StreamID, succeeedEvent.groups.CircuitID, credentials?.username ?? null, credentials?.password ?? null ); } break; case "NOTICE": case "WARN": case "ERR": this.#eventHandler.onLogMessage(data.groups.type, data.groups.data); break; } } // Other helpers Loading Loading @@ -1011,6 +1068,24 @@ export class TorController { } } /** * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and * SOCKS_PASSWORD. * * @param {string} line The circ or stream line to check * @returns {object?} The credentials, or null if not found */ #parseCredentials(line) { const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line); const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line); return username && password ? { username: TorParsers.unescapeString(username[1]), password: TorParsers.unescapeString(password[1]), } : null; } /** * Return an object with all the matches that are in the form `key="value"` or * `key=value`. The values will be unescaped, but no additional parsing will Loading @@ -1030,3 +1105,16 @@ export class TorController { ); } } /** * The event handler interface. * The controller owner can implement this methods to receive asynchronous * notifications from the controller. */ export class TorEventHandler { onBootstrapStatus(status) {} onLogMessage(message) {} onCircuitBuilt(id, nodes) {} onCircuitClosed(id) {} onStreamSucceeded(streamId, circuitId, username, password) {} }
toolkit/components/tor-launcher/TorParsers.sys.mjs +0 −165 Original line number Diff line number Diff line Loading @@ -6,171 +6,6 @@ export const TorStatuses = Object.freeze({ }); export 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 || !aReply.lineArray?.length) { 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 ); } } } return tmpArray; }, // 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. Loading
toolkit/components/tor-launcher/TorProvider.sys.mjs +20 −142 Original line number Diff line number Diff line Loading @@ -6,10 +6,6 @@ import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs"; import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs"; import { TorParsers, TorStatuses, } from "resource://gre/modules/TorParsers.sys.mjs"; import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs"; const lazy = {}; Loading Loading @@ -449,14 +445,7 @@ export class TorProvider { }; this.#torProcess.onRestart = async () => { logger.info("Restarting the tor process"); try { this.#controlConnection.close(); } catch (e) { logger.warn( "Error when closing the previos control port on restart", e ); } this.#closeConnection(); this.#isBootstrapDone = false; this.#lastWarning = {}; this.#circuits.clear(); Loading Loading @@ -597,24 +586,20 @@ export class TorProvider { } async #setupEvents() { // We always liten to these events, because they are needed for the circuit // We always listen to these events, because they are needed for the circuit // display. this.#eventHandlers = new Map([ ["CIRC", this.#processCircEvent.bind(this)], ["STREAM", this.#processStreamEvent.bind(this)], ]); const events = ["CIRC", "STREAM"]; if (this.ownsTorDaemon) { // When we own the tor daemon, we listen to more events, that are used // for about:torconnect or for showing the logs in the settings page. this.#eventHandlers.set( "STATUS_CLIENT", this.#processStatusClient.bind(this) events.push("STATUS_CLIENT", "NOTICE", "WARN", "ERR"); // Do not await on the first bootstrap status retrieval, and do not // propagate its errors. this.#controlConnection .getBootstrapPhase() .then(status => this.#processBootstrapStatus(status, false)) .catch(e => logger.error("Failed to get the first bootstrap status", e) ); this.#eventHandlers.set("NOTICE", this.#processLog.bind(this)); this.#eventHandlers.set("WARN", this.#processLog.bind(this)); this.#eventHandlers.set("ERR", this.#processLog.bind(this)); } const events = Array.from(this.#eventHandlers.keys()); try { logger.debug(`Setting events: ${events.join(" ")}`); await this.#controlConnection.setEvents(events); Loading @@ -623,10 +608,6 @@ export class TorProvider { "We could not enable all the events we need. Tor Browser's functionalities might be reduced.", e ); return; } for (const [type, callback] of this.#eventHandlers.entries()) { this.#monitorEvent(type, callback); } } Loading @@ -641,12 +622,14 @@ export class TorProvider { let controlPort; if (this.#controlPortSettings.ipcFile) { controlPort = lazy.TorController.fromIpcFile( this.#controlPortSettings.ipcFile this.#controlPortSettings.ipcFile, this ); } else { controlPort = lazy.TorController.fromSocketAddress( this.#controlPortSettings.host, this.#controlPortSettings.port this.#controlPortSettings.port, this ); } try { Loading Loading @@ -681,6 +664,10 @@ export class TorProvider { logger.error("Failed to close the control port connection", e); } this.#controlConnection = null; } else { logger.trace( "Requested to close an already closed control port connection" ); } } Loading Loading @@ -901,113 +888,4 @@ export class TorProvider { TorProviderTopics.StreamSucceeded ); } // TODO: These are all parsing functions that should be moved to // TorControlPort. #eventHandlers = null; #monitorEvent(type, callback) { logger.info(`Watching events of type ${type}.`); let replyObj = {}; this.#controlConnection.watchEvent(type, line => { if (!line) { return; } logger.debug("Event response: ", line); const isComplete = TorParsers.parseReplyLine(line, replyObj); if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) { return; } const reply = replyObj; replyObj = {}; if (reply.statusCode !== TorStatuses.EventNotification) { logger.error("Unexpected event status code:", reply.statusCode); return; } if (!reply.lineArray[0].startsWith(`${type} `)) { logger.error("Wrong format for the first line:", reply.lineArray[0]); return; } reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1); try { callback(type, reply.lineArray); } catch (e) { logger.error("Exception while handling an event", reply, e); } }); } #processStatusClient(_type, lines) { const statusObj = TorParsers.parseBootstrapStatus(lines[0]); if (!statusObj) { // No `BOOTSTRAP` in the line return; } this.onBootstrapStatus(statusObj); } #processLog(type, lines) { this.onLogMessage(type, lines.join("\n")); } async #processCircEvent(_type, lines) { const builtEvent = /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec( lines[0] ); const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]); if (builtEvent) { const fp = /\$([0-9a-fA-F]{40})/g; const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g => g[1].toUpperCase() ); // In some cases, we might already receive SOCKS credentials in the line. // However, this might be a problem with onion services: we get also a // 4-hop circuit that we likely do not want to show to the user, // especially because it is used only temporarily, and it would need a // technical explaination. // const credentials = this.#parseCredentials(lines[0]); this.onCircuitBuilt(builtEvent.groups.CircuitID, nodes); } else if (closedEvent) { this.onCircuitClosed(closedEvent.groups.ID); } } #processStreamEvent(_type, lines) { const succeeedEvent = /^(?<StreamID>[a-zA-Z0-9]){1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec( lines[0] ); if (!succeeedEvent) { return; } const credentials = this.#parseCredentials(lines[0]); if (credentials !== null) { this.onStreamSucceeded( succeeedEvent.groups.StreamID, succeeedEvent.groups.CircuitID, credentials.username, credentials.password ); } } /** * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and * SOCKS_PASSWORD. * * @param {string} line The circ or stream line to check * @returns {object?} The credentials, or null if not found */ #parseCredentials(line) { const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line); const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line); return username && password ? { username: TorParsers.unescapeString(username[1]), password: TorParsers.unescapeString(password[1]), } : null; } }