Pier Angelo Vendrame pushed to branch tor-browser-128.6.0esr-14.5-1 at The Tor Project / Applications / Tor Browser

Commits:

6 changed files:

Changes:

  • browser/app/profile/000-tor-browser.js
    ... ... @@ -143,6 +143,7 @@ pref("browser.tor_provider.cp_log_level", "Warn");
    143 143
     pref("lox.log_level", "Warn");
    
    144 144
     pref("torbrowser.bootstrap.log_level", "Info");
    
    145 145
     pref("browser.torsettings.log_level", "Warn");
    
    146
    +pref("browser.torMoat.loglevel", "Warn");
    
    146 147
     pref("browser.tordomainisolator.loglevel", "Warn");
    
    147 148
     pref("browser.torcircuitpanel.loglevel", "Log");
    
    148 149
     pref("browser.tor_android.log_level", "Info");
    

  • browser/components/torpreferences/content/requestBridgeDialog.js
    ... ... @@ -119,6 +119,9 @@ const gRequestBridgeDialog = {
    119 119
       },
    
    120 120
     
    
    121 121
       _setcaptchaImage(uri) {
    
    122
    +    if (!uri) {
    
    123
    +      return;
    
    124
    +    }
    
    122 125
         if (uri != this._captchaImage.src) {
    
    123 126
           this._captchaImage.src = uri;
    
    124 127
           this._dialogHeader.setAttribute(
    

  • toolkit/modules/BridgeDB.sys.mjs
    ... ... @@ -13,6 +13,16 @@ export var BridgeDB = {
    13 13
       _challenge: null,
    
    14 14
       _image: null,
    
    15 15
       _bridges: null,
    
    16
    +  /**
    
    17
    +   * A collection of controllers to abort any ongoing Moat requests if the
    
    18
    +   * dialog is closed.
    
    19
    +   *
    
    20
    +   * NOTE: We do not expect this set to ever contain more than one instance.
    
    21
    +   * However the public API has no assurances to prevent multiple calls.
    
    22
    +   *
    
    23
    +   * @type {Set<AbortController>}
    
    24
    +   */
    
    25
    +  _moatAbortControllers: new Set(),
    
    16 26
     
    
    17 27
       get currentCaptchaImage() {
    
    18 28
         return this._image;
    
    ... ... @@ -28,13 +38,18 @@ export var BridgeDB = {
    28 38
           await this._moatRPC.init();
    
    29 39
         }
    
    30 40
     
    
    31
    -    const response = await this._moatRPC.check(
    
    32
    -      "obfs4",
    
    33
    -      this._challenge,
    
    34
    -      solution,
    
    35
    -      false
    
    36
    -    );
    
    37
    -    this._bridges = response?.bridges;
    
    41
    +    const abortController = new AbortController();
    
    42
    +    this._moatAbortControllers.add(abortController);
    
    43
    +    try {
    
    44
    +      this._bridges = await this._moatRPC.check(
    
    45
    +        "obfs4",
    
    46
    +        this._challenge,
    
    47
    +        solution,
    
    48
    +        abortController.signal
    
    49
    +      );
    
    50
    +    } finally {
    
    51
    +      this._moatAbortControllers.delete(abortController);
    
    52
    +    }
    
    38 53
         return this._bridges;
    
    39 54
       },
    
    40 55
     
    
    ... ... @@ -45,10 +60,20 @@ export var BridgeDB = {
    45 60
             await this._moatRPC.init();
    
    46 61
           }
    
    47 62
     
    
    48
    -      const response = await this._moatRPC.fetch(["obfs4"]);
    
    49
    -      this._challenge = response.challenge;
    
    50
    -      this._image =
    
    51
    -        "data:image/jpeg;base64," + encodeURIComponent(response.image);
    
    63
    +      const abortController = new AbortController();
    
    64
    +      this._moatAbortControllers.add(abortController);
    
    65
    +      let response;
    
    66
    +      try {
    
    67
    +        response = await this._moatRPC.fetch(["obfs4"], abortController.signal);
    
    68
    +      } finally {
    
    69
    +        this._moatAbortControllers.delete(abortController);
    
    70
    +      }
    
    71
    +      if (response) {
    
    72
    +        // Not cancelled.
    
    73
    +        this._challenge = response.challenge;
    
    74
    +        this._image =
    
    75
    +          "data:image/jpeg;base64," + encodeURIComponent(response.image);
    
    76
    +      }
    
    52 77
         } catch (err) {
    
    53 78
           console.error("Could not request a captcha image", err);
    
    54 79
         }
    
    ... ... @@ -56,6 +81,11 @@ export var BridgeDB = {
    56 81
       },
    
    57 82
     
    
    58 83
       close() {
    
    84
    +    // Abort any ongoing requests.
    
    85
    +    for (const controller of this._moatAbortControllers) {
    
    86
    +      controller.abort();
    
    87
    +    }
    
    88
    +    this._moatAbortControllers.clear();
    
    59 89
         this._moatRPC?.uninit();
    
    60 90
         this._moatRPC = null;
    
    61 91
         this._challenge = null;
    

  • toolkit/modules/DomainFrontedRequests.sys.mjs
    ... ... @@ -377,6 +377,15 @@ export class DomainFrontRequestResponseError extends Error {
    377 377
       }
    
    378 378
     }
    
    379 379
     
    
    380
    +/**
    
    381
    + * Thrown when the caller cancels the request.
    
    382
    + */
    
    383
    +export class DomainFrontRequestCancelledError extends Error {
    
    384
    +  constructor(url) {
    
    385
    +    super(`Cancelled request to ${url}`);
    
    386
    +  }
    
    387
    +}
    
    388
    +
    
    380 389
     /**
    
    381 390
      * Callback object to promisify the XPCOM request.
    
    382 391
      */
    
    ... ... @@ -397,8 +406,12 @@ class ResponseListener {
    397 406
         });
    
    398 407
       }
    
    399 408
     
    
    400
    -  // callers wait on this for final response
    
    401
    -  response() {
    
    409
    +  /**
    
    410
    +   * A promise that resolves to the response body from the request.
    
    411
    +   *
    
    412
    +   * @type {Promise<string>}
    
    413
    +   */
    
    414
    +  get response() {
    
    402 415
         return this.#responsePromise;
    
    403 416
       }
    
    404 417
     
    
    ... ... @@ -530,24 +543,74 @@ export class DomainFrontRequestBuilder {
    530 543
        * @param {string} args.method The request method.
    
    531 544
        * @param {string} args.body The request body.
    
    532 545
        * @param {string} args.contentType The "Content-Type" header to set.
    
    533
    -   * @returns {string} The response body.
    
    546
    +   * @param {AbortSignal} [signal] args.signal An optional means of cancelling
    
    547
    +   *   the request early. Will throw DomainFrontRequestCancelledError if
    
    548
    +   *   aborted.
    
    549
    +   * @returns {Promise<string>} A promise that resolves to the response body.
    
    534 550
        */
    
    535
    -  async buildRequest(url, args) {
    
    536
    -    const ch = this.buildHttpHandler(url);
    
    537
    -
    
    538
    -    const inStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
    
    539
    -      Ci.nsIStringInputStream
    
    540
    -    );
    
    541
    -    inStream.setData(args.body, args.body.length);
    
    542
    -    const upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
    
    543
    -    upChannel.setUploadStream(inStream, args.contentType, args.body.length);
    
    544
    -    ch.requestMethod = args.method;
    
    551
    +  buildRequest(url, args) {
    
    552
    +    // Pre-fetch the argument values from `args` so the caller cannot change the
    
    553
    +    // parameters mid-call.
    
    554
    +    const { body, method, contentType, signal } = args;
    
    555
    +    let cancel = null;
    
    556
    +    const promise = new Promise((resolve, reject) => {
    
    557
    +      if (signal?.aborted) {
    
    558
    +        // Unexpected, cancel immediately.
    
    559
    +        reject(new DomainFrontRequestCancelledError(url));
    
    560
    +        return;
    
    561
    +      }
    
    545 562
     
    
    546
    -    // Make request
    
    547
    -    const listener = new ResponseListener();
    
    548
    -    await ch.asyncOpen(listener, ch);
    
    563
    +      let ch = null;
    
    564
    +
    
    565
    +      if (signal) {
    
    566
    +        cancel = () => {
    
    567
    +          // Reject prior to calling cancel, since we want to ignore any error
    
    568
    +          // responses from ResponseListener.
    
    569
    +          // NOTE: In principle we could let ResponseListener throw this error
    
    570
    +          // when it receives NS_ERROR_ABORT, but that would rely on mozilla
    
    571
    +          // never calling this error either.
    
    572
    +          reject(new DomainFrontRequestCancelledError(url));
    
    573
    +          ch?.cancel(Cr.NS_ERROR_ABORT);
    
    574
    +        };
    
    575
    +        signal.addEventListener("abort", cancel);
    
    576
    +      }
    
    549 577
     
    
    550
    -    // wait for response
    
    551
    -    return listener.response();
    
    578
    +      ch = this.buildHttpHandler(url);
    
    579
    +
    
    580
    +      const inStream = Cc[
    
    581
    +        "@mozilla.org/io/string-input-stream;1"
    
    582
    +      ].createInstance(Ci.nsIStringInputStream);
    
    583
    +      inStream.setData(body, body.length);
    
    584
    +      const upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
    
    585
    +      upChannel.setUploadStream(inStream, contentType, body.length);
    
    586
    +      ch.requestMethod = method;
    
    587
    +
    
    588
    +      // Make request
    
    589
    +      const listener = new ResponseListener();
    
    590
    +      ch.asyncOpen(listener);
    
    591
    +      listener.response.then(
    
    592
    +        body => {
    
    593
    +          resolve(body);
    
    594
    +        },
    
    595
    +        error => {
    
    596
    +          reject(error);
    
    597
    +        }
    
    598
    +      );
    
    599
    +    });
    
    600
    +    // Clean up. Do not return this `Promise.finally` since the caller should
    
    601
    +    // not depend on it.
    
    602
    +    // We pre-catch and suppress all errors for this `.finally` to stop the
    
    603
    +    // errors from being duplicated in the console log.
    
    604
    +    promise
    
    605
    +      .catch(() => {})
    
    606
    +      .finally(() => {
    
    607
    +        // Remove the callback for the AbortSignal so that it doesn't hold onto
    
    608
    +        // our channel reference if the caller continues to hold a reference to
    
    609
    +        // AbortSignal.
    
    610
    +        if (cancel) {
    
    611
    +          signal.removeEventListener("abort", cancel);
    
    612
    +        }
    
    613
    +      });
    
    614
    +    return promise;
    
    552 615
       }
    
    553 616
     }

  • toolkit/modules/Moat.sys.mjs
    ... ... @@ -5,13 +5,15 @@
    5 5
     const lazy = {};
    
    6 6
     
    
    7 7
     const log = console.createInstance({
    
    8
    -  maxLogLevel: "Warn",
    
    9 8
       prefix: "Moat",
    
    9
    +  maxLogLevelPref: "browser.torMoat.loglevel",
    
    10 10
     });
    
    11 11
     
    
    12 12
     ChromeUtils.defineESModuleGetters(lazy, {
    
    13 13
       DomainFrontRequestBuilder:
    
    14 14
         "resource://gre/modules/DomainFrontedRequests.sys.mjs",
    
    15
    +  DomainFrontRequestCancelledError:
    
    16
    +    "resource://gre/modules/DomainFrontedRequests.sys.mjs",
    
    15 17
       TorBridgeSource: "resource://gre/modules/TorSettings.sys.mjs",
    
    16 18
     });
    
    17 19
     
    
    ... ... @@ -91,6 +93,19 @@ class InternetTestResponseListener {
    91 93
      * @property {string} [country] - The detected country (region).
    
    92 94
      */
    
    93 95
     
    
    96
    +/**
    
    97
    + * @typedef {Object} CaptchaChallenge
    
    98
    + *
    
    99
    + * The details for a captcha challenge.
    
    100
    + *
    
    101
    + * @property {string} transport - The transport type selected by the Moat
    
    102
    + *   server.
    
    103
    + * @property {string} image - A base64 encoded jpeg with the captcha to
    
    104
    + *   complete.
    
    105
    + * @property {string} challenge - A nonce/cookie string associated with this
    
    106
    + * request.
    
    107
    + */
    
    108
    +
    
    94 109
     /**
    
    95 110
      * Constructs JSON objects and sends requests over Moat.
    
    96 111
      * The documentation about the JSON schemas to use are available at
    
    ... ... @@ -122,17 +137,51 @@ export class MoatRPC {
    122 137
         this.#requestBuilder = null;
    
    123 138
       }
    
    124 139
     
    
    125
    -  async #makeRequest(procedure, args) {
    
    140
    +  /**
    
    141
    +   * @typedef {Object} MoatResult
    
    142
    +   *
    
    143
    +   * The result of a Moat request.
    
    144
    +   *
    
    145
    +   * @property {any} response - The parsed JSON response from the Moat server,
    
    146
    +   *   or `undefined` if the request was cancelled.
    
    147
    +   * @property {boolean} cancelled - Whether the request was cancelled.
    
    148
    +   */
    
    149
    +
    
    150
    +  /**
    
    151
    +   * Make a request to Moat.
    
    152
    +   *
    
    153
    +   * @param {string} procedure - The name of the procedure.
    
    154
    +   * @param {object} args - The arguments to pass in as a JSON string.
    
    155
    +   * @param {AbortSignal} [abortSignal] - An optional signal to be able to abort
    
    156
    +   * the request early.
    
    157
    +   * @returns {MoatResult} - The result of the request.
    
    158
    +   */
    
    159
    +  async #makeRequest(procedure, args, abortSignal) {
    
    126 160
         const procedureURIString = `${Services.prefs.getStringPref(
    
    127 161
           TorLauncherPrefs.moat_service
    
    128 162
         )}/${procedure}`;
    
    129
    -    return JSON.parse(
    
    130
    -      await this.#requestBuilder.buildRequest(procedureURIString, {
    
    131
    -        method: "POST",
    
    132
    -        contentType: "application/vnd.api+json",
    
    133
    -        body: JSON.stringify(args),
    
    134
    -      })
    
    135
    -    );
    
    163
    +    log.info(`Making request to ${procedureURIString}:`, args);
    
    164
    +    let response = undefined;
    
    165
    +    let cancelled = false;
    
    166
    +    try {
    
    167
    +      response = JSON.parse(
    
    168
    +        await this.#requestBuilder.buildRequest(procedureURIString, {
    
    169
    +          method: "POST",
    
    170
    +          contentType: "application/vnd.api+json",
    
    171
    +          body: JSON.stringify(args),
    
    172
    +          signal: abortSignal,
    
    173
    +        })
    
    174
    +      );
    
    175
    +      log.info(`Response to ${procedureURIString}:`, response);
    
    176
    +    } catch (e) {
    
    177
    +      if (abortSignal && e instanceof lazy.DomainFrontRequestCancelledError) {
    
    178
    +        log.info(`Request to ${procedureURIString} cancelled`);
    
    179
    +        cancelled = true;
    
    180
    +      } else {
    
    181
    +        throw e;
    
    182
    +      }
    
    183
    +    }
    
    184
    +    return { response, cancelled };
    
    136 185
       }
    
    137 186
     
    
    138 187
       async testInternetConnection() {
    
    ... ... @@ -147,15 +196,16 @@ export class MoatRPC {
    147 196
         return listener.status;
    
    148 197
       }
    
    149 198
     
    
    150
    -  // Receive a CAPTCHA challenge, takes the following parameters:
    
    151
    -  // - transports: array of transport strings available to us eg: ["obfs4", "meek"]
    
    152
    -  //
    
    153
    -  // returns an object with the following fields:
    
    154
    -  // - transport: a transport string the moat server decides it will send you selected
    
    155
    -  //   from the list of provided transports
    
    156
    -  // - image: a base64 encoded jpeg with the captcha to complete
    
    157
    -  // - challenge: a nonce/cookie string associated with this request
    
    158
    -  async fetch(transports) {
    
    199
    +  /**
    
    200
    +   * Request a CAPTCHA challenge.
    
    201
    +   *
    
    202
    +   * @param {string[]} transports - List of transport strings available to us
    
    203
    +   *   eg: ["obfs4", "meek"].
    
    204
    +   * @param {AbortSignal} abortSignal - A signal to abort the request early.
    
    205
    +   * @returns {?CaptchaChallenge} - The captcha challenge, or `null` if the
    
    206
    +   *   request was aborted by the caller.
    
    207
    +   */
    
    208
    +  async fetch(transports, abortSignal) {
    
    159 209
         if (
    
    160 210
           // ensure this is an array
    
    161 211
           Array.isArray(transports) &&
    
    ... ... @@ -173,7 +223,15 @@ export class MoatRPC {
    173 223
               },
    
    174 224
             ],
    
    175 225
           };
    
    176
    -      const response = await this.#makeRequest("fetch", args);
    
    226
    +      const { response, cancelled } = await this.#makeRequest(
    
    227
    +        "fetch",
    
    228
    +        args,
    
    229
    +        abortSignal
    
    230
    +      );
    
    231
    +      if (cancelled) {
    
    232
    +        return null;
    
    233
    +      }
    
    234
    +
    
    177 235
           if ("errors" in response) {
    
    178 236
             const code = response.errors[0].code;
    
    179 237
             const detail = response.errors[0].detail;
    
    ... ... @@ -189,18 +247,17 @@ export class MoatRPC {
    189 247
         throw new Error("MoatRPC: fetch() expects a non-empty array of strings");
    
    190 248
       }
    
    191 249
     
    
    192
    -  // Submit an answer for a CAPTCHA challenge and get back bridges, takes the following
    
    193
    -  // parameters:
    
    194
    -  // - transport: the transport string associated with a previous fetch request
    
    195
    -  // - challenge: the nonce string associated with the fetch request
    
    196
    -  // - solution: solution to the CAPTCHA associated with the fetch request
    
    197
    -  // - qrcode: true|false whether we want to get back a qrcode containing the bridge strings
    
    198
    -  //
    
    199
    -  // returns an object with the following fields:
    
    200
    -  // - bridges: an array of bridge line strings
    
    201
    -  // - qrcode: base64 encoded jpeg of bridges if requested, otherwise null
    
    202
    -  // if the provided solution is incorrect, returns an empty object
    
    203
    -  async check(transport, challenge, solution, qrcode) {
    
    250
    +  /**
    
    251
    +   * Submit an answer for a previous CAPTCHA fetch to get bridges.
    
    252
    +   *
    
    253
    +   * @param {string} transport - The transport associated with the fetch.
    
    254
    +   * @param {string} challenge - The nonce string associated with the fetch.
    
    255
    +   * @param {string} solution - The solution to the CAPTCHA.
    
    256
    +   * @param {AbortSignal} abortSignal - A signal to abort the request early.
    
    257
    +   * @returns {?string[]} - The bridge lines for a correct solution, or `null`
    
    258
    +   *   if the solution was incorrect or the request was aborted by the caller.
    
    259
    +   */
    
    260
    +  async check(transport, challenge, solution, abortSignal) {
    
    204 261
         const args = {
    
    205 262
           data: [
    
    206 263
             {
    
    ... ... @@ -210,25 +267,30 @@ export class MoatRPC {
    210 267
               transport,
    
    211 268
               challenge,
    
    212 269
               solution,
    
    213
    -          qrcode: qrcode ? "true" : "false",
    
    270
    +          qrcode: "false",
    
    214 271
             },
    
    215 272
           ],
    
    216 273
         };
    
    217
    -    const response = await this.#makeRequest("check", args);
    
    274
    +    const { response, cancelled } = await this.#makeRequest(
    
    275
    +      "check",
    
    276
    +      args,
    
    277
    +      abortSignal
    
    278
    +    );
    
    279
    +    if (cancelled) {
    
    280
    +      return null;
    
    281
    +    }
    
    282
    +
    
    218 283
         if ("errors" in response) {
    
    219 284
           const code = response.errors[0].code;
    
    220 285
           const detail = response.errors[0].detail;
    
    221 286
           if (code == 419 && detail === "The CAPTCHA solution was incorrect.") {
    
    222
    -        return {};
    
    287
    +        return null;
    
    223 288
           }
    
    224 289
     
    
    225 290
           throw new Error(`MoatRPC: ${detail} (${code})`);
    
    226 291
         }
    
    227 292
     
    
    228
    -    const bridges = response.data[0].bridges;
    
    229
    -    const qrcodeImg = qrcode ? response.data[0].qrcode : null;
    
    230
    -
    
    231
    -    return { bridges, qrcode: qrcodeImg };
    
    293
    +    return response.data[0].bridges;
    
    232 294
       }
    
    233 295
     
    
    234 296
       /**
    
    ... ... @@ -296,15 +358,24 @@ export class MoatRPC {
    296 358
        * @param {?string} country - The region to request bridges for, as an
    
    297 359
        *   ISO 3166-1 alpha-2 region code, or `null` to have the server
    
    298 360
        *   automatically determine the region.
    
    361
    +   * @param {AbortSignal} abortSignal - A signal to abort the request early.
    
    299 362
        * @returns {?MoatSettings} - The returned settings from the server, or `null`
    
    300
    -   *   if the region could not be determined by the server.
    
    363
    +   *   if the region could not be determined by the server or the caller
    
    364
    +   *   cancelled the request.
    
    301 365
        */
    
    302
    -  async circumvention_settings(transports, country) {
    
    366
    +  async circumvention_settings(transports, country, abortSignal) {
    
    303 367
         const args = {
    
    304 368
           transports: transports ? transports : [],
    
    305 369
           country,
    
    306 370
         };
    
    307
    -    const response = await this.#makeRequest("circumvention/settings", args);
    
    371
    +    const { response, cancelled } = await this.#makeRequest(
    
    372
    +      "circumvention/settings",
    
    373
    +      args,
    
    374
    +      abortSignal
    
    375
    +    );
    
    376
    +    if (cancelled) {
    
    377
    +      return null;
    
    378
    +    }
    
    308 379
         let settings = {};
    
    309 380
         if ("errors" in response) {
    
    310 381
           const code = response.errors[0].code;
    
    ... ... @@ -334,7 +405,11 @@ export class MoatRPC {
    334 405
       // settings for.
    
    335 406
       async circumvention_countries() {
    
    336 407
         const args = {};
    
    337
    -    return this.#makeRequest("circumvention/countries", args);
    
    408
    +    const { response } = await this.#makeRequest(
    
    409
    +      "circumvention/countries",
    
    410
    +      args
    
    411
    +    );
    
    412
    +    return response;
    
    338 413
       }
    
    339 414
     
    
    340 415
       // Request a copy of the builtin bridges, takes the following parameters:
    
    ... ... @@ -347,7 +422,7 @@ export class MoatRPC {
    347 422
         const args = {
    
    348 423
           transports: transports ? transports : [],
    
    349 424
         };
    
    350
    -    const response = await this.#makeRequest("circumvention/builtin", args);
    
    425
    +    const { response } = await this.#makeRequest("circumvention/builtin", args);
    
    351 426
         if ("errors" in response) {
    
    352 427
           const code = response.errors[0].code;
    
    353 428
           const detail = response.errors[0].detail;
    
    ... ... @@ -366,13 +441,22 @@ export class MoatRPC {
    366 441
        * Request a copy of the default/fallback bridge settings.
    
    367 442
        *
    
    368 443
        * @param {string[]} transports - A list of transports we support.
    
    369
    -   * @returns {MoatBridges[]} - The list of bridges found.
    
    444
    +   * @param {AbortSignal} abortSignal - A signal to abort the request early.
    
    445
    +   * @returns {?MoatBridges[]} - The list of bridges found, or `null` if the
    
    446
    +   *   caller cancelled the request.
    
    370 447
        */
    
    371
    -  async circumvention_defaults(transports) {
    
    448
    +  async circumvention_defaults(transports, abortSignal) {
    
    372 449
         const args = {
    
    373 450
           transports: transports ? transports : [],
    
    374 451
         };
    
    375
    -    const response = await this.#makeRequest("circumvention/defaults", args);
    
    452
    +    const { response, cancelled } = await this.#makeRequest(
    
    453
    +      "circumvention/defaults",
    
    454
    +      args,
    
    455
    +      abortSignal
    
    456
    +    );
    
    457
    +    if (cancelled) {
    
    458
    +      return null;
    
    459
    +    }
    
    376 460
         if ("errors" in response) {
    
    377 461
           const code = response.errors[0].code;
    
    378 462
           const detail = response.errors[0].detail;
    

  • toolkit/modules/TorConnect.sys.mjs
    ... ... @@ -570,6 +570,7 @@ class AutoBootstrapAttempt {
    570 570
             return;
    
    571 571
           }
    
    572 572
     
    
    573
    +      let moatAbortController = new AbortController();
    
    573 574
           // For now, throw any errors we receive from the backend, except when it
    
    574 575
           // was unable to detect user's country/region.
    
    575 576
           // If we use specialized error objects, we could pass the original errors
    
    ... ... @@ -577,11 +578,20 @@ class AutoBootstrapAttempt {
    577 578
           const maybeSettings = await Promise.race([
    
    578 579
             moat.circumvention_settings(
    
    579 580
               [...lazy.TorSettings.builtinBridgeTypes, "vanilla"],
    
    580
    -          options.regionCode === "automatic" ? null : options.regionCode
    
    581
    +          options.regionCode === "automatic" ? null : options.regionCode,
    
    582
    +          moatAbortController.signal
    
    581 583
             ),
    
    582 584
             // This might set maybeSettings to undefined.
    
    583 585
             this.#cancelledPromise,
    
    584 586
           ]);
    
    587
    +      if (this.#cancelled) {
    
    588
    +        // Ended early due to being cancelled. Abort the ongoing Moat request so
    
    589
    +        // that it does not continue unnecessarily in the background.
    
    590
    +        // NOTE: We do not care about circumvention_settings return value or
    
    591
    +        // errors at this point. Nor do we need to await its return. We just
    
    592
    +        // want it to resolve quickly.
    
    593
    +        moatAbortController.abort();
    
    594
    +      }
    
    585 595
           if (this.#cancelled || this.#resolved) {
    
    586 596
             return;
    
    587 597
           }
    
    ... ... @@ -591,15 +601,25 @@ class AutoBootstrapAttempt {
    591 601
           if (maybeSettings?.bridgesList?.length) {
    
    592 602
             this.#bridgesList = maybeSettings.bridgesList;
    
    593 603
           } else {
    
    604
    +        // In principle we could reuse the existing moatAbortController
    
    605
    +        // instance, since its abort method has not been called. But it is
    
    606
    +        // cleaner to use a new instance to avoid triggering any potential
    
    607
    +        // lingering callbacks attached to the AbortSignal.
    
    608
    +        moatAbortController = new AbortController();
    
    594 609
             // Keep consistency with the other call.
    
    595 610
             this.#bridgesList = await Promise.race([
    
    596
    -          moat.circumvention_defaults([
    
    597
    -            ...lazy.TorSettings.builtinBridgeTypes,
    
    598
    -            "vanilla",
    
    599
    -          ]),
    
    611
    +          moat.circumvention_defaults(
    
    612
    +            [...lazy.TorSettings.builtinBridgeTypes, "vanilla"],
    
    613
    +            moatAbortController.signal
    
    614
    +          ),
    
    600 615
               // This might set this.#bridgesList to undefined.
    
    601 616
               this.#cancelledPromise,
    
    602 617
             ]);
    
    618
    +        if (this.#cancelled) {
    
    619
    +          // Ended early due to being cancelled. Abort the ongoing Moat request
    
    620
    +          // so that it does not continue in the background.
    
    621
    +          moatAbortController.abort();
    
    622
    +        }
    
    603 623
           }
    
    604 624
         } finally {
    
    605 625
           // Do not await the uninit.