richard pushed to branch tor-browser-115.3.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits: 14e1b34c by Pier Angelo Vendrame at 2023-09-27T19:39:39+00:00 fixup! Bug 40933: Add tor-launcher functionality
Bug 42131: Check for existing circuits during initialization.
- - - - - 4b7f4df4 by Pier Angelo Vendrame at 2023-09-27T19:39:39+00:00 fixup! Bug 40933: Add tor-launcher functionality
Bug 42132: Poll for circuit information when we did not collect its data already.
- - - - -
2 changed files:
- toolkit/components/tor-launcher/TorControlPort.sys.mjs - toolkit/components/tor-launcher/TorProvider.sys.mjs
Changes:
===================================== toolkit/components/tor-launcher/TorControlPort.sys.mjs ===================================== @@ -272,6 +272,11 @@ class AsyncSocket { * * @typedef {string} NodeFingerprint */ +/** + * @typedef {object} CircuitInfo + * @property {CircuitID} id + * @property {NodeFingerprint[]} nodes + */ /** * @typedef {object} Bridge * @property {string} transport The transport of the bridge, or vanilla if not @@ -729,12 +734,14 @@ export class TorController { /** * Ask Tor a list of circuits. * - * @returns {string[]} An array with a string for each line + * @returns {CircuitInfo[]} An array with a string for each line */ async getCircuits() { const circuits = await this.#getInfo("circuit-status"); - // TODO: Do more parsing once we move the event parsing to this class! - return circuits.split(/\r?\n/); + return circuits + .split(/\r?\n/) + .map(this.#parseCircBuilt.bind(this)) + .filter(circ => circ); }
// Configuration @@ -1022,25 +1029,15 @@ export class TorController { 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 maybeCircuit = this.#parseCircBuilt(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() + if (maybeCircuit) { + this.#eventHandler.onCircuitBuilt( + maybeCircuit.id, + maybeCircuit.nodes ); - // 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); } @@ -1068,7 +1065,7 @@ export class TorController { } }
- // Other helpers + // Parsers
/** * Parse a bootstrap status line. @@ -1099,15 +1096,32 @@ export class TorController { }
/** - * Throw an exception when value is not a string. + * Parse a CIRC BUILT event or a GETINFO circuit-status. * - * @param {any} value The value to check - * @param {string} name The name of the `value` argument + * @param {string} line The line to parse + * @returns {CircuitInfo?} The ID and nodes of the circuit, or null if the + * parsing failed. */ - #expectString(value, name) { - if (typeof value !== "string" && !(value instanceof String)) { - throw new Error(`The ${name} argument is expected to be a string.`); + #parseCircBuilt(line) { + const builtEvent = + /^(?<ID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(,?$([0-9a-fA-F]{40})(?:~[a-zA-Z0-9]{1,19})?)+)/.exec( + line + ); + if (!builtEvent) { + return null; } + 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. + // So we do not try to extract them for now. Otherwise, we could do + // const credentials = this.#parseCredentials(line); + return { id: builtEvent.groups.ID, nodes }; }
/** @@ -1146,6 +1160,20 @@ export class TorController { ) ); } + + // Other helpers + + /** + * Throw an exception when value is not a string. + * + * @param {any} value The value to check + * @param {string} name The name of the `value` argument + */ + #expectString(value, name) { + if (typeof value !== "string" && !(value instanceof String)) { + throw new Error(`The ${name} argument is expected to be a string.`); + } + } }
/**
===================================== toolkit/components/tor-launcher/TorProvider.sys.mjs ===================================== @@ -137,7 +137,7 @@ export class TorProvider { * built before the new identity but not yet used. If we cleaned the map, we * risked of not having the data about it. * - * @type {Map<CircuitID, NodeFingerprint[]>} + * @type {Map<CircuitID, Promise<NodeFingerprint[]>>} */ #circuits = new Map(); /** @@ -204,6 +204,11 @@ export class TorProvider {
logger.debug(`Notifying ${TorProviderTopics.ProcessIsReady}`); Services.obs.notifyObservers(null, TorProviderTopics.ProcessIsReady); + + // If we are using an external Tor daemon, we might need to fetch circuits + // already, in case streams use them. Do not await because we do not want to + // block the intialization on this (it should not fail anyway...). + this.#fetchCircuits(); }
/** @@ -799,6 +804,16 @@ export class TorProvider { return crypto.getRandomValues(new Uint8Array(kPasswordLen)); }
+ /** + * Ask Tor the circuits it already knows to populate our circuit map with the + * circuits that were already open before we started listening for events. + */ + async #fetchCircuits() { + for (const { id, nodes } of await this.#controller.getCircuits()) { + this.onCircuitBuilt(id, nodes); + } + } + // Notification handlers
/** @@ -983,18 +998,35 @@ export class TorProvider { * @param {CircuitID} circuitId The ID of the circuit used by the stream * @param {string} username The SOCKS username * @param {string} password The SOCKS password - * @returns */ - onStreamSucceeded(streamId, circuitId, username, password) { + async onStreamSucceeded(streamId, circuitId, username, password) { if (!username || !password) { return; } logger.debug("Stream succeeded event", username, password, circuitId); - const circuit = this.#circuits.get(circuitId); + let circuit = this.#circuits.get(circuitId); if (!circuit) { - logger.error( - "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers." - ); + circuit = new Promise((resolve, reject) => { + this.#controlConnection.getCircuits().then(circuits => { + for (const { id, nodes } of circuits) { + if (id === circuitId) { + resolve(nodes); + return; + } + // Opportunistically collect circuits, since we are iterating them. + this.#circuits.set(id, nodes); + } + logger.error( + `Seen a STREAM SUCCEEDED with circuit ${circuitId}, but Tor did not send information about it.` + ); + reject(); + }); + }); + this.#circuits.set(circuitId, circuit); + } + try { + circuit = await circuit; + } catch { return; } Services.obs.notifyObservers(
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/c978614...
tor-commits@lists.torproject.org