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.
|