richard pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser

Commits:

11 changed files:

Changes:

  • browser/base/content/browser.js
    ... ... @@ -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",
    

  • browser/components/onionservices/content/authPrompt.js
    ... ... @@ -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(
    

  • browser/components/onionservices/content/savedKeysDialog.js
    ... ... @@ -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);
    

  • browser/components/torcircuit/content/torCircuitPanel.js
    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.
    

  • browser/components/torpreferences/content/connectionPane.js
    ... ... @@ -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();
    

  • toolkit/components/tor-launcher/TorDomainIsolator.jsmtoolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
    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;

  • toolkit/components/tor-launcher/TorMonitorService.sys.mjs
    ... ... @@ -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);
    

  • toolkit/components/tor-launcher/TorParsers.sys.mjs
    ... ... @@ -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),
    

  • toolkit/components/tor-launcher/TorProtocolService.sys.mjs
    ... ... @@ -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) {
    

  • toolkit/components/tor-launcher/TorStartupService.sys.mjs
    ... ... @@ -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",
    

  • toolkit/components/tor-launcher/moz.build
    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",