Pier Angelo Vendrame pushed to branch tor-browser-128.6.0esr-14.5-1 at The Tor Project / Applications / Tor Browser
Commits:
-
169cebe0
by Henry Wilkes at 2025-01-28T10:06:19+00:00
-
400e4ab1
by Henry Wilkes at 2025-01-28T10:06:20+00:00
6 changed files:
- browser/app/profile/000-tor-browser.js
- browser/components/torpreferences/content/requestBridgeDialog.js
- toolkit/modules/BridgeDB.sys.mjs
- toolkit/modules/DomainFrontedRequests.sys.mjs
- toolkit/modules/Moat.sys.mjs
- toolkit/modules/TorConnect.sys.mjs
Changes:
| ... | ... | @@ -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");
|
| ... | ... | @@ -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(
|
| ... | ... | @@ -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;
|
| ... | ... | @@ -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 | } |
| ... | ... | @@ -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;
|
| ... | ... | @@ -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.
|