[tor-commits] [Git][tpo/applications/tor-browser][tor-browser-115.9.0esr-13.5-1] 17 commits: fixup! Lox integration
Pier Angelo Vendrame (@pierov)
git at gitlab.torproject.org
Tue Apr 9 16:49:28 UTC 2024
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/4255b354f2abcfc12e83107ebe19a324cc2430c1...9abd099f1d0bb0889c49e380fb3e2f427df59e23
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/4255b354f2abcfc12e83107ebe19a324cc2430c1...9abd099f1d0bb0889c49e380fb3e2f427df59e23
You're receiving this email because of your account on gitlab.torproject.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.torproject.org/pipermail/tor-commits/attachments/20240409/f7bb61c3/attachment-0001.htm>
More information about the tor-commits
mailing list