Pier Angelo Vendrame pushed to branch tor-browser-115.9.0esr-13.5-1 at The Tor Project / Applications / Tor Browser
Commits: a2c8eab3 by Henry Wilkes at 2024-04-09T13:35:51+01:00 fixup! Lox integration
Bug 42489: Add a logger for the Lox module.
- - - - - 21115918 by Henry Wilkes at 2024-04-09T13:36:00+01:00 fixup! Lox integration
Bug 42489: Change "loxid" to "loxId".
This follows the camel case convention.
- - - - - 64fd04bf by Henry Wilkes at 2024-04-09T13:36:01+01:00 fixup! Lox integration
Bug 42489: Require a loxId argument for most methods.
This ensures that a method only works on the credentials of the *expected* loxId, rather than the newest `TorSettings.bridges.lox_id` value which may change during a session.
We also add `#activeLoxId` to stay in sync with `TorSettings.brigdes.lox_id`. We merge `clearInvites` into its updater.
- - - - - 2411aa86 by Henry Wilkes at 2024-04-09T13:36:01+01:00 fixup! Lox integration
Bug 42489: Make loading the stored preferences unconditional on the #credentials value.
Also change the type of `#events` to always be an Array.
- - - - - 2b7376cd by Henry Wilkes at 2024-04-09T13:36:03+01:00 fixup! Lox integration
Bug 42489: Generated loxId is unlikely to clash with existing ones, but we add some free logic to guarantee this.
- - - - - 4a114a5d by Henry Wilkes at 2024-04-09T13:36:09+01:00 fixup! Lox integration
Bug 42489: Return copies of Lox module internals to ensure they cannot be edited by a caller.
- - - - - e7248fe1 by Henry Wilkes at 2024-04-09T13:57:23+01:00 fixup! Lox integration
Bug 42489: Add notifications to the Lox module.
We ensure changes to credentials pass through #changeCredentials to check whether we should send a notification.
- - - - - e8846a94 by Henry Wilkes at 2024-04-09T13:57:29+01:00 fixup! Lox integration
Bug 42489: Add #assertInitialized.
- - - - - b459399f by Henry Wilkes at 2024-04-09T16:17:17+01:00 fixup! Lox integration
Bug 42489: Change LoxError.
Move the LoxErrors types into LoxError. Only set the type if it is specific.
- - - - - 23921826 by Henry Wilkes at 2024-04-09T16:17:25+01:00 fixup! Lox integration
Bug 42489: Change the Lox Authority URL.
- - - - - c9db0992 by Henry Wilkes at 2024-04-09T16:17:26+01:00 fixup! Lox integration
Bug 42489: Try and re fetch the pubKey, encTable and constants if they failed before.
For example, if they fail via a domain front request, we should try again with `fetch` when we are bootstrapped.
- - - - - a9522e4e by Henry Wilkes at 2024-04-09T16:17:26+01:00 fixup! Lox integration
Bug 42489: Make sure trust level strings are converted to integers.
- - - - - 63e0f941 by Henry Wilkes at 2024-04-09T16:22:17+01:00 fixup! Lox integration
Bug 42489: Tidy #attemptUpgrade by returning early with a promise.
- - - - - 40ca47e1 by Henry Wilkes at 2024-04-09T16:22:18+01:00 fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 42489: Listen for notifications from Lox module, and pass in loxId to methods.
- - - - - 23b9aa5b by Henry Wilkes at 2024-04-09T16:22:29+01:00 fixup! Bug 40597: Implement TorSettings module
Bug 42489: Listen for notifications from Lox module.
Also, do not save the bridge_strings to the preferences if they come from the Lox module.
In DomainFrontedRequests distinguish between reachability errors, response errors and other errors in DomainFrontedRequest to improve Lox error messaging.
- - - - - 46ac1528 by Henry Wilkes at 2024-04-09T16:22:53+01:00 fixup! Bug 40933: Add tor-launcher functionality
Bug 42489: Drop getLocalizedStringForError from TorLauncherUtil.
- - - - - 9abd099f by Henry Wilkes at 2024-04-09T16:22:53+01:00 fixup! Add TorStrings module for localization
Bug 42489: Drop nserror strings.
- - - - -
8 changed files:
- browser/components/torpreferences/content/connectionPane.js - browser/components/torpreferences/content/loxInviteDialog.js - browser/components/torpreferences/content/provideBridgeDialog.js - toolkit/components/lox/Lox.sys.mjs - toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs - toolkit/modules/DomainFrontedRequests.sys.mjs - toolkit/modules/TorSettings.sys.mjs - toolkit/torbutton/chrome/locale/en-US/torlauncher.properties
Changes:
===================================== browser/components/torpreferences/content/connectionPane.js ===================================== @@ -36,7 +36,7 @@ const { TorStrings } = ChromeUtils.importESModule( "resource://gre/modules/TorStrings.sys.mjs" );
-const { Lox } = ChromeUtils.importESModule( +const { Lox, LoxTopics } = ChromeUtils.importESModule( "resource://gre/modules/Lox.sys.mjs" );
@@ -1319,27 +1319,18 @@ const gLoxStatus = { this._invitesButton.addEventListener("click", () => { gSubDialog.open( "chrome://browser/content/torpreferences/loxInviteDialog.xhtml", - { - features: "resizable=yes", - closedCallback: () => { - // TODO: Listen for events from Lox, rather than call _updateInvites - // directly. - this._updateInvites(); - }, - } + { features: "resizable=yes" } ); }); this._unlockAlertButton.addEventListener("click", () => { - // TODO: Have a way to ensure that the cleared event data matches the - // current _loxId - Lox.clearEventData(); - // TODO: Listen for events from Lox, rather than call _updateUnlocks - // directly. - this._updateUnlocks(); + Lox.clearEventData(this._loxId); });
Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged); - // TODO: Listen for new events from Lox, when it is supported. + Services.obs.addObserver(this, LoxTopics.UpdateEvents); + Services.obs.addObserver(this, LoxTopics.UpdateNextUnlock); + Services.obs.addObserver(this, LoxTopics.UpdateRemainingInvites); + Services.obs.addObserver(this, LoxTopics.NewInvite);
// NOTE: Before initializedPromise completes, this area is hidden. TorSettings.initializedPromise.then(() => { @@ -1352,6 +1343,10 @@ const gLoxStatus = { */ uninit() { Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged); + Services.obs.removeObserver(this, LoxTopics.UpdateEvents); + Services.obs.removeObserver(this, LoxTopics.UpdateNextUnlock); + Services.obs.removeObserver(this, LoxTopics.UpdateRemainingInvites); + Services.obs.removeObserver(this, LoxTopics.NewInvite); },
observe(subject, topic, data) { @@ -1365,6 +1360,18 @@ const gLoxStatus = { this._updateLoxId(); } break; + case LoxTopics.UpdateNextUnlock: + this._updateNextUnlock(); + break; + case LoxTopics.UpdateEvents: + this._updatePendingEvents(); + break; + case LoxTopics.UpdateRemainingInvites: + this._updateRemainingInvites(); + break; + case LoxTopics.NewInvite: + this._updateHaveExistingInvites(); + break; } },
@@ -1384,43 +1391,126 @@ const gLoxStatus = { TorSettings.bridges.source === TorBridgeSource.Lox ? TorSettings.bridges.lox_id : ""; - if (loxId !== this._loxId) { - this._loxId = loxId; - this._updateUnlocks(); - this._updateInvites(); + if (loxId === this._loxId) { + return; } + this._loxId = loxId; + // We unset _nextUnlock to ensure the areas no longer use the old value for + // the new loxId. + this._updateNextUnlock(true); + this._updateRemainingInvites(); + this._updateHaveExistingInvites(); + this._updatePendingEvents(); },
/** - * Update the display of the current or next unlock. + * The remaining invites shown, or null if uninitialized or no loxId. + * + * @type {integer?} */ - async _updateUnlocks() { - // Cache the loxId before we await. - const loxId = this._loxId; - - if (!loxId) { - // NOTE: This area should already be hidden by the change in Lox source, - // but we clean up for the next non-empty id. - this._area.classList.remove("show-unlock-alert"); - this._area.classList.remove("show-next-unlock"); + _remainingInvites: null, + /** + * Update the shown value. + */ + _updateRemainingInvites() { + const numInvites = this._loxId + ? Lox.getRemainingInviteCount(this._loxId) + : null; + if (numInvites === this._remainingInvites) { return; } - - let pendingEvents; - let nextUnlock; - let numInvites; - // Fetch the latest events or details about the next unlock. - try { - nextUnlock = await Lox.getNextUnlock(); - pendingEvents = Lox.getEventData(); - numInvites = Lox.getRemainingInviteCount(); - } catch (e) { - console.error("Failed get get lox updates", e); + this._remainingInvites = numInvites; + this._updateUnlockArea(); + this._updateInvitesArea(); + }, + /** + * Whether we have existing invites, or null if uninitialized or no loxId. + * + * @type {boolean?} + */ + _haveExistingInvites: null, + /** + * Update the shown value. + */ + _updateHaveExistingInvites() { + const haveInvites = this._loxId ? !!Lox.getInvites().length : null; + if (haveInvites === this._haveExistingInvites) { + return; + } + this._haveExistingInvites = haveInvites; + this._updateInvitesArea(); + }, + /** + * Details about the next unlock, or null if uninitialized or no loxId. + * + * @type {UnlockData?} + */ + _nextUnlock: null, + /** + * Tracker id to ensure that the results from later calls to _updateNextUnlock + * take priority over earlier calls. + * + * @type {integer} + */ + _nextUnlockCallId: 0, + /** + * Update the shown value asynchronously. + * + * @param {boolean} [unset=false] - Whether to set the _nextUnlock value to + * null before waiting for the new value. I.e. ensure that the current value + * will not be used. + */ + async _updateNextUnlock(unset = false) { + // NOTE: We do not expect the integer to exceed the maximum integer. + this._nextUnlockCallId++; + const callId = this._nextUnlockCallId; + if (unset) { + this._nextUnlock = null; + } + const nextUnlock = this._loxId + ? await Lox.getNextUnlock(this._loxId) + : null; + if (callId !== this._nextUnlockCallId) { + // Replaced by another update. + // E.g. if the _loxId changed. Or if getNextUnlock triggered + // LoxTopics.UpdateNextUnlock. return; } + // Should be safe to trigger the update, even when the value hasn't changed. + this._nextUnlock = nextUnlock; + this._updateUnlockArea(); + }, + /** + * The list of events the user has not yet cleared, or null if uninitialized + * or no loxId. + * + * @type {EventData[]?} + */ + _pendingEvents: null, + /** + * Update the shown value. + */ + _updatePendingEvents() { + // Should be safe to trigger the update, even when the value hasn't changed. + this._pendingEvents = this._loxId ? Lox.getEventData(this._loxId) : null; + this._updateUnlockArea(); + },
- if (loxId !== this._loxId) { - // Replaced during await. + /** + * Update the display of the current or next unlock. + */ + _updateUnlockArea() { + if ( + !this._loxId || + this._pendingEvents === null || + this._remainingInvites === null || + this._nextUnlock === null + ) { + // Uninitialized or no Lox source. + // NOTE: This area may already be hidden by the change in Lox source, + // but we clean up for the next non-empty id. + this._area.classList.remove("show-unlock-alert"); + this._area.classList.remove("show-next-unlock"); return; }
@@ -1428,6 +1518,7 @@ const gLoxStatus = { const alertHadFocus = this._unlockAlert.contains(document.activeElement); const detailsHadFocus = this._detailsArea.contains(document.activeElement);
+ const pendingEvents = this._pendingEvents; const showAlert = !!pendingEvents.length; this._area.classList.toggle("show-unlock-alert", showAlert); this._area.classList.toggle("show-next-unlock", !showAlert); @@ -1479,7 +1570,7 @@ const gLoxStatus = { document.l10n.setAttributes( this._unlockAlertInviteItem, "tor-bridges-lox-new-invites", - { numInvites } + { numInvites: this._remainingInvites } ); this._unlockAlert.classList.toggle( "lox-unlock-upgrade", @@ -1494,7 +1585,7 @@ const gLoxStatus = { const numDays = Math.max( 1, Math.ceil( - (new Date(nextUnlock.date).getTime() - Date.now()) / + (new Date(this._nextUnlock.date).getTime() - Date.now()) / (24 * 60 * 60 * 1000) ) ); @@ -1505,9 +1596,9 @@ const gLoxStatus = { );
// Gain 2 bridges from level 0 to 1. After that gain invites. - const bridgeGain = nextUnlock.nextLevel === 1; - const firstInvites = nextUnlock.nextLevel === 2; - const moreInvites = nextUnlock.nextLevel > 2; + const bridgeGain = this._nextUnlock.nextLevel === 1; + const firstInvites = this._nextUnlock.nextLevel === 2; + const moreInvites = this._nextUnlock.nextLevel > 2;
this._detailsArea.classList.toggle("lox-next-gain-bridges", bridgeGain); this._detailsArea.classList.toggle( @@ -1529,24 +1620,19 @@ const gLoxStatus = { /** * Update the invites area. */ - _updateInvites() { - if (!this._loxId) { - return; - } - - let remainingInvites; - let existingInvites; - // Fetch the latest events or details about the next unlock. - try { - remainingInvites = Lox.getRemainingInviteCount(); - existingInvites = Lox.getInvites().length; - } catch (e) { - console.error("Failed get get remaining invites", e); - return; + _updateInvitesArea() { + let hasInvites; + if ( + !this._loxId || + this._remainingInvites === null || + this._haveExistingInvites === null + ) { + // Not initialized yet. + hasInvites = false; + } else { + hasInvites = this._haveExistingInvites || !!this._remainingInvites; }
- const hasInvites = !!existingInvites || !!remainingInvites; - if (!hasInvites) { if ( this._remainingInvitesEl.contains(document.activeElement) || @@ -1563,11 +1649,13 @@ const gLoxStatus = { // creating new ones. this._detailsArea.classList.toggle("lox-has-invites", hasInvites);
- document.l10n.setAttributes( - this._remainingInvitesEl, - "tor-bridges-lox-remaining-invites", - { numInvites: remainingInvites } - ); + if (hasInvites) { + document.l10n.setAttributes( + this._remainingInvitesEl, + "tor-bridges-lox-remaining-invites", + { numInvites: this._remainingInvites } + ); + } }, };
===================================== browser/components/torpreferences/content/loxInviteDialog.js ===================================== @@ -3,14 +3,14 @@ const { TorSettings, TorSettingsTopics, TorBridgeSource } = ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
-const { Lox, LoxErrors } = ChromeUtils.importESModule( +const { Lox, LoxError, LoxTopics } = ChromeUtils.importESModule( "resource://gre/modules/Lox.sys.mjs" );
/** * Fake Lox module
-const LoxErrors = { +const LoxError = { LoxServerUnreachable: "LoxServerUnreachable", Other: "Other", }; @@ -36,7 +36,7 @@ const Lox = { return; } if (!this.remainingInvites) { - rej({ type: LoxErrors.Other }); + rej({ type: LoxError.Other }); return; } const invite = JSON.stringify({ @@ -104,7 +104,8 @@ const gLoxInvites = { // NOTE: TorSettings should already be initialized when this dialog is // opened. Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged); - // TODO: Listen for new invites from Lox, when supported. + Services.obs.addObserver(this, LoxTopics.UpdateRemainingInvites); + Services.obs.addObserver(this, LoxTopics.NewInvite);
// Set initial _loxId value. Can close this dialog. this._updateLoxId(); @@ -118,6 +119,8 @@ const gLoxInvites = { */ uninit() { Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged); + Services.obs.removeObserver(this, LoxTopics.UpdateRemainingInvites); + Services.obs.removeObserver(this, LoxTopics.NewInvite); },
observe(subject, topic, data) { @@ -131,6 +134,12 @@ const gLoxInvites = { this._updateLoxId(); } break; + case LoxTopics.UpdateRemainingInvites: + this._updateRemainingInvites(); + break; + case LoxTopics.NewInvite: + this._updateExistingInvites(); + break; } },
@@ -204,7 +213,7 @@ const gLoxInvites = { * Update the display of the remaining invites. */ _updateRemainingInvites() { - this._remainingInvites = Lox.getRemainingInviteCount(); + this._remainingInvites = Lox.getRemainingInviteCount(this._loxId);
document.l10n.setAttributes( this._remainingInvitesEl, @@ -254,7 +263,7 @@ const gLoxInvites = { this._connectingEl.focus();
let lostFocus = false; - Lox.generateInvite() + Lox.generateInvite(this._loxId) .finally(() => { // Fetch whether the connecting label still has focus before we hide it. lostFocus = this._connectingEl.contains(document.activeElement); @@ -275,15 +284,11 @@ const gLoxInvites = { // message. this._inviteListEl.focus(); } - - // TODO: When Lox sends out notifications, let the observer handle the - // change rather than calling _updateRemainingInvites directly. - this._updateRemainingInvites(); }, loxError => { console.error("Failed to generate an invite", loxError); - switch (loxError.type) { - case LoxErrors.LoxServerUnreachable: + switch (loxError instanceof LoxError ? loxError.code : null) { + case LoxError.LoxServerUnreachable: this._updateGenerateError("no-server"); break; default:
===================================== browser/components/torpreferences/content/provideBridgeDialog.js ===================================== @@ -15,14 +15,14 @@ const { TorParsers } = ChromeUtils.importESModule( "resource://gre/modules/TorParsers.sys.mjs" );
-const { Lox, LoxErrors } = ChromeUtils.importESModule( +const { Lox, LoxError } = ChromeUtils.importESModule( "resource://gre/modules/Lox.sys.mjs" );
/* * Fake Lox module:
-const LoxErrors = { +const LoxError = { BadInvite: "BadInvite", LoxServerUnreachable: "LoxServerUnreachable", Other: "Other", @@ -30,9 +30,9 @@ const LoxErrors = {
const Lox = { failError: null, - // failError: LoxErrors.BadInvite, - // failError: LoxErrors.LoxServerUnreachable, - // failError: LoxErrors.Other, + // failError: LoxError.BadInvite, + // failError: LoxError.LoxServerUnreachable, + // failError: LoxError.Other, redeemInvite(invite) { return new Promise((res, rej) => { setTimeout(() => { @@ -281,13 +281,13 @@ const gProvideBridgeDialog = { }, loxError => { console.error("Redeeming failed", loxError); - switch (loxError.type) { - case LoxErrors.BadInvite: + switch (loxError instanceof LoxError ? loxError.code : null) { + case LoxError.BadInvite: // TODO: distinguish between a bad invite, an invite that has // expired, and an invite that has already been redeemed. this.updateError({ type: "bad-invite" }); break; - case LoxErrors.LoxServerUnreachable: + case LoxError.LoxServerUnreachable: this.updateError({ type: "no-server" }); break; default:
===================================== toolkit/components/lox/Lox.sys.mjs ===================================== @@ -5,15 +5,32 @@ import { } from "resource://gre/modules/Timer.sys.mjs";
const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "logger", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + return new ConsoleAPI({ + maxLogLevel: "warn", + maxLogLevelPref: "lox.log_level", + prefix: "Lox", + }); +}); + ChromeUtils.defineESModuleGetters(lazy, { DomainFrontRequestBuilder: "resource://gre/modules/DomainFrontedRequests.sys.mjs", + DomainFrontRequestNetworkError: + "resource://gre/modules/DomainFrontedRequests.sys.mjs", + DomainFrontRequestResponseError: + "resource://gre/modules/DomainFrontedRequests.sys.mjs", TorConnect: "resource://gre/modules/TorConnect.sys.mjs", TorConnectState: "resource://gre/modules/TorConnect.sys.mjs", TorSettings: "resource://gre/modules/TorSettings.sys.mjs", TorSettingsTopics: "resource://gre/modules/TorSettings.sys.mjs", TorBridgeSource: "resource://gre/modules/TorSettings.sys.mjs", }); + XPCOMUtils.defineLazyModuleGetters(lazy, { init: "resource://gre/modules/lox_wasm.jsm", open_invite: "resource://gre/modules/lox_wasm.jsm", @@ -37,13 +54,22 @@ XPCOMUtils.defineLazyModuleGetters(lazy, { handle_blockage_migration: "resource://gre/modules/lox_wasm.jsm", });
-export const LoxErrors = Object.freeze({ - BadInvite: "BadInvite", - MissingCredential: "MissingCredential", - LoxServerUnreachable: "LoxServerUnreachable", - NoInvitations: "NoInvitations", - InitError: "InitializationError", - NotInitialized: "NotInitialized", +export const LoxTopics = Object.freeze({ + // Whenever the bridges *might* have changed. + // getBridges only uses #credentials, so this will only fire when it changes. + UpdateBridges: "lox:update-bridges", + // Whenever we gain a new upgrade or blockage event, or clear events. + UpdateEvents: "lox:update-events", + // Whenever the next unlock *might* have changed. + // getNextUnlock uses #credentials and #constants, sow ill fire when either + // value changes. + UpdateNextUnlock: "lox:update-next-unlock", + // Whenever the remaining invites *might* have changed. + // getRemainingInviteCount only uses #credentials, so will only fire when it + // changes. + UpdateRemainingInvites: "lox:update-remaining-invites", + // Whenever we generate a new invite. + NewInvite: "lox:new-invite", });
const LoxSettingsPrefs = Object.freeze({ @@ -56,10 +82,21 @@ const LoxSettingsPrefs = Object.freeze({ constants: "lox.settings.constants", });
-class LoxError extends Error { - constructor(type) { - super(""); - this.type = type; +/** + * Error class for Lox. + */ +export class LoxError extends Error { + static BadInvite = "BadInvite"; + static LoxServerUnreachable = "LoxServerUnreachable"; + + /** + * @param {string} message - The error message. + * @param {string?} [code] - The specific error type, if any. + */ + constructor(message, code = null) { + super(message); + this.name = "LoxError"; + this.code = code; } }
@@ -70,14 +107,65 @@ class LoxImpl { #encTablePromise = null; #constantsPromise = null; #domainFrontedRequests = null; - #invites = null; + /** + * The list of invites generated. + * + * @type {string[]} + */ + #invites = []; #pubKeys = null; #encTable = null; #constants = null; - #credentials = null; + /** + * The latest credentials for a given lox id. + * + * @type {Object<string, string>} + */ + #credentials = {}; + /** + * The list of accumulated blockage or upgrade events. + * + * This can be cleared when the user acknowledges the events. + * + * @type {EventData[]} + */ #events = []; #backgroundInterval = null;
+ /** + * The lox ID that is currently active. + * + * Stays in sync with TorSettings.bridges.lox_id. null when uninitialized. + * + * @type {string?} + */ + #activeLoxId = null; + + /** + * Update the active lox id. + */ + #updateActiveLoxId() { + const loxId = lazy.TorSettings.bridges.lox_id; + if (loxId === this.#activeLoxId) { + return; + } + lazy.logger.debug( + `#activeLoxId switching from "${this.#activeLoxId}" to "${loxId}"` + ); + if (this.#activeLoxId !== null) { + lazy.logger.debug( + `Clearing event data and invites for "${this.#activeLoxId}"` + ); + // If not initializing clear the metadata for the old lox ID when it + // changes. + this.clearEventData(this.#activeLoxId); + // TODO: Do we want to keep invites? See tor-browser#42453 + this.#invites = []; + this.#store(); + } + this.#activeLoxId = loxId; + } + observe(subject, topic, data) { switch (topic) { case lazy.TorSettingsTopics.SettingsChanged: @@ -87,11 +175,8 @@ class LoxImpl { changes.includes("bridges.source") || changes.includes("bridges.lox_id") ) { - // if lox_id has changed, clear event and invite queues - if (changes.includes("bridges.lox_id")) { - this.clearEventData(); - this.clearInvites(); - } + // The lox_id may have changed. + this.#updateActiveLoxId();
// Only run background tasks if Lox is enabled if (this.#inuse) { @@ -108,6 +193,8 @@ class LoxImpl { } break; case lazy.TorSettingsTopics.Ready: + // Set the initial #activeLoxId. + this.#updateActiveLoxId(); // Run background tasks every 12 hours if Lox is enabled if (this.#inuse) { this.#backgroundInterval = setInterval( @@ -119,36 +206,86 @@ class LoxImpl { } }
+ /** + * Assert that the module is initialized. + */ + #assertInitialized() { + if (!this.#initialized) { + throw new LoxError("Not initialized"); + } + } + get #inuse() { return ( + Boolean(this.#activeLoxId) && lazy.TorSettings.bridges.enabled === true && - lazy.TorSettings.bridges.source === lazy.TorBridgeSource.Lox && - lazy.TorSettings.bridges.lox_id + lazy.TorSettings.bridges.source === lazy.TorBridgeSource.Lox ); }
+ /** + * Change some existing credentials for an ID to a new value. + * + * @param {string} loxId - The ID to change the credentials for. + * @param {string} newCredentials - The new credentials to set. + */ + #changeCredentials(loxId, newCredentials) { + // FIXME: Several async methods want to update the credentials, but they + // might race and conflict with each. tor-browser#42492 + if (!newCredentials) { + // Avoid overwriting and losing our current credentials. + throw new LoxError(`Empty credentials being set for ${loxId}`); + } + if (!this.#credentials[loxId]) { + // Unexpected, but we still want to save the value to storage. + lazy.logger.warn(`Lox ID ${loxId} is missing existing credentials`); + } + + this.#credentials[loxId] = newCredentials; + this.#store(); + + // NOTE: In principle we could determine within this module whether the + // bridges, remaining invites, or next unlock changes in value when + // switching credentials. + // However, this logic can be done by the topic observers, as needed. In + // particular, TorSettings.bridges.bridge_strings has its own logic + // determining whether its value has changed. + + // Let TorSettings know about possibly new bridges. + Services.obs.notifyObservers(null, LoxTopics.UpdateBridges); + // Let UI know about changes. + Services.obs.notifyObservers(null, LoxTopics.UpdateRemainingInvites); + Services.obs.notifyObservers(null, LoxTopics.UpdateNextUnlock); + } + + /** + * Fetch the latest credentials. + * + * @param {string} loxId - The ID to get the credentials for. + * + * @returns {string} - The credentials. + */ + #getCredentials(loxId) { + const cred = loxId ? this.#credentials[loxId] : undefined; + if (!cred) { + throw new LoxError(`No credentials for ${loxId}`); + } + return cred; + } + /** * Formats and returns bridges from the stored Lox credential. * - * @param {string} loxid The id string associated with a lox credential. + * @param {string} loxId The id string associated with a lox credential. * * @returns {string[]} An array of formatted bridge lines. The array is empty * if there are no bridges. */ - getBridges(loxid) { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } - if (loxid === null) { - return []; - } - if (!this.#credentials[loxid]) { - // This lox id doesn't correspond to a stored credential - throw new LoxError(LoxErrors.MissingCredential); - } + getBridges(loxId) { + this.#assertInitialized(); // Note: this is messy now but can be mostly removed after we have // https://gitlab.torproject.org/tpo/anti-censorship/lox/-/issues/46 - let bridgelines = JSON.parse(this.#credentials[loxid]).bridgelines; + let bridgelines = JSON.parse(this.#getCredentials(loxId)).bridgelines; let bridges = []; for (const bridge of bridgelines) { let addr = bridge.addr; @@ -219,18 +356,12 @@ class LoxImpl { }
#load() { - if (this.#credentials === null) { - let cred = Services.prefs.getStringPref(LoxSettingsPrefs.credentials, ""); - this.#credentials = cred !== "" ? JSON.parse(cred) : {}; - let invites = Services.prefs.getStringPref(LoxSettingsPrefs.invites, ""); - if (invites !== "") { - this.#invites = JSON.parse(invites); - } - let events = Services.prefs.getStringPref(LoxSettingsPrefs.events, ""); - if (events !== "") { - this.#events = JSON.parse(events); - } - } + const cred = Services.prefs.getStringPref(LoxSettingsPrefs.credentials, ""); + this.#credentials = cred ? JSON.parse(cred) : {}; + const invites = Services.prefs.getStringPref(LoxSettingsPrefs.invites, ""); + this.#invites = invites ? JSON.parse(invites) : []; + const events = Services.prefs.getStringPref(LoxSettingsPrefs.events, ""); + this.#events = events ? JSON.parse(events) : []; this.#pubKeys = Services.prefs.getStringPref( LoxSettingsPrefs.pubkeys, null @@ -246,16 +377,21 @@ class LoxImpl { }
async #getPubKeys() { + // FIXME: We are always refetching #pubKeys, #encTable and #constants once + // per session, but they may change more frequently. tor-browser#42502 if (this.#pubKeyPromise === null) { this.#pubKeyPromise = this.#makeRequest("pubkeys", []) .then(pubKeys => { this.#pubKeys = JSON.stringify(pubKeys); this.#store(); }) - .catch(() => { + .catch(error => { + lazy.logger.debug("Failed to get pubkeys", error); + // Make the next call try again. + this.#pubKeyPromise = null; // We always try to update, but if that doesn't work fall back to stored data if (!this.#pubKeys) { - throw new LoxError(LoxErrors.LoxServerUnreachable); + throw error; } }); } @@ -269,10 +405,13 @@ class LoxImpl { this.#encTable = JSON.stringify(encTable); this.#store(); }) - .catch(() => { + .catch(error => { + lazy.logger.debug("Failed to get encTable", error); + // Make the next call try again. + this.#encTablePromise = null; // Try to update first, but if that doesn't work fall back to stored data if (!this.#encTable) { - throw new LoxError(LoxErrors.LoxServerUnreachable); + throw error; } }); } @@ -284,55 +423,96 @@ class LoxImpl { // Try to update first, but if that doesn't work fall back to stored data this.#constantsPromise = this.#makeRequest("constants", []) .then(constants => { + const prevValue = this.#constants; this.#constants = JSON.stringify(constants); this.#store(); + if (prevValue !== this.#constants) { + Services.obs.notifyObservers(null, LoxTopics.UpdateNextUnlock); + } }) - .catch(() => { + .catch(error => { + lazy.logger.debug("Failed to get constants", error); + // Make the next call try again. + this.#constantsPromise = null; if (!this.#constants) { - throw new LoxError(LoxErrors.LoxServerUnreachable); + throw error; } }); } await this.#constantsPromise; }
+ /** + * Parse a decimal string to a non-negative integer. + * + * @param {string} str - The string to parse. + * @returns {integer} - The integer. + */ + static #parseNonNegativeInteger(str) { + if (typeof str !== "string" || !/^[0-9]+$/.test(str)) { + throw new LoxError(`Expected a non-negative decimal integer: "${str}"`); + } + return parseInt(str, 10); + } + + /** + * Get the current lox trust level. + * + * @param {string} loxId - The ID to fetch the level for. + * @returns {integer} - The trust level. + */ + #getLevel(loxId) { + return LoxImpl.#parseNonNegativeInteger( + lazy.get_trust_level(this.#getCredentials(loxId)) + ); + } + /** * Check for blockages and attempt to perform a levelup * * If either blockages or a levelup happened, add an event to the event queue */ async #backgroundTasks() { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); + this.#assertInitialized(); + let addedEvent = false; + // Only run background tasks for the active lox ID. + const loxId = this.#activeLoxId; + if (!loxId) { + lazy.logger.warn("No loxId for the background task"); + return; } - const loxid = lazy.TorSettings.bridges.lox_id; try { - const levelup = await this.#attemptUpgrade(loxid); + const levelup = await this.#attemptUpgrade(loxId); if (levelup) { - const level = lazy.get_trust_level(this.#credentials[loxid]); + const level = this.#getLevel(loxId); const newEvent = { type: "levelup", newlevel: level, }; this.#events.push(newEvent); this.#store(); + addedEvent = true; } } catch (err) { - console.log(err); + lazy.logger.error(err); } try { - const leveldown = await this.#blockageMigration(loxid); + const leveldown = await this.#blockageMigration(loxId); if (leveldown) { - let level = lazy.get_trust_level(this.#credentials[loxid]); + let level = this.#getLevel(loxId); const newEvent = { type: "blockage", newlevel: level, }; this.#events.push(newEvent); this.#store(); + addedEvent = true; } } catch (err) { - console.log(err); + lazy.logger.error(err); + } + if (addedEvent) { + Services.obs.notifyObservers(null, LoxTopics.UpdateEvents); } }
@@ -356,10 +536,8 @@ class LoxImpl { await lazy.init(this.#window); lazy.set_panic_hook(); if (typeof lazy.open_invite !== "function") { - throw new LoxError(LoxErrors.InitError); + throw new LoxError("Initialization failed"); } - this.#invites = []; - this.#events = []; this.#load(); this.#initialized = true; } @@ -376,14 +554,14 @@ class LoxImpl { } this.#initialized = false; this.#window = null; - this.#invites = null; + this.#invites = []; this.#pubKeys = null; this.#encTable = null; this.#constants = null; this.#pubKeyPromise = null; this.#encTablePromise = null; this.#constantsPromise = null; - this.#credentials = null; + this.#credentials = {}; this.#events = []; if (this.#backgroundInterval) { clearInterval(this.#backgroundInterval); @@ -398,13 +576,11 @@ class LoxImpl { * @returns {bool} Whether the value passed in was a Lox invitation. */ validateInvitation(invite) { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } + this.#assertInitialized(); try { lazy.invitation_is_trusted(invite); } catch (err) { - console.log(err); + lazy.logger.error(err); return false; } return true; @@ -413,11 +589,9 @@ class LoxImpl { // Note: This is only here for testing purposes. We're going to be using telegram // to issue open invitations for Lox bridges. async requestOpenInvite() { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } + this.#assertInitialized(); let invite = await this.#makeRequest("invite", []); - console.log(invite); + lazy.logger.debug(invite); return invite; }
@@ -425,36 +599,37 @@ class LoxImpl { * Redeems a Lox invitation to obtain a credential and bridges. * * @param {string} invite A Lox invitation. - * @returns {string} The loxid of the associated credential on success. + * @returns {string} The loxId of the associated credential on success. */ async redeemInvite(invite) { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } + this.#assertInitialized(); await this.#getPubKeys(); let request = await lazy.open_invite(JSON.parse(invite).invite); - let id = this.#genLoxId(); - let response; - try { - response = await this.#makeRequest( - "openreq", - JSON.parse(request).request - ); - } catch { - throw new LoxError(LoxErrors.LoxServerUnreachable); - } - console.log("openreq response: ", response); + let response = await this.#makeRequest( + "openreq", + JSON.parse(request).request + ); + lazy.logger.debug("openreq response: ", response); if (response.hasOwnProperty("error")) { - throw new LoxError(LoxErrors.BadInvite); + throw new LoxError( + `Error response to "openreq": ${response.error}`, + LoxError.BadInvite + ); } let cred = lazy.handle_new_lox_credential( request, JSON.stringify(response), this.#pubKeys ); - this.#credentials[id] = cred; + // Generate an id that is not already in the #credentials map. + let loxId; + do { + loxId = this.#genLoxId(); + } while (Object.hasOwn(this.#credentials, loxId)); + // Set new credentials. + this.#credentials[loxId] = cred; this.#store(); - return id; + return loxId; }
/** @@ -463,10 +638,9 @@ class LoxImpl { * @returns {string[]} A list of all historical invites. */ getInvites() { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } - return this.#invites; + this.#assertInitialized(); + // Return a copy. + return structuredClone(this.#invites); }
/** @@ -477,212 +651,191 @@ class LoxImpl { * - there is no saved Lox credential, or * - the saved credential does not have any invitations available. * + * @param {string} loxId - The ID to generate an invite for. * @returns {string} A valid Lox invitation. */ - async generateInvite() { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } - const loxid = lazy.TorSettings.bridges.lox_id; - if (!loxid || !this.#credentials[loxid]) { - throw new LoxError(LoxErrors.MissingCredential); - } + async generateInvite(loxId) { + this.#assertInitialized(); await this.#getPubKeys(); await this.#getEncTable(); - let level = lazy.get_trust_level(this.#credentials[loxid]); + let level = this.#getLevel(loxId); if (level < 1) { - throw new LoxError(LoxErrors.NoInvitations); + throw new LoxError(`Cannot generate invites at level ${level}`); } let request = lazy.issue_invite( - JSON.stringify(this.#credentials[loxid]), + JSON.stringify(this.#getCredentials(loxId)), this.#encTable, this.#pubKeys ); - let response; - try { - response = await this.#makeRequest( - "issueinvite", - JSON.parse(request).request - ); - } catch { - throw new LoxError(LoxErrors.LoxServerUnreachable); - } + let response = await this.#makeRequest( + "issueinvite", + JSON.parse(request).request + ); if (response.hasOwnProperty("error")) { - console.log(response.error); - throw new LoxError(LoxErrors.NoInvitations); + lazy.logger.error(response.error); + throw new LoxError(`Error response to "issueinvite": ${response.error}`); } else { - this.#credentials[loxid] = response; const invite = lazy.prepare_invite(response); this.#invites.push(invite); // cap length of stored invites if (this.#invites.len > 50) { this.#invites.shift(); } - return invite; + this.#store(); + this.#changeCredentials(loxId, response); + Services.obs.notifyObservers(null, LoxTopics.NewInvite); + // Return a copy. + // Right now invite is just a string, but that might change in the future. + return structuredClone(invite); } }
/** * Get the number of invites that a user has remaining. * + * @param {string} loxId - The ID to check. * @returns {int} The number of invites that can still be generated by a * user's credential. */ - getRemainingInviteCount() { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } - const loxid = lazy.TorSettings.bridges.lox_id; - if (!loxid || !this.#credentials[loxid]) { - throw new LoxError(LoxErrors.MissingCredential); - } - return parseInt(lazy.get_invites_remaining(this.#credentials[loxid])); + getRemainingInviteCount(loxId) { + this.#assertInitialized(); + return LoxImpl.#parseNonNegativeInteger( + lazy.get_invites_remaining(this.#getCredentials(loxId)) + ); }
- async #blockageMigration(loxid) { - if (!loxid || !this.#credentials[loxid]) { - throw new LoxError(LoxErrors.MissingCredential); - } + async #blockageMigration(loxId) { await this.#getPubKeys(); let request; try { - request = lazy.check_blockage(this.#credentials[loxid], this.#pubKeys); + request = lazy.check_blockage(this.#getCredentials(loxId), this.#pubKeys); } catch { - console.log("Not ready for blockage migration"); + lazy.logger.log("Not ready for blockage migration"); return false; } let response = await this.#makeRequest("checkblockage", request); if (response.hasOwnProperty("error")) { - console.log(response.error); - throw new LoxError(LoxErrors.LoxServerUnreachable); + lazy.logger.error(response.error); + throw new LoxError( + `Error response to "checkblockage": ${response.error}` + ); } const migrationCred = lazy.handle_check_blockage( - this.#credentials[loxid], + this.#getCredentials(loxId), JSON.stringify(response) ); request = lazy.blockage_migration( - this.#credentials[loxid], + this.#getCredentials(loxId), migrationCred, this.#pubKeys ); response = await this.#makeRequest("blockagemigration", request); if (response.hasOwnProperty("error")) { - console.log(response.error); - throw new LoxError(LoxErrors.LoxServerUnreachable); + lazy.logger.error(response.error); + throw new LoxError( + `Error response to "blockagemigration": ${response.error}` + ); } const cred = lazy.handle_blockage_migration( - this.#credentials[loxid], + this.#getCredentials(loxId), JSON.stringify(response), this.#pubKeys ); - this.#credentials[loxid] = cred; - this.#store(); + this.#changeCredentials(loxId, cred); return true; }
/** Attempts to upgrade the currently saved Lox credential. * If an upgrade is available, save an event in the event list. * - * @returns {boolean} whether a levelup event occured + * @returns {boolean} Whether a levelup event occurred. */ - async #attemptUpgrade(loxid) { - if (!loxid || !this.#credentials[loxid]) { - throw new LoxError(LoxErrors.MissingCredential); - } + async #attemptUpgrade(loxId) { await this.#getPubKeys(); await this.#getEncTable(); await this.#getConstants(); - let success = false; - let level = lazy.get_trust_level(this.#credentials[loxid]); + let level = this.#getLevel(loxId); if (level < 1) { // attempt trust promotion instead - try { - success = await this.#trustMigration(); - } catch (err) { - console.log(err); - return false; - } - } else { - let request = lazy.level_up( - this.#credentials[loxid], - this.#encTable, - this.#pubKeys - ); - const response = await this.#makeRequest("levelup", request); - if (response.hasOwnProperty("error")) { - console.log(response.error); - throw new LoxError(LoxErrors.LoxServerUnreachable); - } - const cred = lazy.handle_level_up( - request, - JSON.stringify(response), - this.#pubKeys - ); - this.#credentials[loxid] = cred; - return true; + return this.#trustMigration(loxId); + } + let request = lazy.level_up( + this.#getCredentials(loxId), + this.#encTable, + this.#pubKeys + ); + const response = await this.#makeRequest("levelup", request); + if (response.hasOwnProperty("error")) { + lazy.logger.error(response.error); + throw new LoxError(`Error response to "levelup": ${response.error}`); } - return success; + const cred = lazy.handle_level_up( + request, + JSON.stringify(response), + this.#pubKeys + ); + this.#changeCredentials(loxId, cred); + return true; }
/** * Attempt to migrate from an untrusted to a trusted Lox credential * - * @returns {Promise<bool>} A bool value indicated whether the credential - * was successfully migrated. + * @param {string} loxId - The ID to use. + * @returns {boolean} Whether the credential was successfully migrated. */ - async #trustMigration() { - const loxid = lazy.TorSettings.bridges.lox_id; - if (!loxid || !this.#credentials[loxid]) { - throw new LoxError(LoxErrors.MissingCredential); - } + async #trustMigration(loxId) { await this.#getPubKeys(); return new Promise((resolve, reject) => { let request = ""; try { - request = lazy.trust_promotion(this.#credentials[loxid], this.#pubKeys); + request = lazy.trust_promotion( + this.#getCredentials(loxId), + this.#pubKeys + ); } catch (err) { - console.log("Not ready to upgrade"); + lazy.logger.debug("Not ready to upgrade"); resolve(false); } this.#makeRequest("trustpromo", JSON.parse(request).request) .then(response => { if (response.hasOwnProperty("error")) { + lazy.logger.error("Error response from trustpromo", response.error); resolve(false); } - console.log("Got promotion cred"); - console.log(response); - console.log(request); + lazy.logger.debug("Got promotion cred", response, request); let promoCred = lazy.handle_trust_promotion( request, JSON.stringify(response) ); - console.log("Formatted promotion cred"); + lazy.logger.debug("Formatted promotion cred"); request = lazy.trust_migration( - this.#credentials[loxid], + this.#getCredentials(loxId), promoCred, this.#pubKeys ); - console.log("Formatted migration request"); + lazy.logger.debug("Formatted migration request"); this.#makeRequest("trustmig", JSON.parse(request).request) .then(response => { if (response.hasOwnProperty("error")) { + lazy.logger.error( + "Error response from trustmig", + response.error + ); resolve(false); } - console.log("Got new credential"); + lazy.logger.debug("Got new credential"); let cred = lazy.handle_trust_migration(request, response); - this.#credentials[loxid] = cred; - this.#store(); + this.#changeCredentials(loxId, cred); resolve(true); }) .catch(err => { - console.log(err); - console.log("Failed trust migration"); + lazy.logger.error("Failed trust migration", err); resolve(false); }); }) .catch(err => { - console.log(err); - console.log("Failed trust promotion"); + lazy.logger.error("Failed trust promotion", err); resolve(false); }); }); @@ -701,88 +854,105 @@ class LoxImpl { /** * Get a list of accumulated events. * + * @param {string} loxId - The ID to get events for. * @returns {EventData[]} A list of the accumulated, unacknowledged events * associated with a user's credential. */ - getEventData() { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } - const loxid = lazy.TorSettings.bridges.lox_id; - if (!loxid || !this.#credentials[loxid]) { - throw new LoxError(LoxErrors.MissingCredential); + getEventData(loxId) { + this.#assertInitialized(); + if (loxId !== this.#activeLoxId) { + lazy.logger.warn( + `No event data for loxId ${loxId} since it was replaced by ${ + this.#activeLoxId + }` + ); + return []; } - return this.#events; + // Return a copy. + return structuredClone(this.#events); }
/** * Clears accumulated event data. + * + * Should be called whenever the user acknowledges the existing events. + * + * @param {string} loxId - The ID to clear events for. */ - clearEventData() { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); + clearEventData(loxId) { + this.#assertInitialized(); + if (loxId !== this.#activeLoxId) { + lazy.logger.warn( + `Not clearing event data for loxId ${loxId} since it was replaced by ${ + this.#activeLoxId + }` + ); + return; } this.#events = []; this.#store(); - } - - /** - * Clears accumulated invitations. - */ - clearInvites() { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } - this.#invites = []; - this.#store(); + Services.obs.notifyObservers(null, LoxTopics.UpdateEvents); }
/** * @typedef {object} UnlockData * - * @property {string} date - The date-time for the next level up, formatted as YYYY-MM-DDTHH:mm:ssZ. - * @property {integer} nextLevel - The next level. Levels count from 0, so this will be 1 or greater. - * + * @property {string} date - The date-time for the next level up, formatted as + * YYYY-MM-DDTHH:mm:ssZ. + * @property {integer} nextLevel - The next level. Levels count from 0, so + * this will be 1 or greater. */
/** * Get details about the next feature unlock. * + * NOTE: A call to this method may trigger LoxTopics.UpdateNextUnlock. + * + * @param {string} loxId - The ID to get the unlock for. * @returns {UnlockData} - Details about the next unlock. */ - async getNextUnlock() { - if (!this.#initialized) { - throw new LoxError(LoxErrors.NotInitialized); - } - const loxid = lazy.TorSettings.bridges.lox_id; - if (!loxid || !this.#credentials[loxid]) { - throw new LoxError(LoxErrors.MissingCredential); - } + async getNextUnlock(loxId) { + this.#assertInitialized(); await this.#getConstants(); - let nextUnlocks = JSON.parse( - lazy.get_next_unlock(this.#constants, this.#credentials[loxid]) + let nextUnlock = JSON.parse( + lazy.get_next_unlock(this.#constants, this.#getCredentials(loxId)) ); - const level = parseInt(lazy.get_trust_level(this.#credentials[loxid])); - const unlocks = { - date: nextUnlocks.trust_level_unlock_date, + const level = this.#getLevel(loxId); + return { + date: nextUnlock.trust_level_unlock_date, nextLevel: level + 1, }; - return unlocks; }
async #makeRequest(procedure, args) { // TODO: Customize to for Lox - const serviceUrl = "https://rdsys-frontend-01.torproject.org/lox"; + const serviceUrl = "https://lox.torproject.org"; const url = `${serviceUrl}/${procedure}`;
if (lazy.TorConnect.state === lazy.TorConnectState.Bootstrapped) { - const request = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/vnd.api+json", - }, - body: JSON.stringify(args), - }); + let request; + try { + request = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/vnd.api+json", + }, + body: JSON.stringify(args), + }); + } catch (error) { + lazy.logger.debug("fetch fail", url, args, error); + throw new LoxError( + `fetch "${procedure}" from Lox authority failed: ${error?.message}`, + LoxError.LoxServerUnreachable + ); + } + if (!request.ok) { + lazy.logger.debug("fetch response", url, args, request); + // Do not treat as a LoxServerUnreachable type. + throw new LoxError( + `Lox authority responded to "${procedure}" with ${request.status}: ${request.statusText}` + ); + } return request.json(); }
@@ -803,7 +973,26 @@ class LoxImpl { }); } const builder = await this.#domainFrontedRequests; - return builder.buildPostRequest(url, args); + try { + return await builder.buildPostRequest(url, args); + } catch (error) { + lazy.logger.debug("Domain front request fail", url, args, error); + if (error instanceof lazy.DomainFrontRequestNetworkError) { + throw new LoxError( + `Domain front fetch "${procedure}" from Lox authority failed: ${error?.message}`, + LoxError.LoxServerUnreachable + ); + } + if (error instanceof lazy.DomainFrontRequestResponseError) { + // Do not treat as a LoxServerUnreachable type. + throw new LoxError( + `Lox authority responded to domain front "${procedure}" with ${error.status}: ${error.statusText}` + ); + } + throw new LoxError( + `Domain front request for "${procedure}" from Lox authority failed: ${error?.message}` + ); + } } }
===================================== toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs ===================================== @@ -429,20 +429,6 @@ export const TorLauncherUtil = Object.freeze({ return aStringName; },
- getLocalizedStringForError(aNSResult) { - for (let prop in Cr) { - if (Cr[prop] === aNSResult) { - const key = "nsresult." + prop; - const rv = this.getLocalizedString(key); - if (rv !== key) { - return rv; - } - return prop; // As a fallback, return the NS_ERROR... name. - } - } - return undefined; - }, - getLocalizedBootstrapStatus(aStatusObj, aKeyword) { if (!aStatusObj || !aKeyword) { return "";
===================================== toolkit/modules/DomainFrontedRequests.sys.mjs ===================================== @@ -347,6 +347,31 @@ class MeekTransportAndroid { } }
+/** + * Corresponds to a Network error with the request. + */ +export class DomainFrontRequestNetworkError extends Error { + constructor(request, statusCode) { + super(`Error fetching ${request.name}: ${statusCode}`); + this.name = "DomainFrontRequestNetworkError"; + this.statusCode = statusCode; + } +} + +/** + * Corresponds to a non-ok response from the server. + */ +export class DomainFrontRequestResponseError extends Error { + constructor(request) { + super( + `Error response from ${request.name} server: ${request.responseStatus}` + ); + this.name = "DomainFrontRequestResponseError"; + this.status = request.responseStatus; + this.statusText = request.responseStatusText; + } +} + /** * Callback object to promisify the XPCOM request. */ @@ -379,12 +404,11 @@ class ResponseListener { onStopRequest(request, status) { try { if (!Components.isSuccessCode(status)) { - const errorMessage = - lazy.TorLauncherUtil.getLocalizedStringForError(status); - this.#reject(new Error(errorMessage)); + // Assume this is a network error. + this.#reject(new DomainFrontRequestNetworkError(request, status)); } if (request.responseStatus !== 200) { - this.#reject(new Error(request.responseStatusText)); + this.#reject(new DomainFrontRequestResponseError(request)); } } catch (err) { this.#reject(err);
===================================== toolkit/modules/TorSettings.sys.mjs ===================================== @@ -7,6 +7,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs", Lox: "resource://gre/modules/Lox.sys.mjs", + LoxTopics: "resource://gre/modules/Lox.sys.mjs", TorParsers: "resource://gre/modules/TorParsers.sys.mjs", TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", }); @@ -330,7 +331,17 @@ class TorSettingsImpl { if (!val) { return; } - this.bridges.bridge_strings = lazy.Lox.getBridges(val); + let bridgeStrings; + try { + bridgeStrings = lazy.Lox.getBridges(val); + } catch (error) { + addError(`No bridges for lox_id ${val}: ${error?.message}`); + // Set as invalid, which will make the builtin_type "" and set the + // bridge_strings to be empty at the next #cleanupSettings. + this.bridges.source = TorBridgeSource.Invalid; + return; + } + this.bridges.bridge_strings = bridgeStrings; }, }, }); @@ -692,7 +703,7 @@ class TorSettingsImpl { try { await lazy.Lox.init(); } catch (e) { - lazy.logger.error("Could not initialize Lox.", e.type); + lazy.logger.error("Could not initialize Lox.", e); }
if ( @@ -711,6 +722,8 @@ class TorSettingsImpl { } }
+ Services.obs.addObserver(this, lazy.LoxTopics.UpdateBridges); + lazy.logger.info("Ready"); }
@@ -718,9 +731,28 @@ class TorSettingsImpl { * Unload or uninit our settings. */ async uninit() { + Services.obs.removeObserver(this, lazy.LoxTopics.UpdateBridges); await lazy.Lox.uninit(); }
+ observe(subject, topic, data) { + switch (topic) { + case lazy.LoxTopics.UpdateBridges: + if (this.bridges.lox_id) { + // Fetch the newest bridges. + this.bridges.bridge_strings = lazy.Lox.getBridges( + this.bridges.lox_id + ); + // No need to save to prefs since bridge_strings is not stored for Lox + // source. But we do pass on the changes to TorProvider. + // FIXME: This can compete with TorConnect to reach TorProvider. + // tor-browser#42316 + this.applySettings(); + } + break; + } + } + /** * Check whether the object has been successfully initialized, and throw if * it has not. @@ -763,24 +795,32 @@ class TorSettingsImpl { TorSettingsPrefs.bridges.source, TorBridgeSource.Invalid ); - this.bridges.lox_id = Services.prefs.getStringPref( - TorSettingsPrefs.bridges.lox_id, - "" - ); - if (this.bridges.source == TorBridgeSource.BuiltIn) { - this.bridges.builtin_type = Services.prefs.getStringPref( - TorSettingsPrefs.bridges.builtin_type, - "" - ); - } else { - const bridgeBranchPrefs = Services.prefs - .getBranch(TorSettingsPrefs.bridges.bridge_strings) - .getChildList(""); - this.bridges.bridge_strings = Array.from(bridgeBranchPrefs, pref => - Services.prefs.getStringPref( - `${TorSettingsPrefs.bridges.bridge_strings}${pref}` - ) - ); + switch (this.bridges.source) { + case TorBridgeSource.BridgeDB: + case TorBridgeSource.UserProvided: + this.bridges.bridge_strings = Services.prefs + .getBranch(TorSettingsPrefs.bridges.bridge_strings) + .getChildList("") + .map(pref => + Services.prefs.getStringPref( + `${TorSettingsPrefs.bridges.bridge_strings}${pref}` + ) + ); + break; + case TorBridgeSource.BuiltIn: + // bridge_strings is set via builtin_type. + this.bridges.builtin_type = Services.prefs.getStringPref( + TorSettingsPrefs.bridges.builtin_type, + "" + ); + break; + case TorBridgeSource.Lox: + // bridge_strings is set via lox id. + this.bridges.lox_id = Services.prefs.getStringPref( + TorSettingsPrefs.bridges.lox_id, + "" + ); + break; } /* Proxy */ this.proxy.enabled = Services.prefs.getBoolPref( @@ -866,7 +906,10 @@ class TorSettingsImpl { ); }); // write new ones - if (this.bridges.source !== TorBridgeSource.BuiltIn) { + if ( + this.bridges.source !== TorBridgeSource.Lox && + this.bridges.source !== TorBridgeSource.BuiltIn + ) { this.bridges.bridge_strings.forEach((string, index) => { Services.prefs.setStringPref( `${TorSettingsPrefs.bridges.bridge_strings}.${index}`,
===================================== toolkit/torbutton/chrome/locale/en-US/torlauncher.properties ===================================== @@ -55,7 +55,3 @@ torlauncher.bootstrapWarning.timeout=connection timeout torlauncher.bootstrapWarning.noroute=no route to host torlauncher.bootstrapWarning.ioerror=read/write error torlauncher.bootstrapWarning.pt_missing=missing pluggable transport - -torlauncher.nsresult.NS_ERROR_NET_RESET=The connection to the server was lost. -torlauncher.nsresult.NS_ERROR_CONNECTION_REFUSED=Could not connect to the server. -torlauncher.nsresult.NS_ERROR_PROXY_CONNECTION_REFUSED=Could not connect to the proxy.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/4255b35...
tbb-commits@lists.torproject.org