richard pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
-
66229717
by Pier Angelo Vendrame at 2023-07-27T18:11:55+02:00
-
2cde9fc3
by Pier Angelo Vendrame at 2023-07-27T18:11:56+02:00
-
b2cd0ee8
by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00
-
26152fa9
by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00
-
749aeaca
by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00
-
20641450
by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00
-
1a8be7b1
by Pier Angelo Vendrame at 2023-07-27T18:11:59+02:00
11 changed files:
- browser/base/content/browser.js
- browser/components/onionservices/content/authPrompt.js
- browser/components/onionservices/content/savedKeysDialog.js
- browser/components/torcircuit/content/torCircuitPanel.js
- browser/components/torpreferences/content/connectionPane.js
- toolkit/components/tor-launcher/TorDomainIsolator.jsm → toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
- toolkit/components/tor-launcher/TorMonitorService.sys.mjs
- toolkit/components/tor-launcher/TorParsers.sys.mjs
- toolkit/components/tor-launcher/TorProtocolService.sys.mjs
- toolkit/components/tor-launcher/TorStartupService.sys.mjs
- toolkit/components/tor-launcher/moz.build
Changes:
| ... | ... | @@ -66,6 +66,7 @@ ChromeUtils.defineESModuleGetters(this, { |
| 66 | 66 | TabsSetupFlowManager:
|
| 67 | 67 | "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs",
|
| 68 | 68 | TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
|
| 69 | + TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
|
|
| 69 | 70 | TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
|
| 70 | 71 | UITour: "resource:///modules/UITour.sys.mjs",
|
| 71 | 72 | UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
|
| ... | ... | @@ -100,7 +101,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { |
| 100 | 101 | TorConnect: "resource:///modules/TorConnect.jsm",
|
| 101 | 102 | TorConnectState: "resource:///modules/TorConnect.jsm",
|
| 102 | 103 | TorConnectTopics: "resource:///modules/TorConnect.jsm",
|
| 103 | - TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.jsm",
|
|
| 104 | 104 | Translation: "resource:///modules/translation/TranslationParent.jsm",
|
| 105 | 105 | webrtcUI: "resource:///modules/webrtcUI.jsm",
|
| 106 | 106 | ZoomUI: "resource:///modules/ZoomUI.jsm",
|
| ... | ... | @@ -7,6 +7,7 @@ |
| 7 | 7 | XPCOMUtils.defineLazyModuleGetters(this, {
|
| 8 | 8 | OnionAuthUtil: "chrome://browser/content/onionservices/authUtil.jsm",
|
| 9 | 9 | CommonUtils: "resource://services-common/utils.js",
|
| 10 | + TorProtocolService: "resource://gre/modules/TorProtocolService.jsm",
|
|
| 10 | 11 | TorStrings: "resource:///modules/TorStrings.jsm",
|
| 11 | 12 | });
|
| 12 | 13 | |
| ... | ... | @@ -192,10 +193,6 @@ const OnionAuthPrompt = (function () { |
| 192 | 193 | let controllerFailureMsg =
|
| 193 | 194 | TorStrings.onionServices.authPrompt.failedToSetKey;
|
| 194 | 195 | try {
|
| 195 | - let { controller } = ChromeUtils.import(
|
|
| 196 | - "resource://torbutton/modules/tor-control-port.js"
|
|
| 197 | - );
|
|
| 198 | - let torController = await controller();
|
|
| 199 | 196 | // ^(subdomain.)*onionserviceid.onion$ (case-insensitive)
|
| 200 | 197 | const onionServiceIdRegExp =
|
| 201 | 198 | /^(.*\.)*(?<onionServiceId>[a-z2-7]{56})\.onion$/i;
|
| ... | ... | @@ -206,8 +203,7 @@ const OnionAuthPrompt = (function () { |
| 206 | 203 | |
| 207 | 204 | let checkboxElem = this._getCheckboxElement();
|
| 208 | 205 | let isPermanent = checkboxElem && checkboxElem.checked;
|
| 209 | - torController
|
|
| 210 | - .onionAuthAdd(onionServiceId, base64key, isPermanent)
|
|
| 206 | + TorProtocolService.onionAuthAdd(onionServiceId, base64key, isPermanent)
|
|
| 211 | 207 | .then(aResponse => {
|
| 212 | 208 | // Success! Reload the page.
|
| 213 | 209 | this._browser.sendMessageToActor(
|
| ... | ... | @@ -10,8 +10,8 @@ ChromeUtils.defineModuleGetter( |
| 10 | 10 | |
| 11 | 11 | ChromeUtils.defineModuleGetter(
|
| 12 | 12 | this,
|
| 13 | - "controller",
|
|
| 14 | - "resource://torbutton/modules/tor-control-port.js"
|
|
| 13 | + "TorProtocolService",
|
|
| 14 | + "resource://gre/modules/TorProtocolService.jsm"
|
|
| 15 | 15 | );
|
| 16 | 16 | |
| 17 | 17 | var gOnionServicesSavedKeysDialog = {
|
| ... | ... | @@ -49,11 +49,9 @@ var gOnionServicesSavedKeysDialog = { |
| 49 | 49 | const controllerFailureMsg =
|
| 50 | 50 | TorStrings.onionServices.authPreferences.failedToRemoveKey;
|
| 51 | 51 | try {
|
| 52 | - const torController = await controller();
|
|
| 53 | - |
|
| 54 | 52 | // Remove in reverse index order to avoid issues caused by index changes.
|
| 55 | 53 | for (let i = indexesToDelete.length - 1; i >= 0; --i) {
|
| 56 | - await this._deleteOneKey(torController, indexesToDelete[i]);
|
|
| 54 | + await this._deleteOneKey(indexesToDelete[i]);
|
|
| 57 | 55 | }
|
| 58 | 56 | } catch (e) {
|
| 59 | 57 | if (e.torMessage) {
|
| ... | ... | @@ -127,8 +125,7 @@ var gOnionServicesSavedKeysDialog = { |
| 127 | 125 | try {
|
| 128 | 126 | this._tree.view = this;
|
| 129 | 127 | |
| 130 | - const torController = await controller();
|
|
| 131 | - const keyInfoList = await torController.onionAuthViewKeys();
|
|
| 128 | + const keyInfoList = await TorProtocolService.onionAuthViewKeys();
|
|
| 132 | 129 | if (keyInfoList) {
|
| 133 | 130 | // Filter out temporary keys.
|
| 134 | 131 | this._keyInfoList = keyInfoList.filter(aKeyInfo => {
|
| ... | ... | @@ -165,9 +162,9 @@ var gOnionServicesSavedKeysDialog = { |
| 165 | 162 | },
|
| 166 | 163 | |
| 167 | 164 | // This method may throw; callers should catch errors.
|
| 168 | - async _deleteOneKey(aTorController, aIndex) {
|
|
| 165 | + async _deleteOneKey(aIndex) {
|
|
| 169 | 166 | const keyInfoObj = this._keyInfoList[aIndex];
|
| 170 | - await aTorController.onionAuthRemove(keyInfoObj.hsAddress);
|
|
| 167 | + await TorProtocolService.onionAuthRemove(keyInfoObj.hsAddress);
|
|
| 171 | 168 | this._tree.view.selection.clearRange(aIndex, aIndex);
|
| 172 | 169 | this._keyInfoList.splice(aIndex, 1);
|
| 173 | 170 | this._tree.rowCountChanged(aIndex + 1, -1);
|
| 1 | 1 | /* eslint-env mozilla/browser-window */
|
| 2 | 2 | |
| 3 | -/**
|
|
| 4 | - * Stores the data associated with a circuit node.
|
|
| 5 | - *
|
|
| 6 | - * @typedef NodeData
|
|
| 7 | - * @property {string[]} ipAddrs - The ip addresses associated with this node.
|
|
| 8 | - * @property {string?} bridgeType - The bridge type for this node, or "" if the
|
|
| 9 | - * node is a bridge but the type is unknown, or null if this is not a bridge
|
|
| 10 | - * node.
|
|
| 11 | - * @property {string?} regionCode - An upper case 2-letter ISO3166-1 code for
|
|
| 12 | - * the first ip address, or null if there is no region. This should also be a
|
|
| 13 | - * valid BCP47 Region subtag.
|
|
| 14 | - */
|
|
| 15 | - |
|
| 16 | 3 | /**
|
| 17 | 4 | * Data about the current domain and circuit for a xul:browser.
|
| 18 | 5 | *
|
| ... | ... | @@ -35,29 +22,6 @@ var gTorCircuitPanel = { |
| 35 | 22 | * @type {Element}
|
| 36 | 23 | */
|
| 37 | 24 | toolbarButton: null,
|
| 38 | - /**
|
|
| 39 | - * A list of IDs for "mature" circuits (those that have conveyed a stream).
|
|
| 40 | - *
|
|
| 41 | - * @type {string[]}
|
|
| 42 | - */
|
|
| 43 | - _knownCircuitIDs: [],
|
|
| 44 | - /**
|
|
| 45 | - * Stores the circuit nodes for each SOCKS username/password pair. The keys
|
|
| 46 | - * are of the form "<username>|<password>".
|
|
| 47 | - *
|
|
| 48 | - * @type {Map<string, NodeData[]>}
|
|
| 49 | - */
|
|
| 50 | - _credentialsToCircuitNodes: new Map(),
|
|
| 51 | - /**
|
|
| 52 | - * Browser data for their currently shown page.
|
|
| 53 | - *
|
|
| 54 | - * This data may be stale for a given browser since we only update this data
|
|
| 55 | - * when loading a new page in the currently selected browser, when switching
|
|
| 56 | - * tabs, or if we find a new circuit for the current browser.
|
|
| 57 | - *
|
|
| 58 | - * @type {WeakMap<MozBrowser, BrowserCircuitData>}
|
|
| 59 | - */
|
|
| 60 | - _browserData: new WeakMap(),
|
|
| 61 | 25 | /**
|
| 62 | 26 | * The data for the currently shown browser.
|
| 63 | 27 | *
|
| ... | ... | @@ -71,6 +35,13 @@ var gTorCircuitPanel = { |
| 71 | 35 | */
|
| 72 | 36 | _isActive: false,
|
| 73 | 37 | |
| 38 | + /**
|
|
| 39 | + * The topic on which circuit changes are broadcast.
|
|
| 40 | + *
|
|
| 41 | + * @type {string}
|
|
| 42 | + */
|
|
| 43 | + TOR_CIRCUIT_TOPIC: "TorCircuitChange",
|
|
| 44 | + |
|
| 74 | 45 | /**
|
| 75 | 46 | * Initialize the panel.
|
| 76 | 47 | */
|
| ... | ... | @@ -86,31 +57,6 @@ var gTorCircuitPanel = { |
| 86 | 57 | maxLogLevelPref: "browser.torcircuitpanel.loglevel",
|
| 87 | 58 | });
|
| 88 | 59 | |
| 89 | - const { wait_for_controller } = ChromeUtils.import(
|
|
| 90 | - "resource://torbutton/modules/tor-control-port.js"
|
|
| 91 | - );
|
|
| 92 | - wait_for_controller().then(
|
|
| 93 | - controller => {
|
|
| 94 | - if (!this._isActive) {
|
|
| 95 | - // uninit() was called before resolution.
|
|
| 96 | - return;
|
|
| 97 | - }
|
|
| 98 | - // FIXME: We should be using some dedicated integrated back end to
|
|
| 99 | - // store circuit information, rather than collecting it all here in the
|
|
| 100 | - // front end. See tor-browser#41700.
|
|
| 101 | - controller.watchEvent(
|
|
| 102 | - "STREAM",
|
|
| 103 | - streamEvent => streamEvent.StreamStatus === "SENTCONNECT",
|
|
| 104 | - streamEvent => this._collectCircuit(controller, streamEvent)
|
|
| 105 | - );
|
|
| 106 | - },
|
|
| 107 | - error => {
|
|
| 108 | - this._log.error(
|
|
| 109 | - `Not collecting circuits because of an error: ${error.message}`
|
|
| 110 | - );
|
|
| 111 | - }
|
|
| 112 | - );
|
|
| 113 | - |
|
| 114 | 60 | this.panel = document.getElementById("tor-circuit-panel");
|
| 115 | 61 | this._panelElements = {
|
| 116 | 62 | heading: document.getElementById("tor-circuit-heading"),
|
| ... | ... | @@ -245,6 +191,9 @@ var gTorCircuitPanel = { |
| 245 | 191 | // Notified of new locations for the currently selected browser (tab) *and*
|
| 246 | 192 | // switching selected browser.
|
| 247 | 193 | gBrowser.addProgressListener(this._locationListener);
|
| 194 | + |
|
| 195 | + // Get notifications for circuit changes.
|
|
| 196 | + Services.obs.addObserver(this, this.TOR_CIRCUIT_TOPIC);
|
|
| 248 | 197 | },
|
| 249 | 198 | |
| 250 | 199 | /**
|
| ... | ... | @@ -253,6 +202,17 @@ var gTorCircuitPanel = { |
| 253 | 202 | uninit() {
|
| 254 | 203 | this._isActive = false;
|
| 255 | 204 | gBrowser.removeProgressListener(this._locationListener);
|
| 205 | + Services.obs.removeObserver(this, this.TOR_CIRCUIT_TOPIC);
|
|
| 206 | + },
|
|
| 207 | + |
|
| 208 | + /**
|
|
| 209 | + * Observe circuit changes.
|
|
| 210 | + */
|
|
| 211 | + observe(subject, topic, data) {
|
|
| 212 | + if (topic === this.TOR_CIRCUIT_TOPIC) {
|
|
| 213 | + // TODO: Maybe check if we actually need to do something earlier.
|
|
| 214 | + this._updateCurrentBrowser();
|
|
| 215 | + }
|
|
| 256 | 216 | },
|
| 257 | 217 | |
| 258 | 218 | /**
|
| ... | ... | @@ -286,109 +246,6 @@ var gTorCircuitPanel = { |
| 286 | 246 | window.openWebLinkIn(this._panelElements.aliasLink.href, where);
|
| 287 | 247 | },
|
| 288 | 248 | |
| 289 | - /**
|
|
| 290 | - * Collect circuit data for the found circuits, to be used later for display.
|
|
| 291 | - *
|
|
| 292 | - * @param {controller} controller - The tor controller.
|
|
| 293 | - * @param {object} streamEvent - The streamEvent for the new circuit.
|
|
| 294 | - */
|
|
| 295 | - async _collectCircuit(controller, streamEvent) {
|
|
| 296 | - const id = streamEvent.CircuitID;
|
|
| 297 | - if (this._knownCircuitIDs.includes(id)) {
|
|
| 298 | - return;
|
|
| 299 | - }
|
|
| 300 | - this._log.debug(`New streamEvent.CircuitID: ${id}.`);
|
|
| 301 | - // FIXME: This list grows and is never freed. See tor-browser#41700.
|
|
| 302 | - this._knownCircuitIDs.push(id);
|
|
| 303 | - const circuitStatus = (await controller.getInfo("circuit-status"))?.find(
|
|
| 304 | - circuit => circuit.id === id
|
|
| 305 | - );
|
|
| 306 | - if (!circuitStatus?.SOCKS_USERNAME || !circuitStatus?.SOCKS_PASSWORD) {
|
|
| 307 | - return;
|
|
| 308 | - }
|
|
| 309 | - const nodes = await Promise.all(
|
|
| 310 | - circuitStatus.circuit.map(names =>
|
|
| 311 | - this._nodeDataForCircuit(controller, names)
|
|
| 312 | - )
|
|
| 313 | - );
|
|
| 314 | - // Remove quotes from the strings.
|
|
| 315 | - const username = circuitStatus.SOCKS_USERNAME.replace(/^"(.*)"$/, "$1");
|
|
| 316 | - const password = circuitStatus.SOCKS_PASSWORD.replace(/^"(.*)"$/, "$1");
|
|
| 317 | - const credentials = `${username}|${password}`;
|
|
| 318 | - // FIXME: This map grows and is never freed. We cannot simply request this
|
|
| 319 | - // information when needed because it is no longer available once the
|
|
| 320 | - // circuit is dropped, even if the web page is still displayed.
|
|
| 321 | - // See tor-browser#41700.
|
|
| 322 | - this._credentialsToCircuitNodes.set(credentials, nodes);
|
|
| 323 | - // Update the circuit in case the current page gains a new circuit whilst
|
|
| 324 | - // the popup is still open.
|
|
| 325 | - this._updateCurrentBrowser(credentials);
|
|
| 326 | - },
|
|
| 327 | - |
|
| 328 | - /**
|
|
| 329 | - * Fetch the node data for the given circuit node.
|
|
| 330 | - *
|
|
| 331 | - * @param {controller} controller - The tor controller.
|
|
| 332 | - * @param {string[]} circuitNodeNames - The names for the circuit node. Only
|
|
| 333 | - * the first name, the node id, will be used.
|
|
| 334 | - *
|
|
| 335 | - * @returns {NodeData} - The data for this circuit node.
|
|
| 336 | - */
|
|
| 337 | - async _nodeDataForCircuit(controller, circuitNodeNames) {
|
|
| 338 | - // The first "name" in circuitNodeNames is the id.
|
|
| 339 | - // Remove the leading '$' if present.
|
|
| 340 | - const id = circuitNodeNames[0].replace(/^\$/, "");
|
|
| 341 | - let result = { ipAddrs: [], bridgeType: null, regionCode: null };
|
|
| 342 | - const bridge = (await controller.getConf("bridge"))?.find(
|
|
| 343 | - foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase()
|
|
| 344 | - );
|
|
| 345 | - const addrRe = /^\[?([^\]]+)\]?:\d+$/;
|
|
| 346 | - if (bridge) {
|
|
| 347 | - result.bridgeType = bridge.type ?? "";
|
|
| 348 | - // Attempt to get an IP address from bridge address string.
|
|
| 349 | - const ip = bridge.address.match(addrRe)?.[1];
|
|
| 350 | - if (ip && !ip.startsWith("0.")) {
|
|
| 351 | - result.ipAddrs.push(ip);
|
|
| 352 | - }
|
|
| 353 | - } else {
|
|
| 354 | - // Either dealing with a relay, or a bridge whose fingerprint is not saved
|
|
| 355 | - // in torrc.
|
|
| 356 | - let statusMap;
|
|
| 357 | - try {
|
|
| 358 | - statusMap = await controller.getInfo("ns/id/" + id);
|
|
| 359 | - } catch {
|
|
| 360 | - // getInfo will throw if the given id is not a relay.
|
|
| 361 | - // This probably means we are dealing with a user-provided bridge with
|
|
| 362 | - // no fingerprint.
|
|
| 363 | - // We don't know the ip/ipv6 or type, so leave blank.
|
|
| 364 | - result.bridgeType = "";
|
|
| 365 | - return result;
|
|
| 366 | - }
|
|
| 367 | - if (statusMap.IP && !statusMap.IP.startsWith("0.")) {
|
|
| 368 | - result.ipAddrs.push(statusMap.IP);
|
|
| 369 | - }
|
|
| 370 | - const ip6 = statusMap.IPv6?.match(addrRe)?.[1];
|
|
| 371 | - if (ip6) {
|
|
| 372 | - result.ipAddrs.push(ip6);
|
|
| 373 | - }
|
|
| 374 | - }
|
|
| 375 | - if (result.ipAddrs.length) {
|
|
| 376 | - // Get the country code for the node's IP address.
|
|
| 377 | - let regionCode;
|
|
| 378 | - try {
|
|
| 379 | - // Expect a 2-letter ISO3166-1 code, which should also be a valid BCP47
|
|
| 380 | - // Region subtag.
|
|
| 381 | - regionCode = await controller.getInfo(
|
|
| 382 | - "ip-to-country/" + result.ipAddrs[0]
|
|
| 383 | - );
|
|
| 384 | - } catch {}
|
|
| 385 | - if (regionCode && regionCode !== "??") {
|
|
| 386 | - result.regionCode = regionCode.toUpperCase();
|
|
| 387 | - }
|
|
| 388 | - }
|
|
| 389 | - return result;
|
|
| 390 | - },
|
|
| 391 | - |
|
| 392 | 249 | /**
|
| 393 | 250 | * A list of schemes to never show the circuit display for.
|
| 394 | 251 | *
|
| ... | ... | @@ -398,71 +255,50 @@ var gTorCircuitPanel = { |
| 398 | 255 | *
|
| 399 | 256 | * @type {string[]}
|
| 400 | 257 | */
|
| 401 | - // FIXME: Have a back end that handles this instead. See tor-browser#41700.
|
|
| 258 | + // FIXME: Check if we find a UX to handle some of these cases, and if we
|
|
| 259 | + // manage to solve some technical issues.
|
|
| 260 | + // See tor-browser#41700 and tor-browser!699.
|
|
| 402 | 261 | _ignoredSchemes: ["about", "file", "chrome", "resource"],
|
| 403 | 262 | |
| 404 | 263 | /**
|
| 405 | 264 | * Update the current circuit and domain data for the currently selected
|
| 406 | 265 | * browser, possibly changing the UI.
|
| 407 | - *
|
|
| 408 | - * @param {string?} [matchingCredentials=null] - If given, only update the
|
|
| 409 | - * current browser data if the current browser's credentials match.
|
|
| 410 | 266 | */
|
| 411 | - _updateCurrentBrowser(matchingCredentials = null) {
|
|
| 267 | + _updateCurrentBrowser() {
|
|
| 412 | 268 | const browser = gBrowser.selectedBrowser;
|
| 413 | 269 | const domain = TorDomainIsolator.getDomainForBrowser(browser);
|
| 270 | + const nodes = TorDomainIsolator.getCircuit(
|
|
| 271 | + browser,
|
|
| 272 | + domain,
|
|
| 273 | + browser.contentPrincipal.originAttributes.userContextId
|
|
| 274 | + );
|
|
| 414 | 275 | // We choose the currentURI, which matches what is shown in the URL bar and
|
| 415 | 276 | // will match up with the domain.
|
| 416 | 277 | // In contrast, documentURI corresponds to the shown page. E.g. it could
|
| 417 | 278 | // point to "about:certerror".
|
| 418 | 279 | const scheme = browser.currentURI?.scheme;
|
| 419 | 280 | |
| 420 | - let credentials = TorDomainIsolator.getSocksProxyCredentials(
|
|
| 421 | - domain,
|
|
| 422 | - browser.contentPrincipal.originAttributes.userContextId
|
|
| 423 | - );
|
|
| 424 | - if (credentials) {
|
|
| 425 | - credentials = `${credentials.username}|${credentials.password}`;
|
|
| 426 | - }
|
|
| 427 | - |
|
| 428 | - if (matchingCredentials && matchingCredentials !== credentials) {
|
|
| 429 | - // This update was triggered by the circuit update for some other browser
|
|
| 430 | - // or process.
|
|
| 431 | - return;
|
|
| 432 | - }
|
|
| 433 | - |
|
| 434 | - let nodes = this._credentialsToCircuitNodes.get(credentials) ?? [];
|
|
| 435 | - |
|
| 436 | - const prevData = this._browserData.get(browser);
|
|
| 437 | - if (
|
|
| 438 | - prevData &&
|
|
| 439 | - prevData.domain &&
|
|
| 440 | - prevData.domain === domain &&
|
|
| 441 | - prevData.scheme === scheme &&
|
|
| 442 | - prevData.nodes.length &&
|
|
| 443 | - !nodes.length
|
|
| 444 | - ) {
|
|
| 445 | - // Since this is the same domain, for the same browser, and we used to
|
|
| 446 | - // have circuit nodes, we *assume* we are re-generating a circuit. So we
|
|
| 447 | - // keep the old circuit data around for the time being.
|
|
| 448 | - // FIXME: Have a back end that makes this explicit, rather than an
|
|
| 449 | - // assumption. See tor-browser#41700.
|
|
| 450 | - nodes = prevData.nodes;
|
|
| 451 | - this._log.debug(`Keeping old circuit for ${domain}.`);
|
|
| 452 | - }
|
|
| 453 | - |
|
| 454 | - this._browserData.set(browser, { domain, scheme, nodes });
|
|
| 455 | 281 | if (
|
| 456 | 282 | this._currentBrowserData &&
|
| 457 | 283 | this._currentBrowserData.domain === domain &&
|
| 458 | 284 | this._currentBrowserData.scheme === scheme &&
|
| 459 | - this._currentBrowserData.nodes === nodes
|
|
| 285 | + this._currentBrowserData.nodes.length === nodes.length &&
|
|
| 286 | + // If non-null, the fingerprints of the nodes match.
|
|
| 287 | + (!nodes ||
|
|
| 288 | + nodes.every(
|
|
| 289 | + (n, index) =>
|
|
| 290 | + n.fingerprint === this._currentBrowserData.nodes[index].fingerprint
|
|
| 291 | + ))
|
|
| 460 | 292 | ) {
|
| 461 | 293 | // No change.
|
| 294 | + this._log.debug(
|
|
| 295 | + "Skipping browser update because the data is already up to date."
|
|
| 296 | + );
|
|
| 462 | 297 | return;
|
| 463 | 298 | }
|
| 464 | 299 | |
| 465 | - this._currentBrowserData = this._browserData.get(browser);
|
|
| 300 | + this._currentBrowserData = { domain, scheme, nodes };
|
|
| 301 | + this._log.debug("Updating current browser.", this._currentBrowserData);
|
|
| 466 | 302 | |
| 467 | 303 | if (
|
| 468 | 304 | // Schemes where we always want to hide the display.
|
| ... | ... | @@ -17,6 +17,9 @@ const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource } = |
| 17 | 17 | const { TorProtocolService } = ChromeUtils.import(
|
| 18 | 18 | "resource://gre/modules/TorProtocolService.jsm"
|
| 19 | 19 | );
|
| 20 | +const { TorMonitorService, TorMonitorTopics } = ChromeUtils.import(
|
|
| 21 | + "resource://gre/modules/TorMonitorService.jsm"
|
|
| 22 | +);
|
|
| 20 | 23 | |
| 21 | 24 | const { TorConnect, TorConnectTopics, TorConnectState, TorCensorshipLevel } =
|
| 22 | 25 | ChromeUtils.import("resource:///modules/TorConnect.jsm");
|
| ... | ... | @@ -144,8 +147,6 @@ const gConnectionPane = (function () { |
| 144 | 147 | |
| 145 | 148 | _internetStatus: InternetStatus.Unknown,
|
| 146 | 149 | |
| 147 | - _controller: null,
|
|
| 148 | - |
|
| 149 | 150 | _currentBridgeId: null,
|
| 150 | 151 | |
| 151 | 152 | // populate xul with strings and cache the relevant elements
|
| ... | ... | @@ -727,9 +728,10 @@ const gConnectionPane = (function () { |
| 727 | 728 | };
|
| 728 | 729 | // Use a promise to avoid blocking the population of the page
|
| 729 | 730 | // FIXME: Stop using a JSON file, and switch to properties
|
| 730 | - fetch(
|
|
| 731 | + const annotationPromise = fetch(
|
|
| 731 | 732 | "chrome://browser/content/torpreferences/bridgemoji/annotations.json"
|
| 732 | - ).then(async res => {
|
|
| 733 | + );
|
|
| 734 | + annotationPromise.then(async res => {
|
|
| 733 | 735 | const annotations = await res.json();
|
| 734 | 736 | const bcp47 = Services.locale.appLocaleAsBCP47;
|
| 735 | 737 | const dash = bcp47.indexOf("-");
|
| ... | ... | @@ -749,6 +751,7 @@ const gConnectionPane = (function () { |
| 749 | 751 | ".currently-connected"
|
| 750 | 752 | )) {
|
| 751 | 753 | card.classList.remove("currently-connected");
|
| 754 | + card.querySelector(selectors.bridges.cardQrGrid).style.height = "";
|
|
| 752 | 755 | }
|
| 753 | 756 | if (!this._currentBridgeId) {
|
| 754 | 757 | return;
|
| ... | ... | @@ -769,72 +772,17 @@ const gConnectionPane = (function () { |
| 769 | 772 | placeholder.replaceWith(...cards);
|
| 770 | 773 | this._checkBridgeCardsHeight();
|
| 771 | 774 | };
|
| 772 | - try {
|
|
| 773 | - const { controller } = ChromeUtils.import(
|
|
| 774 | - "resource://torbutton/modules/tor-control-port.js"
|
|
| 775 | - );
|
|
| 776 | - // Avoid the cache because we set our custom event watcher, and at the
|
|
| 777 | - // moment, watchers cannot be removed from a controller.
|
|
| 778 | - controller(true).then(aController => {
|
|
| 779 | - this._controller = aController;
|
|
| 780 | - // Getting the circuits may be enough, if we have bootstrapped for a
|
|
| 781 | - // while, but at the beginning it gives many bridges as connected,
|
|
| 782 | - // because tor pokes all the bridges to find the best one.
|
|
| 783 | - // Also, watching circuit events does not work, at the moment, but in
|
|
| 784 | - // any case, checking the stream has the advantage that we can see if
|
|
| 785 | - // it really used for a connection, rather than tor having created
|
|
| 786 | - // this circuit to check if the bridge can be used. We do this by
|
|
| 787 | - // checking if the stream has SOCKS username, which actually contains
|
|
| 788 | - // the destination of the stream.
|
|
| 789 | - // FIXME: We only know the currentBridge *after* a circuit event, but
|
|
| 790 | - // if the circuit event is sent *before* about:torpreferences is
|
|
| 791 | - // opened we will miss it. Therefore this approach only works if a
|
|
| 792 | - // circuit is created after opening about:torconnect. A dedicated
|
|
| 793 | - // backend outside of about:preferences would help, and could be
|
|
| 794 | - // shared with gTorCircuitPanel. See tor-browser#41700.
|
|
| 795 | - this._controller.watchEvent(
|
|
| 796 | - "STREAM",
|
|
| 797 | - event =>
|
|
| 798 | - event.StreamStatus === "SUCCEEDED" && "SOCKS_USERNAME" in event,
|
|
| 799 | - async event => {
|
|
| 800 | - const circuitStatuses = await this._controller.getInfo(
|
|
| 801 | - "circuit-status"
|
|
| 802 | - );
|
|
| 803 | - if (!circuitStatuses) {
|
|
| 804 | - return;
|
|
| 805 | - }
|
|
| 806 | - for (const status of circuitStatuses) {
|
|
| 807 | - if (status.id === event.CircuitID && status.circuit.length) {
|
|
| 808 | - // The id in the circuit begins with a $ sign.
|
|
| 809 | - const id = status.circuit[0][0].replace(/^\$/, "");
|
|
| 810 | - if (id !== this._currentBridgeId) {
|
|
| 811 | - const bridge = (
|
|
| 812 | - await this._controller.getConf("bridge")
|
|
| 813 | - )?.find(
|
|
| 814 | - foundBridge =>
|
|
| 815 | - foundBridge.ID?.toUpperCase() === id.toUpperCase()
|
|
| 816 | - );
|
|
| 817 | - if (!bridge) {
|
|
| 818 | - // Either there is no bridge, or bridge with no
|
|
| 819 | - // fingerprint.
|
|
| 820 | - this._currentBridgeId = null;
|
|
| 821 | - } else {
|
|
| 822 | - this._currentBridgeId = id;
|
|
| 823 | - }
|
|
| 824 | - this._updateConnectedBridges();
|
|
| 825 | - }
|
|
| 826 | - break;
|
|
| 827 | - }
|
|
| 828 | - }
|
|
| 829 | - }
|
|
| 830 | - );
|
|
| 831 | - });
|
|
| 832 | - } catch (err) {
|
|
| 833 | - console.warn(
|
|
| 834 | - "We could not load torbutton, bridge statuses will not be updated",
|
|
| 835 | - err
|
|
| 836 | - );
|
|
| 837 | - }
|
|
| 775 | + this._checkConnectedBridge = () => {
|
|
| 776 | + // TODO: We could make sure TorSettings is in sync by monitoring also
|
|
| 777 | + // changes of settings. At that point, we could query it, instead of
|
|
| 778 | + // doing a query over the control port.
|
|
| 779 | + const bridge = TorMonitorService.currentBridge;
|
|
| 780 | + if (bridge?.fingerprint !== this._currentBridgeId) {
|
|
| 781 | + this._currentBridgeId = bridge?.fingerprint ?? null;
|
|
| 782 | + this._updateConnectedBridges();
|
|
| 783 | + }
|
|
| 784 | + };
|
|
| 785 | + annotationPromise.then(this._checkConnectedBridge.bind(this));
|
|
| 838 | 786 | |
| 839 | 787 | // Add a new bridge
|
| 840 | 788 | prefpane.querySelector(selectors.bridges.addHeader).textContent =
|
| ... | ... | @@ -927,6 +875,7 @@ const gConnectionPane = (function () { |
| 927 | 875 | });
|
| 928 | 876 | |
| 929 | 877 | Services.obs.addObserver(this, TorConnectTopics.StateChange);
|
| 878 | + Services.obs.addObserver(this, TorMonitorTopics.BridgeChanged);
|
|
| 930 | 879 | },
|
| 931 | 880 | |
| 932 | 881 | init() {
|
| ... | ... | @@ -950,11 +899,7 @@ const gConnectionPane = (function () { |
| 950 | 899 | // unregister our observer topics
|
| 951 | 900 | Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged);
|
| 952 | 901 | Services.obs.removeObserver(this, TorConnectTopics.StateChange);
|
| 953 | - |
|
| 954 | - if (this._controller !== null) {
|
|
| 955 | - this._controller.close();
|
|
| 956 | - this._controller = null;
|
|
| 957 | - }
|
|
| 902 | + Services.obs.removeObserver(this, TorMonitorTopics.BridgeChanged);
|
|
| 958 | 903 | },
|
| 959 | 904 | |
| 960 | 905 | // whether the page should be present in about:preferences
|
| ... | ... | @@ -985,6 +930,12 @@ const gConnectionPane = (function () { |
| 985 | 930 | this.onStateChange();
|
| 986 | 931 | break;
|
| 987 | 932 | }
|
| 933 | + case TorMonitorTopics.BridgeChanged: {
|
|
| 934 | + if (data?.fingerprint !== this._currentBridgeId) {
|
|
| 935 | + this._checkConnectedBridge();
|
|
| 936 | + }
|
|
| 937 | + break;
|
|
| 938 | + }
|
|
| 988 | 939 | }
|
| 989 | 940 | },
|
| 990 | 941 | |
| ... | ... | @@ -1028,7 +979,7 @@ const gConnectionPane = (function () { |
| 1028 | 979 | onRemoveAllBridges() {
|
| 1029 | 980 | TorSettings.bridges.enabled = false;
|
| 1030 | 981 | TorSettings.bridges.bridge_strings = "";
|
| 1031 | - if (TorSettings.bridges.source == TorBridgeSource.BuiltIn) {
|
|
| 982 | + if (TorSettings.bridges.source === TorBridgeSource.BuiltIn) {
|
|
| 1032 | 983 | TorSettings.bridges.builtin_type = "";
|
| 1033 | 984 | }
|
| 1034 | 985 | TorSettings.saveToPrefs();
|
| 1 | -// A component for Tor Browser that puts requests from different
|
|
| 2 | -// first party domains on separate Tor circuits.
|
|
| 3 | - |
|
| 4 | -var EXPORTED_SYMBOLS = ["TorDomainIsolator"];
|
|
| 1 | +/**
|
|
| 2 | + * A component for Tor Browser that puts requests from different first party
|
|
| 3 | + * domains on separate Tor circuits.
|
|
| 4 | + */
|
|
| 5 | 5 | |
| 6 | -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
| 7 | -const { XPCOMUtils } = ChromeUtils.import(
|
|
| 8 | - "resource://gre/modules/XPCOMUtils.jsm"
|
|
| 9 | -);
|
|
| 10 | -const { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
|
|
| 6 | +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
| 7 | +import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
|
|
| 8 | +import {
|
|
| 9 | + clearInterval,
|
|
| 10 | + setInterval,
|
|
| 11 | +} from "resource://gre/modules/Timer.sys.mjs";
|
|
| 11 | 12 | |
| 12 | 13 | const lazy = {};
|
| 13 | 14 | |
| ... | ... | @@ -18,11 +19,10 @@ XPCOMUtils.defineLazyServiceGetters(lazy, { |
| 18 | 19 | ],
|
| 19 | 20 | });
|
| 20 | 21 | |
| 21 | -ChromeUtils.defineModuleGetter(
|
|
| 22 | - lazy,
|
|
| 23 | - "TorProtocolService",
|
|
| 24 | - "resource://gre/modules/TorProtocolService.jsm"
|
|
| 25 | -);
|
|
| 22 | +ChromeUtils.defineESModuleGetters(lazy, {
|
|
| 23 | + TorMonitorTopics: "resource://gre/modules/TorMonitorService.sys.mjs",
|
|
| 24 | + TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
|
|
| 25 | +});
|
|
| 26 | 26 | |
| 27 | 27 | const logger = new ConsoleAPI({
|
| 28 | 28 | prefix: "TorDomainIsolator",
|
| ... | ... | @@ -33,6 +33,12 @@ const logger = new ConsoleAPI({ |
| 33 | 33 | // The string to use instead of the domain when it is not known.
|
| 34 | 34 | const CATCHALL_DOMAIN = "--unknown--";
|
| 35 | 35 | |
| 36 | +// The maximum lifetime for the catch-all circuit in milliseconds.
|
|
| 37 | +// When the catch-all circuit is needed, we check if more than this amount of
|
|
| 38 | +// time has passed since we last changed it nonce, and in case we change it
|
|
| 39 | +// again.
|
|
| 40 | +const CATCHALL_MAX_LIFETIME = 600_000;
|
|
| 41 | + |
|
| 36 | 42 | // The preference to observe, to know whether isolation should be enabled or
|
| 37 | 43 | // disabled.
|
| 38 | 44 | const NON_TOR_PROXY_PREF = "extensions.torbutton.use_nontor_proxy";
|
| ... | ... | @@ -40,23 +46,92 @@ const NON_TOR_PROXY_PREF = "extensions.torbutton.use_nontor_proxy"; |
| 40 | 46 | // The topic of new identity, to observe to cleanup all the nonces.
|
| 41 | 47 | const NEW_IDENTITY_TOPIC = "new-identity-requested";
|
| 42 | 48 | |
| 49 | +// The topic on which we broacast circuit change notifications.
|
|
| 50 | +const TOR_CIRCUIT_TOPIC = "TorCircuitChange";
|
|
| 51 | + |
|
| 52 | +// We have an interval to delete circuits that are not reclaimed by any browser.
|
|
| 53 | +const CLEAR_TIMEOUT = 600_000;
|
|
| 54 | + |
|
| 55 | +/**
|
|
| 56 | + * @typedef {string} CircuitId A string that we use to identify a circuit.
|
|
| 57 | + * Currently, it is a string that combines SOCKS credentials, to make it easier
|
|
| 58 | + * to use as a map key.
|
|
| 59 | + * It is not related to Tor's CircuitIDs.
|
|
| 60 | + */
|
|
| 61 | +/**
|
|
| 62 | + * @typedef {number} BrowserId
|
|
| 63 | + */
|
|
| 64 | +/**
|
|
| 65 | + * @typedef {NodeData[]} CircuitData The data about the nodes, ordered from
|
|
| 66 | + * guard (or bridge) to exit.
|
|
| 67 | + */
|
|
| 68 | +/**
|
|
| 69 | + * @typedef BrowserCircuits Circuits related to a certain combination of
|
|
| 70 | + * isolators (first-party domain and user context ID, currently).
|
|
| 71 | + * @property {CircuitId} current The id of the last known circuit that has been
|
|
| 72 | + * used to fetch data for the isolated context.
|
|
| 73 | + * @property {CircuitId?} pending The id of the last used circuit for this
|
|
| 74 | + * isolation context. We might or might not know data about it, yet. But if we
|
|
| 75 | + * know it, we should move this id into current.
|
|
| 76 | + */
|
|
| 77 | + |
|
| 43 | 78 | class TorDomainIsolatorImpl {
|
| 44 | - // A mutable map that records what nonce we are using for each domain.
|
|
| 79 | + /**
|
|
| 80 | + * A mutable map that records what nonce we are using for each domain.
|
|
| 81 | + *
|
|
| 82 | + * @type {Map<string, string>}
|
|
| 83 | + */
|
|
| 45 | 84 | #noncesForDomains = new Map();
|
| 46 | 85 | |
| 47 | - // A mutable map that records what nonce we are using for each tab container.
|
|
| 86 | + /**
|
|
| 87 | + * A mutable map that records what nonce we are using for each tab container.
|
|
| 88 | + *
|
|
| 89 | + * @type {Map<string, string>}
|
|
| 90 | + */
|
|
| 48 | 91 | #noncesForUserContextId = new Map();
|
| 49 | 92 | |
| 50 | - // A bool that controls if we use SOCKS auth for isolation or not.
|
|
| 93 | + /**
|
|
| 94 | + * Tell whether we use SOCKS auth for isolation or not.
|
|
| 95 | + *
|
|
| 96 | + * @type {boolean}
|
|
| 97 | + */
|
|
| 51 | 98 | #isolationEnabled = true;
|
| 52 | 99 | |
| 53 | - // Specifies when the current catch-all circuit was first used
|
|
| 100 | + /**
|
|
| 101 | + * Specifies when the current catch-all circuit was first used.
|
|
| 102 | + *
|
|
| 103 | + * @type {integer}
|
|
| 104 | + */
|
|
| 54 | 105 | #catchallDirtySince = Date.now();
|
| 55 | 106 | |
| 107 | + /**
|
|
| 108 | + * A map that associates circuit ids to the circuit information.
|
|
| 109 | + *
|
|
| 110 | + * @type {Map<CircuitId, CircuitData>}
|
|
| 111 | + */
|
|
| 112 | + #knownCircuits = new Map();
|
|
| 113 | + |
|
| 114 | + /**
|
|
| 115 | + * A map that associates a certain browser to all the circuits it used or it
|
|
| 116 | + * is going to use.
|
|
| 117 | + * The circuits are keyed on the SOCKS username, which we take for granted
|
|
| 118 | + * being a combination of the first-party domain and the user context id.
|
|
| 119 | + *
|
|
| 120 | + * @type {Map<BrowserId, Map<string, BrowserCircuits>>}
|
|
| 121 | + */
|
|
| 122 | + #browsers = new Map();
|
|
| 123 | + |
|
| 124 | + /**
|
|
| 125 | + * The handle of the interval we use to cleanup old circuit data.
|
|
| 126 | + *
|
|
| 127 | + * @type {number?}
|
|
| 128 | + */
|
|
| 129 | + #cleanupIntervalId = null;
|
|
| 130 | + |
|
| 56 | 131 | /**
|
| 57 | 132 | * Initialize the domain isolator.
|
| 58 | - * This function will setup the proxy filter that injects the credentials and
|
|
| 59 | - * register some observers.
|
|
| 133 | + * This function will setup the proxy filter that injects the credentials,
|
|
| 134 | + * register some observers, and setup the cleaning interval.
|
|
| 60 | 135 | */
|
| 61 | 136 | init() {
|
| 62 | 137 | logger.info("Setup circuit isolation by domain and user context");
|
| ... | ... | @@ -68,14 +143,25 @@ class TorDomainIsolatorImpl { |
| 68 | 143 | |
| 69 | 144 | Services.prefs.addObserver(NON_TOR_PROXY_PREF, this);
|
| 70 | 145 | Services.obs.addObserver(this, NEW_IDENTITY_TOPIC);
|
| 146 | + Services.obs.addObserver(this, lazy.TorMonitorTopics.StreamSucceeded);
|
|
| 147 | + |
|
| 148 | + this.#cleanupIntervalId = setInterval(
|
|
| 149 | + this.#clearKnownCircuits.bind(this),
|
|
| 150 | + CLEAR_TIMEOUT
|
|
| 151 | + );
|
|
| 71 | 152 | }
|
| 72 | 153 | |
| 73 | 154 | /**
|
| 74 | - * Removes the observers added in the initialization.
|
|
| 155 | + * Removes the observers added in the initialization and stops the cleaning
|
|
| 156 | + * interval.
|
|
| 75 | 157 | */
|
| 76 | 158 | uninit() {
|
| 77 | 159 | Services.prefs.removeObserver(NON_TOR_PROXY_PREF, this);
|
| 78 | 160 | Services.obs.removeObserver(this, NEW_IDENTITY_TOPIC);
|
| 161 | + Services.obs.removeObserver(this, lazy.TorMonitorTopics.StreamSucceeded);
|
|
| 162 | + clearInterval(this.#cleanupIntervalId);
|
|
| 163 | + this.#cleanupIntervalId = null;
|
|
| 164 | + this.clearIsolation();
|
|
| 79 | 165 | }
|
| 80 | 166 | |
| 81 | 167 | enable() {
|
| ... | ... | @@ -89,52 +175,52 @@ class TorDomainIsolatorImpl { |
| 89 | 175 | }
|
| 90 | 176 | |
| 91 | 177 | /**
|
| 92 | - * Return the credentials to use as username and password for the SOCKS proxy,
|
|
| 93 | - * given a certain domain and userContextId. Optionally, create them.
|
|
| 178 | + * Get the last circuit used in a certain browser.
|
|
| 179 | + * The returned data is created when the circuit is first seen, therefore it
|
|
| 180 | + * could be stale (i.e., the circuit might not be available anymore).
|
|
| 94 | 181 | *
|
| 95 | - * @param {string} firstPartyDomain The first party domain associated to the requests
|
|
| 96 | - * @param {string} userContextId The context ID associated to the request
|
|
| 97 | - * @param {bool} create Whether to create the nonce, if it is not available
|
|
| 98 | - * @returns {object|null} Either the credential, or null if we do not have them and create is
|
|
| 99 | - * false.
|
|
| 182 | + * @param {MozBrowser} browser The browser to get data for
|
|
| 183 | + * @param {string} domain The first party domain we want to get the circuit
|
|
| 184 | + * for
|
|
| 185 | + * @param {number} userContextId The user context domain we want to get the
|
|
| 186 | + * circuit for
|
|
| 187 | + * @returns {NodeData[]} The node data, or an empty array if we do not have
|
|
| 188 | + * data for the requested key.
|
|
| 100 | 189 | */
|
| 101 | - getSocksProxyCredentials(firstPartyDomain, userContextId, create = false) {
|
|
| 102 | - if (!this.#noncesForDomains.has(firstPartyDomain)) {
|
|
| 103 | - if (!create) {
|
|
| 104 | - return null;
|
|
| 105 | - }
|
|
| 106 | - const nonce = this.#nonce();
|
|
| 107 | - logger.info(`New nonce for first party ${firstPartyDomain}: ${nonce}`);
|
|
| 108 | - this.#noncesForDomains.set(firstPartyDomain, nonce);
|
|
| 190 | + getCircuit(browser, domain, userContextId) {
|
|
| 191 | + const username = this.#makeUsername(domain, userContextId);
|
|
| 192 | + const circuits = this.#browsers.get(browser.browserId)?.get(username);
|
|
| 193 | + // This is the only place where circuit data can go out, so the only place
|
|
| 194 | + // where it makes a difference to check whether the pending circuit is still
|
|
| 195 | + // pending, or it has actually got data.
|
|
| 196 | + const pending = this.#knownCircuits.get(circuits?.pending);
|
|
| 197 | + if (pending?.length) {
|
|
| 198 | + circuits.current = circuits.pending;
|
|
| 199 | + circuits.pending = null;
|
|
| 200 | + return pending;
|
|
| 109 | 201 | }
|
| 110 | - if (!this.#noncesForUserContextId.has(userContextId)) {
|
|
| 111 | - if (!create) {
|
|
| 112 | - return null;
|
|
| 113 | - }
|
|
| 114 | - const nonce = this.#nonce();
|
|
| 115 | - logger.info(`New nonce for userContextId ${userContextId}: ${nonce}`);
|
|
| 116 | - this.#noncesForUserContextId.set(userContextId, nonce);
|
|
| 117 | - }
|
|
| 118 | - return {
|
|
| 119 | - username: this.#makeUsername(firstPartyDomain, userContextId),
|
|
| 120 | - password:
|
|
| 121 | - this.#noncesForDomains.get(firstPartyDomain) +
|
|
| 122 | - this.#noncesForUserContextId.get(userContextId),
|
|
| 123 | - };
|
|
| 202 | + // TODO: At this point we already know if we expect a circuit change for
|
|
| 203 | + // this key: (circuit?.pending && !pending). However, we do not consume this
|
|
| 204 | + // data yet in the frontend, so do not send it for now.
|
|
| 205 | + return this.#knownCircuits.get(circuits?.current) ?? [];
|
|
| 124 | 206 | }
|
| 125 | 207 | |
| 126 | 208 | /**
|
| 127 | 209 | * Create a new nonce for the FP domain of the selected browser and reload the
|
| 128 | 210 | * tab with a new circuit.
|
| 129 | 211 | *
|
| 130 | - * @param {object} browser Should be the gBrowser from the context of the
|
|
| 131 | - * caller
|
|
| 212 | + * @param {object} globalBrowser Should be the gBrowser from the context of
|
|
| 213 | + * the caller
|
|
| 132 | 214 | */
|
| 133 | - newCircuitForBrowser(browser) {
|
|
| 134 | - const firstPartyDomain = getDomainForBrowser(browser.selectedBrowser);
|
|
| 215 | + newCircuitForBrowser(globalBrowser) {
|
|
| 216 | + const browser = globalBrowser.selectedBrowser;
|
|
| 217 | + const firstPartyDomain = getDomainForBrowser(browser);
|
|
| 135 | 218 | this.#newCircuitForDomain(firstPartyDomain);
|
| 136 | - // TODO: How to properly handle the user context? Should we use
|
|
| 137 | - // (domain, userContextId) pairs, instead of concatenating nonces?
|
|
| 219 | + const { username, password } = this.#getSocksProxyCredentials(
|
|
| 220 | + firstPartyDomain,
|
|
| 221 | + browser.contentPrincipal.originAttributes.userContextId
|
|
| 222 | + );
|
|
| 223 | + this.#trackBrowser(browser, username, password);
|
|
| 138 | 224 | browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
|
| 139 | 225 | }
|
| 140 | 226 | |
| ... | ... | @@ -147,12 +233,15 @@ class TorDomainIsolatorImpl { |
| 147 | 233 | |
| 148 | 234 | // Per-domain and per contextId nonces are stored in maps, so simply clear
|
| 149 | 235 | // them.
|
| 236 | + // Notice that the catch-all circuit is included in #noncesForDomains, so we
|
|
| 237 | + // are implicilty cleaning it. Should this change, we should change its
|
|
| 238 | + // nonce explicitly here.
|
|
| 150 | 239 | this.#noncesForDomains.clear();
|
| 151 | 240 | this.#noncesForUserContextId.clear();
|
| 241 | + this.#catchallDirtySince = Date.now();
|
|
| 152 | 242 | |
| 153 | - // Force a rotation on the next catch-all circuit use by setting the
|
|
| 154 | - // creation time to the epoch.
|
|
| 155 | - this.#catchallDirtySince = 0;
|
|
| 243 | + this.#knownCircuits.clear();
|
|
| 244 | + this.#browsers.clear();
|
|
| 156 | 245 | }
|
| 157 | 246 | |
| 158 | 247 | async observe(subject, topic, data) {
|
| ... | ... | @@ -173,55 +262,20 @@ class TorDomainIsolatorImpl { |
| 173 | 262 | logger.error("Could not send the newnym command", e);
|
| 174 | 263 | // TODO: What UX to use here? See tor-browser#41708
|
| 175 | 264 | }
|
| 265 | + } else if (topic === lazy.TorMonitorTopics.StreamSucceeded) {
|
|
| 266 | + const { username, password, circuit } = subject.wrappedJSObject;
|
|
| 267 | + this.#updateCircuit(username, password, circuit);
|
|
| 176 | 268 | }
|
| 177 | 269 | }
|
| 178 | 270 | |
| 179 | 271 | /**
|
| 180 | - * Setup a filter that for every HTTPChannel, replaces the default SOCKS proxy
|
|
| 181 | - * with one that authenticates to the SOCKS server (the tor client process)
|
|
| 182 | - * with a username (the first party domain and userContextId) and a nonce
|
|
| 183 | - * password.
|
|
| 184 | - * Tor provides a separate circuit for each username+password combination.
|
|
| 272 | + * Setup a filter that for every HTTPChannel.
|
|
| 185 | 273 | */
|
| 186 | 274 | #setupProxyFilter() {
|
| 187 | - const filterFunction = (aChannel, aProxy) => {
|
|
| 188 | - if (!this.#isolationEnabled) {
|
|
| 189 | - return aProxy;
|
|
| 190 | - }
|
|
| 191 | - try {
|
|
| 192 | - const channel = aChannel.QueryInterface(Ci.nsIChannel);
|
|
| 193 | - let firstPartyDomain =
|
|
| 194 | - channel.loadInfo.originAttributes.firstPartyDomain;
|
|
| 195 | - const userContextId = channel.loadInfo.originAttributes.userContextId;
|
|
| 196 | - if (firstPartyDomain === "") {
|
|
| 197 | - firstPartyDomain = CATCHALL_DOMAIN;
|
|
| 198 | - if (Date.now() - this.#catchallDirtySince > 1000 * 10 * 60) {
|
|
| 199 | - logger.info(
|
|
| 200 | - "tor catchall circuit has been dirty for over 10 minutes. Rotating."
|
|
| 201 | - );
|
|
| 202 | - this.#newCircuitForDomain(CATCHALL_DOMAIN);
|
|
| 203 | - this.#catchallDirtySince = Date.now();
|
|
| 204 | - }
|
|
| 205 | - }
|
|
| 206 | - const replacementProxy = this.#applySocksProxyCredentials(
|
|
| 207 | - aProxy,
|
|
| 208 | - firstPartyDomain,
|
|
| 209 | - userContextId
|
|
| 210 | - );
|
|
| 211 | - logger.debug(
|
|
| 212 | - `Requested ${channel.URI.spec} via ${replacementProxy.username}:${replacementProxy.password}`
|
|
| 213 | - );
|
|
| 214 | - return replacementProxy;
|
|
| 215 | - } catch (e) {
|
|
| 216 | - logger.error("Error while setting a new proxy", e);
|
|
| 217 | - return null;
|
|
| 218 | - }
|
|
| 219 | - };
|
|
| 220 | - |
|
| 221 | 275 | lazy.ProtocolProxyService.registerChannelFilter(
|
| 222 | 276 | {
|
| 223 | - applyFilter(aChannel, aProxy, aCallback) {
|
|
| 224 | - aCallback.onProxyFilterResult(filterFunction(aChannel, aProxy));
|
|
| 277 | + applyFilter: (aChannel, aProxy, aCallback) => {
|
|
| 278 | + aCallback.onProxyFilterResult(this.#proxyFilter(aChannel, aProxy));
|
|
| 225 | 279 | },
|
| 226 | 280 | },
|
| 227 | 281 | 0
|
| ... | ... | @@ -229,33 +283,96 @@ class TorDomainIsolatorImpl { |
| 229 | 283 | }
|
| 230 | 284 | |
| 231 | 285 | /**
|
| 232 | - * Takes a proxyInfo object (originalProxy) and returns a new proxyInfo
|
|
| 233 | - * object with the same properties, except the username is set to the
|
|
| 234 | - * the domain and userContextId, and the password is a nonce.
|
|
| 286 | + * Replaces the default SOCKS proxy with one that authenticates to the SOCKS
|
|
| 287 | + * server (the tor client process) with a username (the first party domain and
|
|
| 288 | + * userContextId) and a nonce password.
|
|
| 289 | + * Tor provides a separate circuit for each username+password combination.
|
|
| 290 | + *
|
|
| 291 | + * @param {nsIChannel} aChannel The channel we are setting the proxy for
|
|
| 292 | + * @param {nsIProxyInfo} aProxy The original proxy
|
|
| 293 | + * @returns {nsIProxyInfo} The new proxy to use
|
|
| 235 | 294 | */
|
| 236 | - #applySocksProxyCredentials(originalProxy, domain, userContextId) {
|
|
| 237 | - const proxy = originalProxy.QueryInterface(Ci.nsIProxyInfo);
|
|
| 238 | - const { username, password } = this.getSocksProxyCredentials(
|
|
| 239 | - domain,
|
|
| 240 | - userContextId,
|
|
| 241 | - true
|
|
| 242 | - );
|
|
| 243 | - return lazy.ProtocolProxyService.newProxyInfoWithAuth(
|
|
| 244 | - "socks",
|
|
| 245 | - proxy.host,
|
|
| 246 | - proxy.port,
|
|
| 247 | - username,
|
|
| 248 | - password,
|
|
| 249 | - "", // aProxyAuthorizationHeader
|
|
| 250 | - "", // aConnectionIsolationKey
|
|
| 251 | - proxy.flags,
|
|
| 252 | - proxy.failoverTimeout,
|
|
| 253 | - proxy.failoverProxy
|
|
| 254 | - );
|
|
| 295 | + #proxyFilter(aChannel, aProxy) {
|
|
| 296 | + if (!this.#isolationEnabled) {
|
|
| 297 | + return aProxy;
|
|
| 298 | + }
|
|
| 299 | + try {
|
|
| 300 | + const channel = aChannel.QueryInterface(Ci.nsIChannel);
|
|
| 301 | + let firstPartyDomain = channel.loadInfo.originAttributes.firstPartyDomain;
|
|
| 302 | + const userContextId = channel.loadInfo.originAttributes.userContextId;
|
|
| 303 | + if (!firstPartyDomain) {
|
|
| 304 | + firstPartyDomain = CATCHALL_DOMAIN;
|
|
| 305 | + if (Date.now() - this.#catchallDirtySince > CATCHALL_MAX_LIFETIME) {
|
|
| 306 | + logger.info(
|
|
| 307 | + "tor catchall circuit has reached its maximum lifetime. Rotating."
|
|
| 308 | + );
|
|
| 309 | + this.#newCircuitForDomain(CATCHALL_DOMAIN);
|
|
| 310 | + }
|
|
| 311 | + }
|
|
| 312 | + const { username, password } = this.#getSocksProxyCredentials(
|
|
| 313 | + firstPartyDomain,
|
|
| 314 | + userContextId
|
|
| 315 | + );
|
|
| 316 | + const browser = this.#getBrowserForChannel(channel);
|
|
| 317 | + if (browser) {
|
|
| 318 | + this.#trackBrowser(browser, username, password);
|
|
| 319 | + }
|
|
| 320 | + logger.debug(`Requested ${channel.URI.spec} via ${username}:${password}`);
|
|
| 321 | + const proxy = aProxy.QueryInterface(Ci.nsIProxyInfo);
|
|
| 322 | + return lazy.ProtocolProxyService.newProxyInfoWithAuth(
|
|
| 323 | + "socks",
|
|
| 324 | + proxy.host,
|
|
| 325 | + proxy.port,
|
|
| 326 | + username,
|
|
| 327 | + password,
|
|
| 328 | + "", // aProxyAuthorizationHeader
|
|
| 329 | + "", // aConnectionIsolationKey
|
|
| 330 | + proxy.flags,
|
|
| 331 | + proxy.failoverTimeout,
|
|
| 332 | + proxy.failoverProxy
|
|
| 333 | + );
|
|
| 334 | + } catch (e) {
|
|
| 335 | + logger.error("Error while setting a new proxy", e);
|
|
| 336 | + return null;
|
|
| 337 | + }
|
|
| 338 | + }
|
|
| 339 | + |
|
| 340 | + /**
|
|
| 341 | + * Return the credentials to use as username and password for the SOCKS proxy,
|
|
| 342 | + * given a certain domain and userContextId.
|
|
| 343 | + * A new random password will be created if not available yet.
|
|
| 344 | + *
|
|
| 345 | + * @param {string} firstPartyDomain The first party domain associated to the
|
|
| 346 | + * requests
|
|
| 347 | + * @param {number} userContextId The context ID associated to the request
|
|
| 348 | + * @returns {object} The credentials
|
|
| 349 | + */
|
|
| 350 | + #getSocksProxyCredentials(firstPartyDomain, userContextId) {
|
|
| 351 | + if (!this.#noncesForDomains.has(firstPartyDomain)) {
|
|
| 352 | + const nonce = this.#nonce();
|
|
| 353 | + logger.info(`New nonce for first party ${firstPartyDomain}: ${nonce}`);
|
|
| 354 | + this.#noncesForDomains.set(firstPartyDomain, nonce);
|
|
| 355 | + }
|
|
| 356 | + if (!this.#noncesForUserContextId.has(userContextId)) {
|
|
| 357 | + const nonce = this.#nonce();
|
|
| 358 | + logger.info(`New nonce for userContextId ${userContextId}: ${nonce}`);
|
|
| 359 | + this.#noncesForUserContextId.set(userContextId, nonce);
|
|
| 360 | + }
|
|
| 361 | + // TODO: How to properly handle the user-context? Should we use
|
|
| 362 | + // (domain, userContextId) pairs, instead of concatenating nonces?
|
|
| 363 | + return {
|
|
| 364 | + username: this.#makeUsername(firstPartyDomain, userContextId),
|
|
| 365 | + password:
|
|
| 366 | + this.#noncesForDomains.get(firstPartyDomain) +
|
|
| 367 | + this.#noncesForUserContextId.get(userContextId),
|
|
| 368 | + };
|
|
| 255 | 369 | }
|
| 256 | 370 | |
| 257 | 371 | /**
|
| 258 | 372 | * Combine the needed data into a username for the proxy.
|
| 373 | + *
|
|
| 374 | + * @param {string} domain The first-party domain associated to the request
|
|
| 375 | + * @param {integer} userContextId The userContextId associated to the request
|
|
| 259 | 376 | */
|
| 260 | 377 | #makeUsername(domain, userContextId) {
|
| 261 | 378 | if (!domain) {
|
| ... | ... | @@ -264,12 +381,26 @@ class TorDomainIsolatorImpl { |
| 264 | 381 | return `${domain}:${userContextId}`;
|
| 265 | 382 | }
|
| 266 | 383 | |
| 384 | + /**
|
|
| 385 | + * Combine SOCKS username and password into a string to use as ID.
|
|
| 386 | + *
|
|
| 387 | + * @param {string} username The SOCKS username
|
|
| 388 | + * @param {string} password The SOCKS password
|
|
| 389 | + * @returns {CircuitId} A string that combines username and password and can
|
|
| 390 | + * be used for map lookups.
|
|
| 391 | + */
|
|
| 392 | + #credentialsToId(username, password) {
|
|
| 393 | + return `${username}|${password}`;
|
|
| 394 | + }
|
|
| 395 | + |
|
| 267 | 396 | /**
|
| 268 | 397 | * Generate a new 128 bit random tag.
|
| 269 | 398 | *
|
| 270 | 399 | * Strictly speaking both using a cryptographic entropy source and using 128
|
| 271 | 400 | * bits of entropy for the tag are likely overkill, as correct behavior only
|
| 272 | 401 | * depends on how unlikely it is for there to be a collision.
|
| 402 | + *
|
|
| 403 | + * @returns {string} The random nonce
|
|
| 273 | 404 | */
|
| 274 | 405 | #nonce() {
|
| 275 | 406 | return Array.from(crypto.getRandomValues(new Uint8Array(16)), byte =>
|
| ... | ... | @@ -279,12 +410,18 @@ class TorDomainIsolatorImpl { |
| 279 | 410 | |
| 280 | 411 | /**
|
| 281 | 412 | * Re-generate the nonce for a certain domain.
|
| 413 | + *
|
|
| 414 | + * @param {string?} domain The first-party domain to re-create the nonce for.
|
|
| 415 | + * If empty or null, the catchall domain will be used.
|
|
| 282 | 416 | */
|
| 283 | 417 | #newCircuitForDomain(domain) {
|
| 284 | 418 | if (!domain) {
|
| 285 | 419 | domain = CATCHALL_DOMAIN;
|
| 286 | 420 | }
|
| 287 | 421 | this.#noncesForDomains.set(domain, this.#nonce());
|
| 422 | + if (domain === CATCHALL_DOMAIN) {
|
|
| 423 | + this.#catchallDirtySince = Date.now();
|
|
| 424 | + }
|
|
| 288 | 425 | logger.info(
|
| 289 | 426 | `New domain isolation for ${domain}: ${this.#noncesForDomains.get(
|
| 290 | 427 | domain
|
| ... | ... | @@ -296,6 +433,8 @@ class TorDomainIsolatorImpl { |
| 296 | 433 | * Re-generate the nonce for a userContextId.
|
| 297 | 434 | *
|
| 298 | 435 | * Currently, this function is not hooked to anything.
|
| 436 | + *
|
|
| 437 | + * @param {integer} userContextId The userContextId to re-create the nonce for
|
|
| 299 | 438 | */
|
| 300 | 439 | #newCircuitForUserContextId(userContextId) {
|
| 301 | 440 | this.#noncesForUserContextId.set(userContextId, this.#nonce());
|
| ... | ... | @@ -305,13 +444,182 @@ class TorDomainIsolatorImpl { |
| 305 | 444 | )}`
|
| 306 | 445 | );
|
| 307 | 446 | }
|
| 447 | + |
|
| 448 | + /**
|
|
| 449 | + * Try to extract a browser from a channel.
|
|
| 450 | + *
|
|
| 451 | + * @param {nsIChannel} channel The channel to extract the browser from
|
|
| 452 | + * @returns {MozBrowser?} The browser the channel is associated to
|
|
| 453 | + */
|
|
| 454 | + #getBrowserForChannel(channel) {
|
|
| 455 | + const browsers =
|
|
| 456 | + channel.loadInfo.browsingContext?.topChromeWindow?.gBrowser.browsers;
|
|
| 457 | + if (!browsers || !channel.loadInfo.browsingContext?.browserId) {
|
|
| 458 | + return null;
|
|
| 459 | + }
|
|
| 460 | + for (const browser of browsers) {
|
|
| 461 | + if (browser.browserId === channel.loadInfo.browsingContext.browserId) {
|
|
| 462 | + logger.debug(
|
|
| 463 | + "Matched browser with browserId",
|
|
| 464 | + channel.loadInfo.browsingContext.browserId
|
|
| 465 | + );
|
|
| 466 | + return browser;
|
|
| 467 | + }
|
|
| 468 | + }
|
|
| 469 | + // Expected to arrive here for example for the update checker.
|
|
| 470 | + // If we find a way to check that, we could raise the level to a warn.
|
|
| 471 | + logger.debug("Browser not matched", channel);
|
|
| 472 | + return null;
|
|
| 473 | + }
|
|
| 474 | + |
|
| 475 | + /**
|
|
| 476 | + * Associate the SOCKS credentials to a browser.
|
|
| 477 | + * If needed (the browser is associated for the first time, or it was already
|
|
| 478 | + * known but its credential changed), notify the related circuit display.
|
|
| 479 | + *
|
|
| 480 | + * @param {MozBrowser} browser The browser to track
|
|
| 481 | + * @param {string} username The SOCKS username
|
|
| 482 | + * @param {string} password The SOCKS password
|
|
| 483 | + */
|
|
| 484 | + #trackBrowser(browser, username, password) {
|
|
| 485 | + let browserCircuits = this.#browsers.get(browser.browserId);
|
|
| 486 | + if (!browserCircuits) {
|
|
| 487 | + browserCircuits = new Map();
|
|
| 488 | + this.#browsers.set(browser.browserId, browserCircuits);
|
|
| 489 | + }
|
|
| 490 | + const circuitIds = browserCircuits.get(username) ?? {};
|
|
| 491 | + const id = this.#credentialsToId(username, password);
|
|
| 492 | + if (circuitIds.current === id) {
|
|
| 493 | + // The circuit with these credentials was already built (we already knew
|
|
| 494 | + // its nodes, or we would not have promoted it to the current circuit).
|
|
| 495 | + // We do not need to do anything else, because we cannot detect a change
|
|
| 496 | + // of nodes here.
|
|
| 497 | + return;
|
|
| 498 | + }
|
|
| 499 | + |
|
| 500 | + logger.debug(
|
|
| 501 | + `Found new credentials ${username} ${password} for browser`,
|
|
| 502 | + browser
|
|
| 503 | + );
|
|
| 504 | + const circuit = this.#knownCircuits.get(id);
|
|
| 505 | + if (circuit?.length) {
|
|
| 506 | + circuitIds.current = id;
|
|
| 507 | + if (circuitIds.pending === id) {
|
|
| 508 | + circuitIds.pending = null;
|
|
| 509 | + }
|
|
| 510 | + browserCircuits.set(username, circuitIds);
|
|
| 511 | + // FIXME: We only notify the circuit display when we have a change that
|
|
| 512 | + // involves circuits whose nodes are known, for now. We need to resolve a
|
|
| 513 | + // few other techical problems (e.g., associate the circuit to the
|
|
| 514 | + // document?) and develop a UX with some animation to notify the circuit
|
|
| 515 | + // display more often.
|
|
| 516 | + // See tor-browser#41700 and tor-browser!699.
|
|
| 517 | + // In any case, notify the circuit display only after the internal map has
|
|
| 518 | + // been updated.
|
|
| 519 | + this.#notifyCircuitDisplay();
|
|
| 520 | + } else if (circuitIds.pending !== id) {
|
|
| 521 | + // We do not have node data, so we store that we might need to track this.
|
|
| 522 | + // Otherwise, when a circuit is ready, we do not know which browser was it
|
|
| 523 | + // used for.
|
|
| 524 | + circuitIds.pending = id;
|
|
| 525 | + browserCircuits.set(username, circuitIds);
|
|
| 526 | + }
|
|
| 527 | + }
|
|
| 528 | + |
|
| 529 | + /**
|
|
| 530 | + * Update a circuit, and notify the related circuit displays if it changed.
|
|
| 531 | + *
|
|
| 532 | + * This function is called when a certain stream has succeeded and so we can
|
|
| 533 | + * associate its SOCKS credential to the circuit it is using.
|
|
| 534 | + * We receive only the fingerprints of the circuit nodes, but they are enough
|
|
| 535 | + * to check if the circuit has changed. If it has, we also get the nodes'
|
|
| 536 | + * information through the control port.
|
|
| 537 | + *
|
|
| 538 | + * @param {string} username The SOCKS username
|
|
| 539 | + * @param {string} password The SOCKS password
|
|
| 540 | + * @param {NodeFingerprint[]} circuit The fingerprints of the nodes that
|
|
| 541 | + * compose the circuit
|
|
| 542 | + */
|
|
| 543 | + async #updateCircuit(username, password, circuit) {
|
|
| 544 | + const id = this.#credentialsToId(username, password);
|
|
| 545 | + let data = this.#knownCircuits.get(id) ?? [];
|
|
| 546 | + // Should we modify the lower layer to send a circuit identifier, instead?
|
|
| 547 | + if (
|
|
| 548 | + circuit.length === data.length &&
|
|
| 549 | + circuit.every((id, index) => id === data[index].fingerprint)
|
|
| 550 | + ) {
|
|
| 551 | + return;
|
|
| 552 | + }
|
|
| 553 | + |
|
| 554 | + data = await Promise.all(
|
|
| 555 | + circuit.map(fingerprint =>
|
|
| 556 | + lazy.TorProtocolService.getNodeInfo(fingerprint)
|
|
| 557 | + )
|
|
| 558 | + );
|
|
| 559 | + this.#knownCircuits.set(id, data);
|
|
| 560 | + // We know that something changed, but we cannot know if anyone is
|
|
| 561 | + // interested in this change. So, we have to notify all the possible
|
|
| 562 | + // consumers of the data in any case.
|
|
| 563 | + // Not being specific and let them check if they need to do something allows
|
|
| 564 | + // us to keep a simpler structure.
|
|
| 565 | + this.#notifyCircuitDisplay();
|
|
| 566 | + }
|
|
| 567 | + |
|
| 568 | + /**
|
|
| 569 | + * Broadcast a notification when a circuit changed, or a browser is changing
|
|
| 570 | + * circuit (which might happen also in case of navigation).
|
|
| 571 | + */
|
|
| 572 | + #notifyCircuitDisplay() {
|
|
| 573 | + Services.obs.notifyObservers(null, TOR_CIRCUIT_TOPIC);
|
|
| 574 | + }
|
|
| 575 | + |
|
| 576 | + /**
|
|
| 577 | + * Clear the known circuit information, when they are not needed anymore.
|
|
| 578 | + *
|
|
| 579 | + * We keep circuit data around for a while. We decouple it from the underlying
|
|
| 580 | + * tor circuit management in case the user clicks on the circuit display when
|
|
| 581 | + * circuit has long gone.
|
|
| 582 | + * However, data accumulate during a session. So, since we store all the
|
|
| 583 | + * browsers that used a circuit anyway, every now and then we check if we
|
|
| 584 | + * still know browsers using a certain circuits. If there are not, we forget
|
|
| 585 | + * about it.
|
|
| 586 | + *
|
|
| 587 | + * This function is run by an interval.
|
|
| 588 | + */
|
|
| 589 | + #clearKnownCircuits() {
|
|
| 590 | + logger.info("Running the circuit cleanup");
|
|
| 591 | + const windows = [];
|
|
| 592 | + const enumerator = Services.wm.getEnumerator("navigator:browser");
|
|
| 593 | + while (enumerator.hasMoreElements()) {
|
|
| 594 | + windows.push(enumerator.getNext());
|
|
| 595 | + }
|
|
| 596 | + const browsers = windows
|
|
| 597 | + .flatMap(win => win.gBrowser.browsers.map(b => b.browserId))
|
|
| 598 | + .filter(id => this.#browsers.has(id));
|
|
| 599 | + this.#browsers = new Map(browsers.map(id => [id, this.#browsers.get(id)]));
|
|
| 600 | + this.#knownCircuits = new Map(
|
|
| 601 | + Array.from(this.#browsers.values(), circuits =>
|
|
| 602 | + Array.from(circuits.values(), ids => {
|
|
| 603 | + const r = [];
|
|
| 604 | + const current = this.#knownCircuits.get(ids.current);
|
|
| 605 | + if (current) {
|
|
| 606 | + r.push([ids.current, current]);
|
|
| 607 | + }
|
|
| 608 | + const pending = this.#knownCircuits.get(ids.pending);
|
|
| 609 | + if (pending) {
|
|
| 610 | + r.push([ids.pending, pending]);
|
|
| 611 | + }
|
|
| 612 | + return r;
|
|
| 613 | + })
|
|
| 614 | + ).flat(2)
|
|
| 615 | + );
|
|
| 616 | + }
|
|
| 308 | 617 | }
|
| 309 | 618 | |
| 310 | 619 | /**
|
| 311 | 620 | * Get the first party domain for a certain browser.
|
| 312 | 621 | *
|
| 313 | - * @param browser The browser to get the FP-domain for.
|
|
| 314 | - *
|
|
| 622 | + * @param {MozBrowser} browser The browser to get the FP-domain for.
|
|
| 315 | 623 | * Please notice that it should be gBrowser.selectedBrowser, because
|
| 316 | 624 | * browser.documentURI is the actual shown page, and might be an error page.
|
| 317 | 625 | * In this case, we rely on currentURI, which for gBrowser is an alias of
|
| ... | ... | @@ -358,6 +666,6 @@ function getDomainForBrowser(browser) { |
| 358 | 666 | return fpd;
|
| 359 | 667 | }
|
| 360 | 668 | |
| 361 | -const TorDomainIsolator = new TorDomainIsolatorImpl();
|
|
| 669 | +export const TorDomainIsolator = new TorDomainIsolatorImpl();
|
|
| 362 | 670 | // Reduce global vars pollution
|
| 363 | 671 | TorDomainIsolator.getDomainForBrowser = getDomainForBrowser; |
| ... | ... | @@ -19,6 +19,10 @@ ChromeUtils.defineModuleGetter( |
| 19 | 19 | "resource://torbutton/modules/tor-control-port.js"
|
| 20 | 20 | );
|
| 21 | 21 | |
| 22 | +ChromeUtils.defineESModuleGetters(lazy, {
|
|
| 23 | + TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
|
|
| 24 | +});
|
|
| 25 | + |
|
| 22 | 26 | const logger = new ConsoleAPI({
|
| 23 | 27 | maxLogLevel: "warn",
|
| 24 | 28 | maxLogLevelPref: "browser.tor_monitor_service.log_level",
|
| ... | ... | @@ -37,12 +41,34 @@ const TorTopics = Object.freeze({ |
| 37 | 41 | ProcessRestarted: "TorProcessRestarted",
|
| 38 | 42 | });
|
| 39 | 43 | |
| 44 | +export const TorMonitorTopics = Object.freeze({
|
|
| 45 | + BridgeChanged: "TorBridgeChanged",
|
|
| 46 | + StreamSucceeded: "TorStreamSucceeded",
|
|
| 47 | +});
|
|
| 48 | + |
|
| 40 | 49 | const ControlConnTimings = Object.freeze({
|
| 41 | 50 | initialDelayMS: 25, // Wait 25ms after the process has started, before trying to connect
|
| 42 | 51 | maxRetryMS: 10000, // Retry at most every 10 seconds
|
| 43 | 52 | timeoutMS: 5 * 60 * 1000, // Wait at most 5 minutes for tor to start
|
| 44 | 53 | });
|
| 45 | 54 | |
| 55 | +/**
|
|
| 56 | + * From control-spec.txt:
|
|
| 57 | + * CircuitID = 1*16 IDChar
|
|
| 58 | + * IDChar = ALPHA / DIGIT
|
|
| 59 | + * Currently, Tor only uses digits, but this may change.
|
|
| 60 | + *
|
|
| 61 | + * @typedef {string} CircuitID
|
|
| 62 | + */
|
|
| 63 | +/**
|
|
| 64 | + * The fingerprint of a node.
|
|
| 65 | + * From control-spec.txt:
|
|
| 66 | + * Fingerprint = "$" 40*HEXDIG
|
|
| 67 | + * However, we do not keep the $ in our structures.
|
|
| 68 | + *
|
|
| 69 | + * @typedef {string} NodeFingerprint
|
|
| 70 | + */
|
|
| 71 | + |
|
| 46 | 72 | /**
|
| 47 | 73 | * This service monitors an existing Tor instance, or starts one, if needed, and
|
| 48 | 74 | * then starts monitoring it.
|
| ... | ... | @@ -52,7 +78,7 @@ const ControlConnTimings = Object.freeze({ |
| 52 | 78 | */
|
| 53 | 79 | export const TorMonitorService = {
|
| 54 | 80 | _connection: null,
|
| 55 | - _eventsToMonitor: Object.freeze(["STATUS_CLIENT", "NOTICE", "WARN", "ERR"]),
|
|
| 81 | + _eventHandlers: {},
|
|
| 56 | 82 | _torLog: [], // Array of objects with date, type, and msg properties.
|
| 57 | 83 | _startTimeout: null,
|
| 58 | 84 | |
| ... | ... | @@ -64,6 +90,28 @@ export const TorMonitorService = { |
| 64 | 90 | |
| 65 | 91 | _inited: false,
|
| 66 | 92 | |
| 93 | + /**
|
|
| 94 | + * Stores the nodes of a circuit. Keys are cicuit IDs, and values are the node
|
|
| 95 | + * fingerprints.
|
|
| 96 | + *
|
|
| 97 | + * Theoretically, we could hook this map up to the new identity notification,
|
|
| 98 | + * but in practice it does not work. Tor pre-builds circuits, and the NEWNYM
|
|
| 99 | + * signal does not affect them. So, we might end up using a circuit that was
|
|
| 100 | + * built before the new identity but not yet used. If we cleaned the map, we
|
|
| 101 | + * risked of not having the data about it.
|
|
| 102 | + *
|
|
| 103 | + * @type {Map<CircuitID, NodeFingerprint[]>}
|
|
| 104 | + */
|
|
| 105 | + _circuits: new Map(),
|
|
| 106 | + /**
|
|
| 107 | + * The last used bridge, or null if bridges are not in use or if it was not
|
|
| 108 | + * possible to detect the bridge. This needs the user to have specified bridge
|
|
| 109 | + * lines with fingerprints to work.
|
|
| 110 | + *
|
|
| 111 | + * @type {NodeFingerprint?}
|
|
| 112 | + */
|
|
| 113 | + _currentBridge: null,
|
|
| 114 | + |
|
| 67 | 115 | // Public methods
|
| 68 | 116 | |
| 69 | 117 | // Starts Tor, if needed, and starts monitoring for events
|
| ... | ... | @@ -72,14 +120,28 @@ export const TorMonitorService = { |
| 72 | 120 | return;
|
| 73 | 121 | }
|
| 74 | 122 | this._inited = true;
|
| 123 | + |
|
| 124 | + // We always liten to these events, because they are needed for the circuit
|
|
| 125 | + // display.
|
|
| 126 | + this._eventHandlers = new Map([
|
|
| 127 | + ["CIRC", this._processCircEvent.bind(this)],
|
|
| 128 | + ["STREAM", this._processStreamEvent.bind(this)],
|
|
| 129 | + ]);
|
|
| 130 | + |
|
| 75 | 131 | if (this.ownsTorDaemon) {
|
| 132 | + // When we own the tor daemon, we listen to more events, that are used
|
|
| 133 | + // for about:torconnect or for showing the logs in the settings page.
|
|
| 134 | + this._eventHandlers.set("STATUS_CLIENT", (_eventType, lines) =>
|
|
| 135 | + this._processBootstrapStatus(lines[0], false)
|
|
| 136 | + );
|
|
| 137 | + this._eventHandlers.set("NOTICE", this._processLog.bind(this));
|
|
| 138 | + this._eventHandlers.set("WARN", this._processLog.bind(this));
|
|
| 139 | + this._eventHandlers.set("ERR", this._processLog.bind(this));
|
|
| 76 | 140 | this._controlTor();
|
| 77 | 141 | } else {
|
| 78 | - logger.info(
|
|
| 79 | - "Not starting the event monitor, as we do not own the Tor daemon."
|
|
| 80 | - );
|
|
| 142 | + this._startEventMonitor();
|
|
| 81 | 143 | }
|
| 82 | - logger.debug("TorMonitorService initialized");
|
|
| 144 | + logger.info("TorMonitorService initialized");
|
|
| 83 | 145 | },
|
| 84 | 146 | |
| 85 | 147 | // Closes the connection that monitors for events.
|
| ... | ... | @@ -153,6 +215,18 @@ export const TorMonitorService = { |
| 153 | 215 | return !!this._connection;
|
| 154 | 216 | },
|
| 155 | 217 | |
| 218 | + /**
|
|
| 219 | + * Return the data about the current bridge, if any, or null.
|
|
| 220 | + * We can detect bridge only when the configured bridge lines include the
|
|
| 221 | + * fingerprints.
|
|
| 222 | + *
|
|
| 223 | + * @returns {NodeData?} The node information, or null if the first node
|
|
| 224 | + * is not a bridge, or no circuit has been opened, yet.
|
|
| 225 | + */
|
|
| 226 | + get currentBridge() {
|
|
| 227 | + return this._currentBridge;
|
|
| 228 | + },
|
|
| 229 | + |
|
| 156 | 230 | // Private methods
|
| 157 | 231 | |
| 158 | 232 | async _startProcess() {
|
| ... | ... | @@ -272,7 +346,7 @@ export const TorMonitorService = { |
| 272 | 346 | |
| 273 | 347 | // TODO: optionally monitor INFO and DEBUG log messages.
|
| 274 | 348 | let reply = await conn.sendCommand(
|
| 275 | - "SETEVENTS " + this._eventsToMonitor.join(" ")
|
|
| 349 | + "SETEVENTS " + Array.from(this._eventHandlers.keys()).join(" ")
|
|
| 276 | 350 | );
|
| 277 | 351 | reply = TorParsers.parseCommandResponse(reply);
|
| 278 | 352 | if (!TorParsers.commandSucceeded(reply)) {
|
| ... | ... | @@ -281,14 +355,10 @@ export const TorMonitorService = { |
| 281 | 355 | return false;
|
| 282 | 356 | }
|
| 283 | 357 | |
| 284 | - // FIXME: At the moment it is not possible to start the event monitor
|
|
| 285 | - // when we do start the tor process. So, does it make sense to keep this
|
|
| 286 | - // control?
|
|
| 287 | 358 | if (this._torProcess) {
|
| 288 | 359 | this._torProcess.connectionWorked();
|
| 289 | 360 | }
|
| 290 | - |
|
| 291 | - if (!TorLauncherUtil.shouldOnlyConfigureTor) {
|
|
| 361 | + if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) {
|
|
| 292 | 362 | try {
|
| 293 | 363 | await this._takeTorOwnership(conn);
|
| 294 | 364 | } catch (e) {
|
| ... | ... | @@ -297,7 +367,31 @@ export const TorMonitorService = { |
| 297 | 367 | }
|
| 298 | 368 | |
| 299 | 369 | this._connection = conn;
|
| 300 | - this._waitForEventData();
|
|
| 370 | + |
|
| 371 | + for (const [type, callback] of this._eventHandlers.entries()) {
|
|
| 372 | + this._monitorEvent(type, callback);
|
|
| 373 | + }
|
|
| 374 | + |
|
| 375 | + // Populate the circuit map already, in case we are connecting to an
|
|
| 376 | + // external tor daemon.
|
|
| 377 | + try {
|
|
| 378 | + const reply = await this._connection.sendCommand(
|
|
| 379 | + "GETINFO circuit-status"
|
|
| 380 | + );
|
|
| 381 | + const lines = reply.split(/\r?\n/);
|
|
| 382 | + if (lines.shift() === "250+circuit-status=") {
|
|
| 383 | + for (const line of lines) {
|
|
| 384 | + if (line === ".") {
|
|
| 385 | + break;
|
|
| 386 | + }
|
|
| 387 | + // _processCircEvent processes only one line at a time
|
|
| 388 | + this._processCircEvent("CIRC", [line]);
|
|
| 389 | + }
|
|
| 390 | + }
|
|
| 391 | + } catch (e) {
|
|
| 392 | + logger.warn("Could not populate the initial circuit map", e);
|
|
| 393 | + }
|
|
| 394 | + |
|
| 301 | 395 | return true;
|
| 302 | 396 | },
|
| 303 | 397 | |
| ... | ... | @@ -318,65 +412,49 @@ export const TorMonitorService = { |
| 318 | 412 | }
|
| 319 | 413 | },
|
| 320 | 414 | |
| 321 | - _waitForEventData() {
|
|
| 322 | - if (!this._connection) {
|
|
| 323 | - return;
|
|
| 324 | - }
|
|
| 325 | - logger.debug("Start watching events:", this._eventsToMonitor);
|
|
| 415 | + _monitorEvent(type, callback) {
|
|
| 416 | + logger.info(`Watching events of type ${type}.`);
|
|
| 326 | 417 | let replyObj = {};
|
| 327 | - for (const torEvent of this._eventsToMonitor) {
|
|
| 328 | - this._connection.watchEvent(
|
|
| 329 | - torEvent,
|
|
| 330 | - null,
|
|
| 331 | - line => {
|
|
| 332 | - if (!line) {
|
|
| 333 | - return;
|
|
| 334 | - }
|
|
| 335 | - logger.debug("Event response: ", line);
|
|
| 336 | - const isComplete = TorParsers.parseReplyLine(line, replyObj);
|
|
| 337 | - if (isComplete) {
|
|
| 338 | - this._processEventReply(replyObj);
|
|
| 339 | - replyObj = {};
|
|
| 340 | - }
|
|
| 341 | - },
|
|
| 342 | - true
|
|
| 343 | - );
|
|
| 344 | - }
|
|
| 418 | + this._connection.watchEvent(
|
|
| 419 | + type,
|
|
| 420 | + null,
|
|
| 421 | + line => {
|
|
| 422 | + if (!line) {
|
|
| 423 | + return;
|
|
| 424 | + }
|
|
| 425 | + logger.debug("Event response: ", line);
|
|
| 426 | + const isComplete = TorParsers.parseReplyLine(line, replyObj);
|
|
| 427 | + if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) {
|
|
| 428 | + return;
|
|
| 429 | + }
|
|
| 430 | + const reply = replyObj;
|
|
| 431 | + replyObj = {};
|
|
| 432 | + if (reply.statusCode !== TorStatuses.EventNotification) {
|
|
| 433 | + logger.error("Unexpected event status code:", reply.statusCode);
|
|
| 434 | + return;
|
|
| 435 | + }
|
|
| 436 | + if (!reply.lineArray[0].startsWith(`${type} `)) {
|
|
| 437 | + logger.error("Wrong format for the first line:", reply.lineArray[0]);
|
|
| 438 | + return;
|
|
| 439 | + }
|
|
| 440 | + reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1);
|
|
| 441 | + try {
|
|
| 442 | + callback(type, reply.lineArray);
|
|
| 443 | + } catch (e) {
|
|
| 444 | + logger.error("Exception while handling an event", reply, e);
|
|
| 445 | + }
|
|
| 446 | + },
|
|
| 447 | + true
|
|
| 448 | + );
|
|
| 345 | 449 | },
|
| 346 | 450 | |
| 347 | - _processEventReply(aReply) {
|
|
| 348 | - if (aReply._parseError || !aReply.lineArray.length) {
|
|
| 349 | - return;
|
|
| 350 | - }
|
|
| 351 | - |
|
| 352 | - if (aReply.statusCode !== TorStatuses.EventNotification) {
|
|
| 353 | - logger.warn("Unexpected event status code:", aReply.statusCode);
|
|
| 354 | - return;
|
|
| 355 | - }
|
|
| 356 | - |
|
| 357 | - // TODO: do we need to handle multiple lines?
|
|
| 358 | - const s = aReply.lineArray[0];
|
|
| 359 | - const idx = s.indexOf(" ");
|
|
| 360 | - if (idx === -1) {
|
|
| 361 | - return;
|
|
| 362 | - }
|
|
| 363 | - const eventType = s.substring(0, idx);
|
|
| 364 | - const msg = s.substring(idx + 1).trim();
|
|
| 365 | - |
|
| 366 | - if (eventType === "STATUS_CLIENT") {
|
|
| 367 | - this._processBootstrapStatus(msg, false);
|
|
| 368 | - return;
|
|
| 369 | - } else if (!this._eventsToMonitor.includes(eventType)) {
|
|
| 370 | - logger.debug(`Dropping unlistened event ${eventType}`);
|
|
| 371 | - return;
|
|
| 372 | - }
|
|
| 373 | - |
|
| 374 | - if (eventType === "WARN" || eventType === "ERR") {
|
|
| 451 | + _processLog(type, lines) {
|
|
| 452 | + if (type === "WARN" || type === "ERR") {
|
|
| 375 | 453 | // Notify so that Copy Log can be enabled.
|
| 376 | 454 | Services.obs.notifyObservers(null, TorTopics.HasWarnOrErr);
|
| 377 | 455 | }
|
| 378 | 456 | |
| 379 | - const now = new Date();
|
|
| 457 | + const date = new Date();
|
|
| 380 | 458 | const maxEntries = Services.prefs.getIntPref(
|
| 381 | 459 | "extensions.torlauncher.max_tor_log_entries",
|
| 382 | 460 | 1000
|
| ... | ... | @@ -384,8 +462,10 @@ export const TorMonitorService = { |
| 384 | 462 | if (maxEntries > 0 && this._torLog.length >= maxEntries) {
|
| 385 | 463 | this._torLog.splice(0, 1);
|
| 386 | 464 | }
|
| 387 | - this._torLog.push({ date: now, type: eventType, msg });
|
|
| 388 | - const logString = `Tor ${eventType}: ${msg}`;
|
|
| 465 | + |
|
| 466 | + const msg = lines.join("\n");
|
|
| 467 | + this._torLog.push({ date, type, msg });
|
|
| 468 | + const logString = `Tor ${type}: ${msg}`;
|
|
| 389 | 469 | logger.info(logString);
|
| 390 | 470 | },
|
| 391 | 471 | |
| ... | ... | @@ -461,8 +541,108 @@ export const TorMonitorService = { |
| 461 | 541 | }
|
| 462 | 542 | },
|
| 463 | 543 | |
| 544 | + async _processCircEvent(_type, lines) {
|
|
| 545 | + const builtEvent =
|
|
| 546 | + /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
|
|
| 547 | + lines[0]
|
|
| 548 | + );
|
|
| 549 | + const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]);
|
|
| 550 | + if (builtEvent) {
|
|
| 551 | + const fp = /\$([0-9a-fA-F]{40})/g;
|
|
| 552 | + const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
|
|
| 553 | + g[1].toUpperCase()
|
|
| 554 | + );
|
|
| 555 | + this._circuits.set(builtEvent.groups.CircuitID, nodes);
|
|
| 556 | + // Ignore circuits of length 1, that are used, for example, to probe
|
|
| 557 | + // bridges. So, only store them, since we might see streams that use them,
|
|
| 558 | + // but then early-return.
|
|
| 559 | + if (nodes.length === 1) {
|
|
| 560 | + return;
|
|
| 561 | + }
|
|
| 562 | + // In some cases, we might already receive SOCKS credentials in the line.
|
|
| 563 | + // However, this might be a problem with onion services: we get also a
|
|
| 564 | + // 4-hop circuit that we likely do not want to show to the user,
|
|
| 565 | + // especially because it is used only temporarily, and it would need a
|
|
| 566 | + // technical explaination.
|
|
| 567 | + // this._checkCredentials(lines[0], nodes);
|
|
| 568 | + if (this._currentBridge?.fingerprint !== nodes[0]) {
|
|
| 569 | + const nodeInfo = await lazy.TorProtocolService.getNodeInfo(nodes[0]);
|
|
| 570 | + let notify = false;
|
|
| 571 | + if (nodeInfo?.bridgeType) {
|
|
| 572 | + logger.info(`Bridge changed to ${nodes[0]}`);
|
|
| 573 | + this._currentBridge = nodeInfo;
|
|
| 574 | + notify = true;
|
|
| 575 | + } else if (this._currentBridge) {
|
|
| 576 | + logger.info("Bridges disabled");
|
|
| 577 | + this._currentBridge = null;
|
|
| 578 | + notify = true;
|
|
| 579 | + }
|
|
| 580 | + if (notify) {
|
|
| 581 | + Services.obs.notifyObservers(
|
|
| 582 | + null,
|
|
| 583 | + TorMonitorTopics.BridgeChanged,
|
|
| 584 | + this._currentBridge
|
|
| 585 | + );
|
|
| 586 | + }
|
|
| 587 | + }
|
|
| 588 | + } else if (closedEvent) {
|
|
| 589 | + this._circuits.delete(closedEvent.groups.ID);
|
|
| 590 | + }
|
|
| 591 | + },
|
|
| 592 | + |
|
| 593 | + _processStreamEvent(_type, lines) {
|
|
| 594 | + // The first block is the stream ID, which we do not need at the moment.
|
|
| 595 | + const succeeedEvent =
|
|
| 596 | + /^[a-zA-Z0-9]{1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec(
|
|
| 597 | + lines[0]
|
|
| 598 | + );
|
|
| 599 | + if (!succeeedEvent) {
|
|
| 600 | + return;
|
|
| 601 | + }
|
|
| 602 | + const circuit = this._circuits.get(succeeedEvent.groups.CircuitID);
|
|
| 603 | + if (!circuit) {
|
|
| 604 | + logger.error(
|
|
| 605 | + "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers.",
|
|
| 606 | + lines[0]
|
|
| 607 | + );
|
|
| 608 | + return;
|
|
| 609 | + }
|
|
| 610 | + this._checkCredentials(lines[0], circuit);
|
|
| 611 | + },
|
|
| 612 | + |
|
| 613 | + /**
|
|
| 614 | + * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and
|
|
| 615 | + * SOCKS_PASSWORD. In case, notify observers that we could associate a certain
|
|
| 616 | + * circuit to these credentials.
|
|
| 617 | + *
|
|
| 618 | + * @param {string} line The circ or stream line to check
|
|
| 619 | + * @param {NodeFingerprint[]} circuit The fingerprints of the nodes in the
|
|
| 620 | + * circuit.
|
|
| 621 | + */
|
|
| 622 | + _checkCredentials(line, circuit) {
|
|
| 623 | + const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line);
|
|
| 624 | + const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line);
|
|
| 625 | + if (!username || !password) {
|
|
| 626 | + return;
|
|
| 627 | + }
|
|
| 628 | + Services.obs.notifyObservers(
|
|
| 629 | + {
|
|
| 630 | + wrappedJSObject: {
|
|
| 631 | + username: TorParsers.unescapeString(username[1]),
|
|
| 632 | + password: TorParsers.unescapeString(password[1]),
|
|
| 633 | + circuit,
|
|
| 634 | + },
|
|
| 635 | + },
|
|
| 636 | + TorMonitorTopics.StreamSucceeded
|
|
| 637 | + );
|
|
| 638 | + },
|
|
| 639 | + |
|
| 464 | 640 | _shutDownEventMonitor() {
|
| 465 | - this._connection?.close();
|
|
| 641 | + try {
|
|
| 642 | + this._connection?.close();
|
|
| 643 | + } catch (e) {
|
|
| 644 | + logger.error("Could not close the connection to the control port", e);
|
|
| 645 | + }
|
|
| 466 | 646 | this._connection = null;
|
| 467 | 647 | if (this._startTimeout !== null) {
|
| 468 | 648 | clearTimeout(this._startTimeout);
|
| ... | ... | @@ -181,12 +181,12 @@ export const TorParsers = Object.freeze({ |
| 181 | 181 | return aStr;
|
| 182 | 182 | }
|
| 183 | 183 | const escaped = aStr
|
| 184 | - .replace("\\", "\\\\")
|
|
| 185 | - .replace('"', '\\"')
|
|
| 186 | - .replace("\n", "\\n")
|
|
| 187 | - .replace("\r", "\\r")
|
|
| 188 | - .replace("\t", "\\t")
|
|
| 189 | - .replace(/[^\x20-\x7e]+/g, text => {
|
|
| 184 | + .replaceAll("\\", "\\\\")
|
|
| 185 | + .replaceAll('"', '\\"')
|
|
| 186 | + .replaceAll("\n", "\\n")
|
|
| 187 | + .replaceAll("\r", "\\r")
|
|
| 188 | + .replaceAll("\t", "\\t")
|
|
| 189 | + .replaceAll(/[^\x20-\x7e]+/g, text => {
|
|
| 190 | 190 | const encoder = new TextEncoder();
|
| 191 | 191 | return Array.from(
|
| 192 | 192 | encoder.encode(text),
|
| ... | ... | @@ -40,6 +40,20 @@ const logger = new ConsoleAPI({ |
| 40 | 40 | prefix: "TorProtocolService",
|
| 41 | 41 | });
|
| 42 | 42 | |
| 43 | +/**
|
|
| 44 | + * Stores the data associated with a circuit node.
|
|
| 45 | + *
|
|
| 46 | + * @typedef NodeData
|
|
| 47 | + * @property {string} fingerprint The node fingerprint.
|
|
| 48 | + * @property {string[]} ipAddrs - The ip addresses associated with this node.
|
|
| 49 | + * @property {string?} bridgeType - The bridge type for this node, or "" if the
|
|
| 50 | + * node is a bridge but the type is unknown, or null if this is not a bridge
|
|
| 51 | + * node.
|
|
| 52 | + * @property {string?} regionCode - An upper case 2-letter ISO3166-1 code for
|
|
| 53 | + * the first ip address, or null if there is no region. This should also be a
|
|
| 54 | + * valid BCP47 Region subtag.
|
|
| 55 | + */
|
|
| 56 | + |
|
| 43 | 57 | // Manage the connection to tor's control port, to update its settings and query
|
| 44 | 58 | // other useful information.
|
| 45 | 59 | //
|
| ... | ... | @@ -188,6 +202,89 @@ export const TorProtocolService = { |
| 188 | 202 | return TorParsers.parseReply(cmd, keyword, response);
|
| 189 | 203 | },
|
| 190 | 204 | |
| 205 | + async getBridges() {
|
|
| 206 | + // Ideally, we would not need this function, because we should be the one
|
|
| 207 | + // setting them with TorSettings. However, TorSettings is not notified of
|
|
| 208 | + // change of settings. So, asking tor directly with the control connection
|
|
| 209 | + // is the most reliable way of getting the configured bridges, at the
|
|
| 210 | + // moment. Also, we are using this for the circuit display, which should
|
|
| 211 | + // work also when we are not configuring the tor daemon, but just using it.
|
|
| 212 | + return this._withConnection(conn => {
|
|
| 213 | + return conn.getConf("bridge");
|
|
| 214 | + });
|
|
| 215 | + },
|
|
| 216 | + |
|
| 217 | + /**
|
|
| 218 | + * Returns tha data about a relay or a bridge.
|
|
| 219 | + *
|
|
| 220 | + * @param {string} id The fingerprint of the node to get data about
|
|
| 221 | + * @returns {NodeData}
|
|
| 222 | + */
|
|
| 223 | + async getNodeInfo(id) {
|
|
| 224 | + return this._withConnection(async conn => {
|
|
| 225 | + const node = {
|
|
| 226 | + fingerprint: id,
|
|
| 227 | + ipAddrs: [],
|
|
| 228 | + bridgeType: null,
|
|
| 229 | + regionCode: null,
|
|
| 230 | + };
|
|
| 231 | + const bridge = (await conn.getConf("bridge"))?.find(
|
|
| 232 | + foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase()
|
|
| 233 | + );
|
|
| 234 | + const addrRe = /^\[?([^\]]+)\]?:\d+$/;
|
|
| 235 | + if (bridge) {
|
|
| 236 | + node.bridgeType = bridge.type ?? "";
|
|
| 237 | + // Attempt to get an IP address from bridge address string.
|
|
| 238 | + const ip = bridge.address.match(addrRe)?.[1];
|
|
| 239 | + if (ip && !ip.startsWith("0.")) {
|
|
| 240 | + node.ipAddrs.push(ip);
|
|
| 241 | + }
|
|
| 242 | + } else {
|
|
| 243 | + // Either dealing with a relay, or a bridge whose fingerprint is not
|
|
| 244 | + // saved in torrc.
|
|
| 245 | + const info = await conn.getInfo(`ns/id/${id}`);
|
|
| 246 | + if (info.IP && !info.IP.startsWith("0.")) {
|
|
| 247 | + node.ipAddrs.push(info.IP);
|
|
| 248 | + }
|
|
| 249 | + const ip6 = info.IPv6?.match(addrRe)?.[1];
|
|
| 250 | + if (ip6) {
|
|
| 251 | + node.ipAddrs.push(ip6);
|
|
| 252 | + }
|
|
| 253 | + }
|
|
| 254 | + if (node.ipAddrs.length) {
|
|
| 255 | + // Get the country code for the node's IP address.
|
|
| 256 | + let regionCode;
|
|
| 257 | + try {
|
|
| 258 | + // Expect a 2-letter ISO3166-1 code, which should also be a valid
|
|
| 259 | + // BCP47 Region subtag.
|
|
| 260 | + regionCode = await conn.getInfo("ip-to-country/" + node.ipAddrs[0]);
|
|
| 261 | + } catch {}
|
|
| 262 | + if (regionCode && regionCode !== "??") {
|
|
| 263 | + node.regionCode = regionCode.toUpperCase();
|
|
| 264 | + }
|
|
| 265 | + }
|
|
| 266 | + return node;
|
|
| 267 | + });
|
|
| 268 | + },
|
|
| 269 | + |
|
| 270 | + async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) {
|
|
| 271 | + return this._withConnection(conn => {
|
|
| 272 | + return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent);
|
|
| 273 | + });
|
|
| 274 | + },
|
|
| 275 | + |
|
| 276 | + async onionAuthRemove(hsAddress) {
|
|
| 277 | + return this._withConnection(conn => {
|
|
| 278 | + return conn.onionAuthRemove(hsAddress);
|
|
| 279 | + });
|
|
| 280 | + },
|
|
| 281 | + |
|
| 282 | + async onionAuthViewKeys() {
|
|
| 283 | + return this._withConnection(conn => {
|
|
| 284 | + return conn.onionAuthViewKeys();
|
|
| 285 | + });
|
|
| 286 | + },
|
|
| 287 | + |
|
| 191 | 288 | // TODO: transform the following 4 functions in getters. At the moment they
|
| 192 | 289 | // are also used in torbutton.
|
| 193 | 290 | |
| ... | ... | @@ -630,6 +727,16 @@ export const TorProtocolService = { |
| 630 | 727 | }
|
| 631 | 728 | },
|
| 632 | 729 | |
| 730 | + async _withConnection(func) {
|
|
| 731 | + // TODO: Make more robust?
|
|
| 732 | + const conn = await this._getConnection();
|
|
| 733 | + try {
|
|
| 734 | + return await func(conn);
|
|
| 735 | + } finally {
|
|
| 736 | + this._returnConnection();
|
|
| 737 | + }
|
|
| 738 | + },
|
|
| 739 | + |
|
| 633 | 740 | // If aConn is omitted, the cached connection is closed.
|
| 634 | 741 | _closeConnection() {
|
| 635 | 742 | if (this._controlConnection) {
|
| ... | ... | @@ -3,6 +3,7 @@ const lazy = {}; |
| 3 | 3 | // We will use the modules only when the profile is loaded, so prefer lazy
|
| 4 | 4 | // loading
|
| 5 | 5 | ChromeUtils.defineESModuleGetters(lazy, {
|
| 6 | + TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
|
|
| 6 | 7 | TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
|
| 7 | 8 | TorMonitorService: "resource://gre/modules/TorMonitorService.sys.mjs",
|
| 8 | 9 | TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
|
| ... | ... | @@ -19,12 +20,6 @@ ChromeUtils.defineModuleGetter( |
| 19 | 20 | "resource:///modules/TorSettings.jsm"
|
| 20 | 21 | );
|
| 21 | 22 | |
| 22 | -ChromeUtils.defineModuleGetter(
|
|
| 23 | - lazy,
|
|
| 24 | - "TorDomainIsolator",
|
|
| 25 | - "resource://gre/modules/TorDomainIsolator.jsm"
|
|
| 26 | -);
|
|
| 27 | - |
|
| 28 | 23 | /* Browser observer topis */
|
| 29 | 24 | const BrowserTopics = Object.freeze({
|
| 30 | 25 | ProfileAfterChange: "profile-after-change",
|
| 1 | 1 | EXTRA_JS_MODULES += [
|
| 2 | 2 | "TorBootstrapRequest.sys.mjs",
|
| 3 | - "TorDomainIsolator.jsm",
|
|
| 3 | + "TorDomainIsolator.sys.mjs",
|
|
| 4 | 4 | "TorLauncherUtil.sys.mjs",
|
| 5 | 5 | "TorMonitorService.sys.mjs",
|
| 6 | 6 | "TorParsers.sys.mjs",
|