Pier Angelo Vendrame pushed to branch tor-browser-128.4.0esr-14.5-1 at The Tor Project / Applications / Tor Browser
Commits:
8fee92cd by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 40597: Implement TorSettings module
Bug 41710: Move bootstrapping attempts into a new class.
We copy the logic from "BootstrappingState" and "AutoBootstrappingState"
into "BootstrappingAttempt" and "AutoBootstrappingAttempt". The main
difference is that we can do the following:
```
bootstrapAttempt = new BootstrapAttempt();
bootstrapResult = await bootstrapAttempt.run();
// ...
bootstrapAttempt.cancel();
```
rather than using the "StateCallback" class, which requires some
complicated state management.
Moreover, "AutoBootstrappingAttempt" will use "BootstrappingAttempt" for
each of its attempts. So the logic for bootstrapping can be kept in one
place.
Some other changes:
1. Censorship simulation will no longer necessarily avoid the Moat
calls, so these can be tested.
2. It would be possible to perform the internet test when
auto-bootstrapping as well.
3. When auto-bootstrapping, if "Bootstrapping" produces an error other
than a "BootstrapError", we can end it early.
4. No longer set TorConnect internals.
5. More fine-grained control over censorship simulation and offline
simulation.
- - - - -
1086febd by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 40597: Implement TorSettings module
Bug 41710: Remove StateCallback.
- - - - -
55d915e5 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 40597: Implement TorSettings module
Bug 41710: Return early from TorConnect.init if not enabled.
- - - - -
802af522 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 40597: Implement TorSettings module
Bug 41710: Replace StateCallback in TorConnect.
Instead of managing the abstract "State" we directly manager the user
"Stage". We provide backward compatibility with the "State" for android
and about:torconnect. Eventually this logic can be dropped from these
endpoints and they can listen for changes in the "Stage" instead.
The behaviour for about:torconnect is mostly the same as before, with
some exceptions:
1. If the user sees the "Offline" state, and starts and cancels the
bootstrap, they should return the to the "Offline" state, rather
than "ConnectToTor".
2. Trying to start a bootstrap via the UI before the settings have
loaded will do nothing.
3. Pressing a breadcrumb whilst bootstrapping will now also cancel the
bootstrap.
- - - - -
cc17defe by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser
Bug 42550 - Switch about:torconnect to use TorConnect.stage to control
the shown stage and sync pages.
Now TorConnect entirely controls which stage should be shown to the
user, and "about:torconnect" simply relays the user actions up to
TorConnect to handle. In particular, we stop sending out
"torconnect:broadcast-user-action" to sync pages.
We also show "Try Again" if the user cancels the first bootstrap
attempt without an error.
We also do not try and sync the selected region between pages. However
all pages should still show the *actually* submitted region after a
bootstrap fails. E.g. to confirm their location.
We also allow the user to re-select "Automatic" when they use
breadcrumbs to go back a stage.
Also change gTorConnectTitlebarStatus and gTorConnectUrlbarButton to use
TorConnectStage.
- - - - -
771381c5 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 40597: Implement TorSettings module
Bug 41710: Switch TorConnect.openTorConnect to use new methods.
- - - - -
fafc6ff3 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 41710: Switch from TorConnect.state to TorConnect.stage.
- - - - -
2ddd2029 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 40597: Implement TorSettings module
Bug 41710: Remove unused TorConnect properties.
- - - - -
3a27b920 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 42247: Android helpers for the TorProvider
Bug 41710: Switch Android to new TorConnect methods and add TODOs.
- - - - -
4aa64ccc by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Lox integration
Bug 41710: Switch from TorConnectState to TorConnectStage.
- - - - -
56837486 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser
Bug 41710: Move viewTorLogs and openTorPreferences to TorConnectParent.
- - - - -
ebe8a652 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 40597: Implement TorSettings module
Bug 41710: Move viewTorLogs to TorConnectParent.
- - - - -
8f95d948 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! [android] Enable the connect assist experiments on alpha
Bug 41710: Remove onSettingsRequested.
- - - - -
bba68db8 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! [android] Add Tor integration and UI
Bug 41710: Remove onSettingsRequested.
- - - - -
a6be0160 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Temporary changes to about:torconnect for Android.
Bug 41710: Remove onSettingsRequested.
TorConnect.openTorPreferences was removed.
- - - - -
be125ecd by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! Bug 40597: Implement TorSettings module
Bug 41710: Remove the beginBootstrap, beginAutoBootstrap, and
cancelBootstrap methods.
- - - - -
1f6fc087 by Henry Wilkes at 2024-11-13T08:23:22+00:00
fixup! [android] Add Tor integration and UI
Bug 41710: Fix onBootstrapProgress for new TorConnect.
The bootstrap progress signal is now released every time the stage
changes.
- - - - -
18 changed files:
- browser/base/content/browser.js
- browser/base/content/browser.js.globals
- browser/components/torpreferences/content/builtinBridgeDialog.js
- browser/components/torpreferences/content/connectionPane.js
- browser/components/torpreferences/content/provideBridgeDialog.js
- browser/components/torpreferences/content/requestBridgeDialog.js
- mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
- mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorControllerGV.kt
- mobile/android/geckoview/src/main/java/org/mozilla/geckoview/TorIntegrationAndroid.java
- toolkit/components/lox/Lox.sys.mjs
- toolkit/components/torconnect/TorConnectChild.sys.mjs
- toolkit/components/torconnect/TorConnectParent.sys.mjs
- toolkit/components/torconnect/content/aboutTorConnect.js
- toolkit/components/torconnect/content/torConnectTitlebarStatus.js
- toolkit/components/torconnect/content/torConnectUrlbarButton.js
- toolkit/modules/RemotePageAccessManager.sys.mjs
- toolkit/modules/TorAndroidIntegration.sys.mjs
- toolkit/modules/TorConnect.sys.mjs
Changes:
=====================================
browser/base/content/browser.js
=====================================
@@ -85,7 +85,7 @@ ChromeUtils.defineESModuleGetters(this, {
TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
- TorConnectState: "resource://gre/modules/TorConnect.sys.mjs",
+ TorConnectStage: "resource://gre/modules/TorConnect.sys.mjs",
TorConnectTopics: "resource://gre/modules/TorConnect.sys.mjs",
TorUIUtils: "resource:///modules/TorUIUtils.sys.mjs",
TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
=====================================
browser/base/content/browser.js.globals
=====================================
@@ -276,7 +276,7 @@
"TorDomainIsolator",
"gTorCircuitPanel",
"TorConnect",
- "TorConnectState",
+ "TorConnectStage",
"TorConnectTopics",
"gTorConnectUrlbarButton",
"gTorConnectTitlebarStatus",
=====================================
browser/components/torpreferences/content/builtinBridgeDialog.js
=====================================
@@ -79,14 +79,14 @@ const gBuiltinBridgeDialog = {
this._acceptButton = dialog.getButton("accept");
- Services.obs.addObserver(this, TorConnectTopics.StateChange);
+ Services.obs.addObserver(this, TorConnectTopics.StageChange);
this.onSelectChange();
this.onAcceptStateChange();
},
uninit() {
- Services.obs.removeObserver(this, TorConnectTopics.StateChange);
+ Services.obs.removeObserver(this, TorConnectTopics.StageChange);
},
onSelectChange() {
@@ -107,7 +107,7 @@ const gBuiltinBridgeDialog = {
observe(subject, topic) {
switch (topic) {
- case TorConnectTopics.StateChange:
+ case TorConnectTopics.StageChange:
this.onAcceptStateChange();
break;
}
=====================================
browser/components/torpreferences/content/connectionPane.js
=====================================
@@ -22,7 +22,7 @@ const { TorProviderBuilder, TorProviderTopics } = ChromeUtils.importESModule(
"resource://gre/modules/TorProviderBuilder.sys.mjs"
);
-const { TorConnect, TorConnectTopics, TorConnectState, TorCensorshipLevel } =
+const { TorConnect, TorConnectTopics, TorConnectStage, TorCensorshipLevel } =
ChromeUtils.importESModule("resource://gre/modules/TorConnect.sys.mjs");
const { MoatRPC } = ChromeUtils.importESModule(
@@ -2195,18 +2195,7 @@ const gBridgeSettings = {
// Start Bootstrapping, which should use the configured bridges.
// NOTE: We do this regardless of any previous TorConnect Error.
- if (TorConnect.canBeginBootstrap) {
- TorConnect.beginBootstrap();
- }
- // Open "about:torconnect".
- // FIXME: If there has been a previous bootstrapping error then
- // "about:torconnect" will be trying to get the user to use
- // AutoBootstrapping. It is not set up to handle a forced direct
- // entry to plain Bootstrapping from this dialog so the UI will
- // not be aligned. In particular the
- // AboutTorConnect.uiState.bootstrapCause will be aligned to
- // whatever was shown previously in "about:torconnect" instead.
- TorConnect.openTorConnect();
+ TorConnect.openTorConnect({ beginBootstrapping: "hard" });
});
},
// closedCallback should be called after gSubDialog has already
@@ -2322,27 +2311,27 @@ const gNetworkStatus = {
"network-status-tor-connect-button"
);
this._torConnectButton.addEventListener("click", () => {
- TorConnect.openTorConnect({ beginBootstrap: true });
+ TorConnect.openTorConnect({ beginBootstrapping: "soft" });
});
this._updateInternetStatus("unknown");
this._updateTorConnectionStatus();
- Services.obs.addObserver(this, TorConnectTopics.StateChange);
+ Services.obs.addObserver(this, TorConnectTopics.StageChange);
},
/**
* Un-initialize the area.
*/
uninit() {
- Services.obs.removeObserver(this, TorConnectTopics.StateChange);
+ Services.obs.removeObserver(this, TorConnectTopics.StageChange);
},
observe(subject, topic) {
switch (topic) {
// triggered when tor connect state changes and we may
// need to update the messagebox
- case TorConnectTopics.StateChange: {
+ case TorConnectTopics.StageChange: {
this._updateTorConnectionStatus();
break;
}
@@ -2433,7 +2422,8 @@ const gNetworkStatus = {
const buttonHadFocus = this._torConnectButton.contains(
document.activeElement
);
- const isBootstrapped = TorConnect.state === TorConnectState.Bootstrapped;
+ const isBootstrapped =
+ TorConnect.stageName === TorConnectStage.Bootstrapped;
const isBlocked = !isBootstrapped && TorConnect.potentiallyBlocked;
let l10nId;
if (isBootstrapped) {
@@ -2527,7 +2517,8 @@ const gConnectionPane = (function () {
);
chooseForMe.addEventListener("command", () => {
TorConnect.openTorConnect({
- beginAutoBootstrap: location.value,
+ beginBootstrapping: "hard",
+ regionCode: location.value,
});
});
this._populateLocations = () => {
@@ -2558,7 +2549,7 @@ const gConnectionPane = (function () {
locationEntries.append(...items);
};
locationEntries.append(
- createItem("", TorStrings.settings.bridgeLocationAutomatic)
+ createItem("automatic", TorStrings.settings.bridgeLocationAutomatic)
);
if (TorConnect.countryCodes.length) {
locationEntries.append(
@@ -2607,7 +2598,7 @@ const gConnectionPane = (function () {
this.onViewTorLogs();
});
- Services.obs.addObserver(this, TorConnectTopics.StateChange);
+ Services.obs.addObserver(this, TorConnectTopics.StageChange);
},
init() {
@@ -2629,7 +2620,7 @@ const gConnectionPane = (function () {
// unregister our observer topics
Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
- Services.obs.removeObserver(this, TorConnectTopics.StateChange);
+ Services.obs.removeObserver(this, TorConnectTopics.StageChange);
},
// whether the page should be present in about:preferences
@@ -2653,7 +2644,7 @@ const gConnectionPane = (function () {
}
// triggered when tor connect state changes and we may
// need to update the messagebox
- case TorConnectTopics.StateChange: {
+ case TorConnectTopics.StageChange: {
this._showAutoconfiguration();
break;
}
=====================================
browser/components/torpreferences/content/provideBridgeDialog.js
=====================================
@@ -128,14 +128,14 @@ const gProvideBridgeDialog = {
this.onDialogAccept(event)
);
- Services.obs.addObserver(this, TorConnectTopics.StateChange);
+ Services.obs.addObserver(this, TorConnectTopics.StageChange);
this.setPage("entry");
this.checkValue();
},
uninit() {
- Services.obs.removeObserver(this, TorConnectTopics.StateChange);
+ Services.obs.removeObserver(this, TorConnectTopics.StageChange);
},
/**
@@ -512,7 +512,7 @@ const gProvideBridgeDialog = {
observe(subject, topic) {
switch (topic) {
- case TorConnectTopics.StateChange:
+ case TorConnectTopics.StageChange:
this.onAcceptStateChange();
break;
}
=====================================
browser/components/torpreferences/content/requestBridgeDialog.js
=====================================
@@ -91,14 +91,14 @@ const gRequestBridgeDialog = {
selectors.incorrectCaptchaHbox
);
- Services.obs.addObserver(this, TorConnectTopics.StateChange);
+ Services.obs.addObserver(this, TorConnectTopics.StageChange);
this.onAcceptStateChange();
},
uninit() {
BridgeDB.close();
// Unregister our observer topics.
- Services.obs.removeObserver(this, TorConnectTopics.StateChange);
+ Services.obs.removeObserver(this, TorConnectTopics.StageChange);
},
onAcceptStateChange() {
@@ -113,7 +113,7 @@ const gRequestBridgeDialog = {
observe(subject, topic) {
switch (topic) {
- case TorConnectTopics.StateChange:
+ case TorConnectTopics.StageChange:
this.onAcceptStateChange();
break;
}
=====================================
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
=====================================
@@ -1438,7 +1438,4 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity, TorIn
navHost.navController.navigate(NavGraphDirections.actionStartupHome())
}
override fun onBootstrapError(code: String?, message: String?, phase: String?, reason: String?) = Unit
- override fun onSettingsRequested() {
- navHost.navController.navigate(NavGraphDirections.actionGlobalSettingsFragment())
- }
}
=====================================
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorControllerGV.kt
=====================================
@@ -332,20 +332,28 @@ class TorControllerGV(
// TorEventsBootstrapStateChangeListener
override fun onBootstrapProgress(progress: Double, hasWarnings: Boolean) {
Log.d(TAG, "onBootstrapProgress($progress, $hasWarnings)")
+ // TODO: onBootstrapProgress should only be used to change the shown
+ // bootstrap percentage or a Tor log option during a "Bootstrapping"
+ // stage.
+ // The progress value should not be used to change the `lastKnownStatus`
+ // value or determine if a bootstrap has started or completed. The
+ // TorConnectStage should be used instead.
if (progress == 100.0) {
lastKnownStatus = TorConnectState.Bootstrapped
wasTorBootstrapped = true
onTorConnected()
- } else {
- lastKnownStatus = TorConnectState.Bootstrapping
+ } else if (lastKnownStatus == TorConnectState.Bootstrapping) {
onTorConnecting()
-
}
onTorStatusUpdate("", lastKnownStatus.toTorStatus().status, progress)
}
// TorEventsBootstrapStateChangeListener
override fun onBootstrapComplete() {
+ // TODO: There should be no need to respond to the BootstrapComplete
+ // event if we are already handling TorConnectStage.Bootstrapped.
+ // In particular, `lastKnownStatus` and onTorConnected should be set in
+ // response to a change in TorConnectStage instead.
lastKnownStatus = TorConnectState.Bootstrapped
this.onTorConnected()
}
@@ -354,9 +362,4 @@ class TorControllerGV(
override fun onBootstrapError(code: String?, message: String?, phase: String?, reason: String?) {
lastKnownError = TorError(code ?: "", message ?: "", phase ?: "", reason ?: "")
}
-
- // TorEventsBootstrapStateChangeListener
- override fun onSettingsRequested() {
- // noop
- }
}
=====================================
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/TorIntegrationAndroid.java
=====================================
@@ -44,7 +44,6 @@ public class TorIntegrationAndroid implements BundleEventListener {
private static final String EVENT_TOR_LOGS = "GeckoView:Tor:Logs";
private static final String EVENT_SETTINGS_READY = "GeckoView:Tor:SettingsReady";
private static final String EVENT_SETTINGS_CHANGED = "GeckoView:Tor:SettingsChanged";
- private static final String EVENT_SETTINGS_OPEN = "GeckoView:Tor:OpenSettings";
// Events we emit
private static final String EVENT_SETTINGS_GET = "GeckoView:Tor:SettingsGet";
@@ -118,8 +117,7 @@ public class TorIntegrationAndroid implements BundleEventListener {
EVENT_CONNECT_ERROR,
EVENT_BOOTSTRAP_PROGRESS,
EVENT_BOOTSTRAP_COMPLETE,
- EVENT_TOR_LOGS,
- EVENT_SETTINGS_OPEN);
+ EVENT_TOR_LOGS);
}
@Override // BundleEventListener
@@ -176,10 +174,6 @@ public class TorIntegrationAndroid implements BundleEventListener {
for (TorLogListener listener : mLogListeners) {
listener.onLog(type, msg);
}
- } else if (EVENT_SETTINGS_OPEN.equals(event)) {
- for (BootstrapStateChangeListener listener : mBootstrapStateListeners) {
- listener.onSettingsRequested();
- }
}
}
@@ -641,8 +635,6 @@ public class TorIntegrationAndroid implements BundleEventListener {
void onBootstrapComplete();
void onBootstrapError(String code, String message, String phase, String reason);
-
- void onSettingsRequested();
}
public interface TorLogListener {
=====================================
toolkit/components/lox/Lox.sys.mjs
=====================================
@@ -23,7 +23,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
DomainFrontRequestResponseError:
"resource://gre/modules/DomainFrontedRequests.sys.mjs",
TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
- TorConnectState: "resource://gre/modules/TorConnect.sys.mjs",
+ TorConnectStage: "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",
@@ -1049,7 +1049,7 @@ class LoxImpl {
const method = "POST";
const contentType = "application/vnd.api+json";
- if (lazy.TorConnect.state === lazy.TorConnectState.Bootstrapped) {
+ if (lazy.TorConnect.stageName === lazy.TorConnectStage.Bootstrapped) {
let request;
try {
request = await fetch(url, {
=====================================
toolkit/components/torconnect/TorConnectChild.sys.mjs
=====================================
@@ -77,7 +77,7 @@ export class TorConnectChild extends RemotePageChild {
receiveMessage(message) {
super.receiveMessage(message);
- if (message.name === "torconnect:state-change") {
+ if (message.name === "torconnect:stage-change") {
this.#maybeRedirect();
}
}
=====================================
toolkit/components/torconnect/TorConnectParent.sys.mjs
=====================================
@@ -2,29 +2,20 @@
import { TorStrings } from "resource://gre/modules/TorStrings.sys.mjs";
import {
- InternetStatus,
TorConnect,
TorConnectTopics,
- TorConnectState,
} from "resource://gre/modules/TorConnect.sys.mjs";
import {
TorSettings,
TorSettingsTopics,
} from "resource://gre/modules/TorSettings.sys.mjs";
-const BroadcastTopic = "about-torconnect:broadcast";
-
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
HomePage: "resource:///modules/HomePage.sys.jsm",
});
-const log = console.createInstance({
- maxLogLevel: "Warn",
- prefix: "TorConnectParent",
-});
-
/*
This object is basically a marshalling interface between the TorConnect module
and a particular about:torconnect page
@@ -40,31 +31,6 @@ export class TorConnectParent extends JSWindowActorParent {
const self = this;
- this.state = {
- State: TorConnect.state,
- StateChanged: false,
- PreviousState: TorConnectState.Initial,
- ErrorCode: TorConnect.errorCode,
- ErrorDetails: TorConnect.errorDetails,
- BootstrapProgress: TorConnect.bootstrapProgress,
- InternetStatus: TorConnect.internetStatus,
- DetectedLocation: TorConnect.detectedLocation,
- ShowViewLog: TorConnect.logHasWarningOrError,
- HasEverFailed: TorConnect.hasEverFailed,
- UIState: TorConnect.uiState,
- };
-
- // Workaround for a race condition, but we should fix it asap.
- // about:torconnect is loaded before TorSettings is actually initialized.
- // The getter might throw and the page not loaded correctly as a result.
- // Silence any warning for now, but we should really fix it.
- // See also tor-browser#41921.
- try {
- this.state.QuickStartEnabled = TorSettings.quickstart.enabled;
- } catch (e) {
- this.state.QuickStartEnabled = false;
- }
-
// JSWindowActiveParent derived objects cannot observe directly, so create a
// member object to do our observing for us.
//
@@ -72,103 +38,54 @@ export class TorConnectParent extends JSWindowActorParent {
// module, and maintains a state object which we pass down to our
// about:torconnect page, which uses the state object to update its UI.
this.torConnectObserver = {
- observe(aSubject, aTopic) {
- let obj = aSubject?.wrappedJSObject;
-
- // Update our state struct based on received torconnect topics and
- // forward on to aboutTorConnect.js.
- self.state.StateChanged = false;
- switch (aTopic) {
- case TorConnectTopics.StateChange: {
- self.state.PreviousState = self.state.State;
- self.state.State = obj.state;
- self.state.StateChanged = true;
- // Clear any previous error information if we are bootstrapping.
- if (self.state.State === TorConnectState.Bootstrapping) {
- self.state.ErrorCode = null;
- self.state.ErrorDetails = null;
- }
- self.state.BootstrapProgress = TorConnect.bootstrapProgress;
- self.state.ShowViewLog = TorConnect.logHasWarningOrError;
- self.state.HasEverFailed = TorConnect.hasEverFailed;
- break;
- }
- case TorConnectTopics.BootstrapProgress: {
- self.state.BootstrapProgress = obj.progress;
- self.state.ShowViewLog = obj.hasWarnings;
- break;
- }
- case TorConnectTopics.BootstrapComplete: {
- // noop
+ observe(subject, topic) {
+ const obj = subject?.wrappedJSObject;
+ switch (topic) {
+ case TorConnectTopics.StageChange:
+ self.sendAsyncMessage("torconnect:stage-change", obj);
break;
- }
- case TorConnectTopics.Error: {
- self.state.ErrorCode = obj.code;
- self.state.ErrorDetails = obj;
- self.state.InternetStatus = TorConnect.internetStatus;
- self.state.DetectedLocation = TorConnect.detectedLocation;
- self.state.ShowViewLog = true;
+ case TorConnectTopics.BootstrapProgress:
+ self.sendAsyncMessage("torconnect:bootstrap-progress", obj);
break;
- }
- case TorSettingsTopics.Ready: {
- if (
- self.state.QuickStartEnabled !== TorSettings.quickstart.enabled
- ) {
- self.state.QuickStartEnabled = TorSettings.quickstart.enabled;
- } else {
- return;
+ case TorSettingsTopics.SettingsChanged:
+ if (!obj.changes.includes("quickstart.enabled")) {
+ break;
}
+ // eslint-disable-next-lined no-fallthrough
+ case TorSettingsTopics.Ready:
+ self.sendAsyncMessage(
+ "torconnect:quickstart-changed",
+ TorSettings.quickstart.enabled
+ );
break;
- }
- case TorSettingsTopics.SettingsChanged: {
- if (
- aSubject.wrappedJSObject.changes.includes("quickstart.enabled")
- ) {
- self.state.QuickStartEnabled = TorSettings.quickstart.enabled;
- } else {
- // this isn't a setting torconnect cares about
- return;
- }
- break;
- }
- default: {
- log.warn(`TorConnect: unhandled observe topic '${aTopic}'`);
- }
}
-
- self.sendAsyncMessage("torconnect:state-change", self.state);
},
};
- // Observe all of the torconnect:.* topics.
- for (const key in TorConnectTopics) {
- const topic = TorConnectTopics[key];
- Services.obs.addObserver(this.torConnectObserver, topic);
- }
+ Services.obs.addObserver(
+ this.torConnectObserver,
+ TorConnectTopics.StageChange
+ );
+ Services.obs.addObserver(
+ this.torConnectObserver,
+ TorConnectTopics.BootstrapProgress
+ );
Services.obs.addObserver(this.torConnectObserver, TorSettingsTopics.Ready);
Services.obs.addObserver(
this.torConnectObserver,
TorSettingsTopics.SettingsChanged
);
-
- this.userActionObserver = {
- observe(aSubject) {
- let obj = aSubject?.wrappedJSObject;
- if (obj) {
- obj.connState = self.state;
- self.sendAsyncMessage("torconnect:user-action", obj);
- }
- },
- };
- Services.obs.addObserver(this.userActionObserver, BroadcastTopic);
}
willDestroy() {
- // Stop observing all of our torconnect:.* topics.
- for (const key in TorConnectTopics) {
- const topic = TorConnectTopics[key];
- Services.obs.removeObserver(this.torConnectObserver, topic);
- }
+ Services.obs.removeObserver(
+ this.torConnectObserver,
+ TorConnectTopics.StageChange
+ );
+ Services.obs.removeObserver(
+ this.torConnectObserver,
+ TorConnectTopics.BootstrapProgress
+ );
Services.obs.removeObserver(
this.torConnectObserver,
TorSettingsTopics.Ready
@@ -177,7 +94,6 @@ export class TorConnectParent extends JSWindowActorParent {
this.torConnectObserver,
TorSettingsTopics.SettingsChanged
);
- Services.obs.removeObserver(this.userActionObserver, BroadcastTopic);
}
async receiveMessage(message) {
@@ -192,48 +108,57 @@ export class TorConnectParent extends JSWindowActorParent {
TorSettings.saveToPrefs().applySettings();
break;
case "torconnect:open-tor-preferences":
- TorConnect.openTorPreferences();
- break;
- case "torconnect:cancel-bootstrap":
- TorConnect.cancelBootstrap();
- break;
- case "torconnect:begin-bootstrap":
- TorConnect.beginBootstrap();
- break;
- case "torconnect:begin-autobootstrap":
- TorConnect.beginAutoBootstrap(message.data);
+ this.browsingContext.top.embedderElement.ownerGlobal.openPreferences(
+ "connection"
+ );
break;
case "torconnect:view-tor-logs":
- TorConnect.viewTorLogs();
+ this.browsingContext.top.embedderElement.ownerGlobal.openPreferences(
+ "connection-viewlogs"
+ );
break;
case "torconnect:restart":
Services.startup.quit(
Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit
);
break;
- case "torconnect:set-ui-state":
- TorConnect.uiState = message.data;
- this.state.UIState = TorConnect.uiState;
+ case "torconnect:start-again":
+ TorConnect.startAgain();
+ break;
+ case "torconnect:choose-region":
+ TorConnect.chooseRegion();
+ break;
+ case "torconnect:begin-bootstrapping":
+ TorConnect.beginBootstrapping(message.data.regionCode);
break;
- case "torconnect:broadcast-user-action":
- Services.obs.notifyObservers(message.data, BroadcastTopic);
+ case "torconnect:cancel-bootstrapping":
+ TorConnect.cancelBootstrapping();
break;
- case "torconnect:get-init-args":
+ case "torconnect:get-init-args": {
// Called on AboutTorConnect.init(), pass down all state data it needs
// to init.
- // pretend this is a state transition on init
- // so we always get fresh UI
- this.state.StateChanged = true;
- this.state.UIState = TorConnect.uiState;
+ let quickstartEnabled = false;
+
+ // Workaround for a race condition, but we should fix it asap.
+ // about:torconnect is loaded before TorSettings is actually initialized.
+ // The getter might throw and the page not loaded correctly as a result.
+ // Silence any warning for now, but we should really fix it.
+ // See also tor-browser#41921.
+ try {
+ quickstartEnabled = TorSettings.quickstart.enabled;
+ } catch (e) {
+ // Do not throw.
+ }
+
return {
TorStrings,
- TorConnectState,
- InternetStatus,
Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr",
- State: this.state,
CountryNames: TorConnect.countryNames,
+ stage: TorConnect.stage,
+ quickstartEnabled,
};
+ }
case "torconnect:get-country-codes":
return TorConnect.getCountryCodes();
}
=====================================
toolkit/components/torconnect/content/aboutTorConnect.js
=====================================
@@ -7,8 +7,6 @@
// populated in AboutTorConnect.init()
let TorStrings = {};
-let TorConnectState = {};
-let InternetStatus = {};
const UIStates = Object.freeze({
ConnectToTor: "ConnectToTor",
@@ -135,53 +133,23 @@ class AboutTorConnect {
tryBridgeButton: document.querySelector(this.selectors.buttons.tryBridge),
});
- uiState = {
- currentState: UIStates.ConnectToTor,
- allowAutomaticLocation: true,
- selectedLocation: "automatic",
- bootstrapCause: UIStates.ConnectToTor,
- };
+ selectedLocation;
+ shownStage = null;
locations = {};
- constructor() {
- this.uiStates = Object.freeze(
- Object.fromEntries([
- [UIStates.ConnectToTor, this.showConnectToTor.bind(this)],
- [UIStates.Offline, this.showOffline.bind(this)],
- [UIStates.ConnectionAssist, this.showConnectionAssistant.bind(this)],
- [UIStates.CouldNotLocate, this.showCouldNotLocate.bind(this)],
- [UIStates.LocationConfirm, this.showLocationConfirmation.bind(this)],
- [UIStates.FinalError, this.showFinalError.bind(this)],
- ])
- );
- }
-
- beginBootstrap() {
- RPMSendAsyncMessage("torconnect:begin-bootstrap");
- }
-
- beginAutoBootstrap(countryCode) {
- if (countryCode === "automatic") {
- countryCode = "";
- }
- RPMSendAsyncMessage("torconnect:begin-autobootstrap", countryCode);
+ beginBootstrapping() {
+ RPMSendAsyncMessage("torconnect:begin-bootstrapping", {});
}
- cancelBootstrap() {
- RPMSendAsyncMessage("torconnect:cancel-bootstrap");
- }
-
- transitionUIState(nextState, connState) {
- if (nextState !== this.uiState.currentState) {
- this.uiState.currentState = nextState;
- this.saveUIState();
- }
- this.uiStates[nextState](connState);
+ beginAutoBootstrapping(regionCode) {
+ RPMSendAsyncMessage("torconnect:begin-bootstrapping", {
+ regionCode,
+ });
}
- saveUIState() {
- RPMSendAsyncMessage("torconnect:set-ui-state", this.uiState);
+ cancelBootstrapping() {
+ RPMSendAsyncMessage("torconnect:cancel-bootstrapping");
}
/*
@@ -305,19 +273,6 @@ class AboutTorConnect {
this.elements.longContentText.append(...args);
}
- setProgress(description, visible, percent) {
- this.elements.progressDescription.textContent = description;
- if (visible) {
- this.show(this.elements.progressMeter);
- this.elements.progressMeter.style.setProperty(
- "--progress-percent",
- `${percent}%`
- );
- } else {
- this.hide(this.elements.progressMeter);
- }
- }
-
setBreadcrumbsStatus(connectToTor, connectionAssist, tryBridge) {
this.elements.breadcrumbContainer.classList.remove("hidden");
const elems = [
@@ -362,22 +317,17 @@ class AboutTorConnect {
return TorStrings.torConnect.bootstrapStatus[status] ?? status;
}
- getMaybeLocalizedError(state) {
- if (!state?.ErrorCode) {
- return "";
- }
- switch (state.ErrorCode) {
+ getMaybeLocalizedError(error) {
+ switch (error.code) {
case "Offline":
return TorStrings.torConnect.offline;
case "BootstrapError": {
- const details = state.ErrorDetails?.cause;
- if (!details?.phase || !details?.reason) {
+ if (!error.phase || !error.reason) {
return TorStrings.torConnect.torBootstrapFailed;
}
- let status = this.getLocalizedStatus(details.phase);
+ let status = this.getLocalizedStatus(error.phase);
const reason =
- TorStrings.torConnect.bootstrapWarning[details.reason] ??
- details.reason;
+ TorStrings.torConnect.bootstrapWarning[error.reason] ?? error.reason;
return TorStrings.torConnect.bootstrapFailedDetails
.replace("%1$S", status)
.replace("%2$S", reason);
@@ -392,13 +342,10 @@ class AboutTorConnect {
// A standard JS error, or something for which we do probably do not
// have a translation. Returning the original message is the best we can
// do.
- return state.ErrorDetails.message;
+ return error.message;
default:
- console.warn(
- `Unknown error code: ${state.ErrorCode}`,
- state.ErrorDetails
- );
- return state.ErrorDetails?.message ?? state.ErrorCode;
+ console.warn(`Unknown error code: ${error.code}`, error);
+ return error.message || error.code;
}
}
@@ -406,109 +353,119 @@ class AboutTorConnect {
These methods update the UI based on the current TorConnect state
*/
- updateUI(state) {
- // calls update_$state()
- this[`update_${state.State}`](state);
- this.elements.quickstartToggle.pressed = state.QuickStartEnabled;
- }
+ updateStage(stage) {
+ if (stage.name === this.shownStage) {
+ return;
+ }
- /* Per-state updates */
+ this.shownStage = stage.name;
+ this.selectedLocation = stage.defaultRegion;
- update_Initial(state) {
- this.showConnectToTor(state);
- }
+ let showProgress = false;
+ let showLog = false;
+ switch (stage.name) {
+ case "Disabled":
+ console.error("Should not be open when TorConnect is disabled");
+ break;
+ case "Loading":
+ case "Start":
+ // Loading is not currnetly handled, treat the same as "Start", but UI
+ // will be unresponsive.
+ this.showStart(stage.tryAgain, stage.potentiallyBlocked);
+ break;
+ case "Bootstrapping":
+ showProgress = true;
+ this.showBootstrapping(stage.bootstrapTrigger, stage.tryAgain);
+ break;
+ case "Offline":
+ showLog = true;
+ this.showOffline();
+ break;
+ case "ChooseRegion":
+ showLog = true;
+ this.showChooseRegion(stage.error);
+ break;
+ case "RegionNotFound":
+ showLog = true;
+ this.showRegionNotFound();
+ break;
+ case "ConfirmRegion":
+ showLog = true;
+ this.showConfirmRegion(stage.error);
+ break;
+ case "FinalError":
+ showLog = true;
+ this.showFinalError(stage.error);
+ break;
+ case "Bootstrapped":
+ showProgress = true;
+ this.showBootstrapped();
+ break;
+ default:
+ console.error(`Unknown stage ${stage.name}`);
+ break;
+ }
- update_Configuring(state) {
- if (
- state.StateChanged &&
- (state.PreviousState === TorConnectState.Bootstrapping ||
- state.PreviousState === TorConnectState.AutoBootstrapping)
- ) {
- // The bootstrap has been cancelled
- this.transitionUIState(this.uiState.bootstrapCause, state);
+ if (showProgress) {
+ this.show(this.elements.progressMeter);
+ } else {
+ this.hide(this.elements.progressMeter);
}
- }
- update_AutoBootstrapping(state) {
- this.showBootstrapping(state);
- }
+ this.updateBootstrappingStatus(stage.bootstrappingStatus);
- update_Bootstrapping(state) {
- this.showBootstrapping(state);
+ if (showLog) {
+ this.show(this.elements.viewLogButton);
+ } else {
+ this.hide(this.elements.viewLogButton);
+ }
}
- update_Error(state) {
- if (!state.StateChanged) {
- return;
- }
- if (state.InternetStatus === InternetStatus.Offline) {
- this.transitionUIState(UIStates.Offline, state);
- } else if (state.PreviousState === TorConnectState.Bootstrapping) {
- this.transitionUIState(UIStates.ConnectionAssist, state);
- } else if (state.PreviousState === TorConnectState.AutoBootstrapping) {
- if (this.uiState.bootstrapCause === UIStates.ConnectionAssist) {
- if (this.getLocation() === "automatic") {
- this.uiState.allowAutomaticLocation = false;
- if (!state.DetectedLocation) {
- this.transitionUIState(UIStates.CouldNotLocate, state);
- return;
- }
- // Change the location only here, to avoid overriding any user change/
- // insisting with the detected location
- this.setLocation(state.DetectedLocation);
- }
- this.transitionUIState(UIStates.LocationConfirm, state);
- } else {
- this.transitionUIState(UIStates.FinalError, state);
- }
- } else {
- console.error(
- "We received an error starting from an unexpected state",
- state
- );
+ updateBootstrappingStatus(data) {
+ this.elements.progressMeter.style.setProperty(
+ "--progress-percent",
+ `${data.progress}%`
+ );
+ if (this.shownStage === "Bootstrapping" && data.hasWarning) {
+ // When bootstrapping starts, we hide the log button, but we re-show it if
+ // we get a warning.
+ this.show(this.elements.viewLogButton);
}
}
- update_Bootstrapped(_state) {
- const showProgressbar = true;
+ updateQuickstart(enabled) {
+ this.elements.quickstartToggle.pressed = enabled;
+ }
+ showBootstrapped() {
this.setTitle(TorStrings.torConnect.torConnected, "");
this.setLongText(TorStrings.settings.torPreferencesDescription);
- this.setProgress("", showProgressbar, 100);
+ this.elements.progressDescription.textContent = "";
this.hideButtons();
}
- update_Disabled(_state) {
- // TODO: we should probably have some UX here if a user goes to about:torconnect when
- // it isn't in use (eg using tor-launcher or system tor)
- }
-
- showConnectToTor(state) {
+ showStart(tryAgain, potentiallyBlocked) {
this.setTitle(TorStrings.torConnect.torConnect, "");
this.setLongText(TorStrings.settings.torPreferencesDescription);
- this.setProgress("", false);
- this.hide(this.elements.viewLogButton);
+ this.elements.progressDescription.textContent = "";
this.hideButtons();
this.show(this.elements.quickstartContainer);
this.show(this.elements.configureButton);
this.show(this.elements.connectButton, true);
- if (state?.StateChanged) {
- this.elements.connectButton.focus();
+ this.elements.connectButton.focus();
+ if (tryAgain) {
+ this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
}
- if (state?.HasEverFailed) {
+ if (potentiallyBlocked) {
this.setBreadcrumbsStatus(
BreadcrumbStatus.Active,
BreadcrumbStatus.Default,
BreadcrumbStatus.Disabled
);
- this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
}
- this.uiState.bootstrapCause = UIStates.ConnectToTor;
- this.saveUIState();
}
- showBootstrapping(state) {
- const showProgressbar = true;
+ showBootstrapping(trigger, tryAgain) {
let title = "";
let description = "";
const breadcrumbs = [
@@ -516,128 +473,114 @@ class AboutTorConnect {
BreadcrumbStatus.Disabled,
BreadcrumbStatus.Disabled,
];
- switch (this.uiState.bootstrapCause) {
- case UIStates.ConnectToTor:
+ switch (trigger) {
+ case "Start":
+ case "Offline":
breadcrumbs[0] = BreadcrumbStatus.Active;
- title = state.HasEverFailed
+ title = tryAgain
? TorStrings.torConnect.tryAgain
: TorStrings.torConnect.torConnecting;
description = TorStrings.settings.torPreferencesDescription;
break;
- case UIStates.ConnectionAssist:
+ case "ChooseRegion":
breadcrumbs[2] = BreadcrumbStatus.Active;
title = TorStrings.torConnect.tryingBridge;
description = TorStrings.torConnect.assistDescription;
break;
- case UIStates.CouldNotLocate:
+ case "RegionNotFound":
breadcrumbs[2] = BreadcrumbStatus.Active;
title = TorStrings.torConnect.tryingBridgeAgain;
description = TorStrings.torConnect.errorLocationDescription;
break;
- case UIStates.LocationConfirm:
+ case "ConfirmRegion":
breadcrumbs[2] = BreadcrumbStatus.Active;
title = TorStrings.torConnect.tryingBridgeAgain;
description = TorStrings.torConnect.isLocationCorrectDescription;
break;
+ default:
+ console.warn("Unrecognized bootstrap trigger", trigger);
+ break;
}
this.setTitle(title, "");
this.showConfigureConnectionLink(description);
- this.setProgress("", showProgressbar, state.BootstrapProgress);
- if (state.HasEverFailed) {
+ this.elements.progressDescription.textContent = "";
+ if (tryAgain) {
this.setBreadcrumbsStatus(...breadcrumbs);
} else {
this.hideBreadcrumbs();
}
this.hideButtons();
- if (state.ShowViewLog) {
- this.show(this.elements.viewLogButton);
- } else {
- this.hide(this.elements.viewLogButton);
- }
this.show(this.elements.cancelButton);
- if (state.StateChanged) {
- this.elements.cancelButton.focus();
- }
+ this.elements.cancelButton.focus();
}
- showOffline(state) {
+ showOffline() {
this.setTitle(TorStrings.torConnect.noInternet, "offline");
this.setLongText(TorStrings.torConnect.noInternetDescription);
- this.setProgress(this.getMaybeLocalizedError(state), false);
+ this.elements.progressDescription.textContent =
+ TorStrings.torConnect.offline;
this.setBreadcrumbsStatus(
BreadcrumbStatus.Default,
BreadcrumbStatus.Active,
BreadcrumbStatus.Hidden
);
- this.show(this.elements.viewLogButton);
this.hideButtons();
this.show(this.elements.configureButton);
this.show(this.elements.connectButton, true);
this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
}
- showConnectionAssistant(state) {
+ showChooseRegion(error) {
this.setTitle(TorStrings.torConnect.couldNotConnect, "assist");
this.showConfigureConnectionLink(TorStrings.torConnect.assistDescription);
- this.setProgress(this.getMaybeLocalizedError(state), false);
+ this.elements.progressDescription.textContent =
+ this.getMaybeLocalizedError(error);
this.setBreadcrumbsStatus(
BreadcrumbStatus.Default,
BreadcrumbStatus.Active,
BreadcrumbStatus.Disabled
);
- this.showLocationForm(false, TorStrings.torConnect.tryBridge);
- if (state?.StateChanged) {
- this.elements.tryBridgeButton.focus();
- }
- this.uiState.bootstrapCause = UIStates.ConnectionAssist;
- this.saveUIState();
+ this.showLocationForm(true, TorStrings.torConnect.tryBridge);
+ this.elements.tryBridgeButton.focus();
}
- showCouldNotLocate(state) {
- this.uiState.allowAutomaticLocation = false;
+ showRegionNotFound() {
this.setTitle(TorStrings.torConnect.errorLocation, "location");
this.showConfigureConnectionLink(
TorStrings.torConnect.errorLocationDescription
);
- this.setProgress(TorStrings.torConnect.cannotDetermineCountry, false);
+ this.elements.progressDescription.textContent =
+ TorStrings.torConnect.cannotDetermineCountry;
this.setBreadcrumbsStatus(
BreadcrumbStatus.Default,
BreadcrumbStatus.Active,
BreadcrumbStatus.Disabled
);
- this.show(this.elements.viewLogButton);
- this.showLocationForm(true, TorStrings.torConnect.tryBridge);
- if (state.StateChanged) {
- this.elements.tryBridgeButton.focus();
- }
- this.uiState.bootstrapCause = UIStates.CouldNotLocate;
- this.saveUIState();
+ this.showLocationForm(false, TorStrings.torConnect.tryBridge);
+ this.elements.tryBridgeButton.focus();
}
- showLocationConfirmation(state) {
+ showConfirmRegion(error) {
this.setTitle(TorStrings.torConnect.isLocationCorrect, "location");
this.showConfigureConnectionLink(
TorStrings.torConnect.isLocationCorrectDescription
);
- this.setProgress(this.getMaybeLocalizedError(state), false);
+ this.elements.progressDescription.textContent =
+ this.getMaybeLocalizedError(error);
this.setBreadcrumbsStatus(
BreadcrumbStatus.Default,
BreadcrumbStatus.Default,
BreadcrumbStatus.Active
);
- this.show(this.elements.viewLogButton);
- this.showLocationForm(true, TorStrings.torConnect.tryAgain);
- if (state.StateChanged) {
- this.elements.tryBridgeButton.focus();
- }
- this.uiState.bootstrapCause = UIStates.LocationConfirm;
- this.saveUIState();
+ this.showLocationForm(false, TorStrings.torConnect.tryAgain);
+ this.elements.tryBridgeButton.focus();
}
- showFinalError(state) {
+ showFinalError(error) {
this.setTitle(TorStrings.torConnect.finalError, "final");
this.setLongText(TorStrings.torConnect.finalErrorDescription);
- this.setProgress(this.getMaybeLocalizedError(state), false);
+ this.elements.progressDescription.textContent =
+ this.getMaybeLocalizedError(error);
this.setBreadcrumbsStatus(
BreadcrumbStatus.Default,
BreadcrumbStatus.Default,
@@ -665,7 +608,7 @@ class AboutTorConnect {
}
}
- showLocationForm(isError, buttonLabel) {
+ showLocationForm(isChoose, buttonLabel) {
this.hideButtons();
RPMSendQuery("torconnect:get-country-codes").then(codes => {
if (codes && codes.length) {
@@ -674,7 +617,7 @@ class AboutTorConnect {
}
});
let firstOpt = this.elements.locationDropdownSelect.options[0];
- if (this.uiState.allowAutomaticLocation) {
+ if (isChoose) {
firstOpt.value = "automatic";
firstOpt.textContent = TorStrings.torConnect.automatic;
} else {
@@ -685,7 +628,7 @@ class AboutTorConnect {
this.validateLocation();
this.show(this.elements.locationDropdownLabel);
this.show(this.elements.locationDropdown);
- this.elements.locationDropdownLabel.classList.toggle("error", isError);
+ this.elements.locationDropdownLabel.classList.toggle("error", !isChoose);
this.show(this.elements.tryBridgeButton, true);
if (buttonLabel !== undefined) {
this.elements.tryBridgeButton.textContent = buttonLabel;
@@ -697,12 +640,8 @@ class AboutTorConnect {
return this.elements.locationDropdownSelect.options[selectedIndex].value;
}
- setLocation(code) {
- if (!code) {
- code = this.uiState.selectedLocation;
- } else {
- this.uiState.selectedLocation = code;
- }
+ setLocation() {
+ const code = this.selectedLocation;
if (this.getLocation() === code) {
return;
}
@@ -726,13 +665,7 @@ class AboutTorConnect {
document.documentElement.setAttribute("dir", direction);
this.elements.connectToTorLink.addEventListener("click", () => {
- if (this.uiState.currentState === UIStates.ConnectToTor) {
- return;
- }
- this.transitionUIState(UIStates.ConnectToTor, null);
- RPMSendAsyncMessage("torconnect:broadcast-user-action", {
- uiState: UIStates.ConnectToTor,
- });
+ RPMSendAsyncMessage("torconnect:start-again");
});
this.elements.connectToTorLabel.textContent =
TorStrings.torConnect.torConnect;
@@ -747,10 +680,7 @@ class AboutTorConnect {
) {
return;
}
- this.transitionUIState(UIStates.ConnectionAssist, null);
- RPMSendAsyncMessage("torconnect:broadcast-user-action", {
- uiState: UIStates.ConnectionAssist,
- });
+ RPMSendAsyncMessage("torconnect:choose-region");
});
this.elements.connectionAssistLabel.textContent =
TorStrings.torConnect.breadcrumbAssist;
@@ -786,23 +716,18 @@ class AboutTorConnect {
this.elements.cancelButton.textContent = TorStrings.torConnect.cancel;
this.elements.cancelButton.addEventListener("click", () => {
- this.cancelBootstrap();
+ this.cancelBootstrapping();
});
this.elements.connectButton.textContent =
TorStrings.torConnect.torConnectButton;
this.elements.connectButton.addEventListener("click", () => {
- this.beginBootstrap();
+ this.beginBootstrapping();
});
this.populateLocations();
this.elements.locationDropdownSelect.addEventListener("change", () => {
- this.uiState.selectedLocation = this.getLocation();
- this.saveUIState();
this.validateLocation();
- RPMSendAsyncMessage("torconnect:broadcast-user-action", {
- location: this.uiState.selectedLocation,
- });
});
this.elements.locationDropdownLabel.textContent =
@@ -811,10 +736,8 @@ class AboutTorConnect {
this.elements.tryBridgeButton.textContent = TorStrings.torConnect.tryBridge;
this.elements.tryBridgeButton.addEventListener("click", () => {
const value = this.getLocation();
- if (value === "automatic") {
- this.beginAutoBootstrap();
- } else {
- this.beginAutoBootstrap(value);
+ if (value) {
+ this.beginAutoBootstrapping(value);
}
});
@@ -846,17 +769,14 @@ class AboutTorConnect {
initObservers() {
// TorConnectParent feeds us state blobs to we use to update our UI
- RPMAddMessageListener("torconnect:state-change", ({ data }) => {
- this.updateUI(data);
+ RPMAddMessageListener("torconnect:stage-change", ({ data }) => {
+ this.updateStage(data);
});
- RPMAddMessageListener("torconnect:user-action", ({ data }) => {
- if (data.location) {
- this.uiState.selectedLocation = data.location;
- this.setLocation();
- }
- if (data.uiState !== undefined) {
- this.transitionUIState(data.uiState, data.connState);
- }
+ RPMAddMessageListener("torconnect:bootstrap-progress", ({ data }) => {
+ this.updateBootstrappingStatus(data);
+ });
+ RPMAddMessageListener("torconnect:quickstart-change", ({ data }) => {
+ this.updateQuickstart(data);
});
}
@@ -866,7 +786,7 @@ class AboutTorConnect {
// integers, so we must resort to a string compare here :(
// see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for relevant documentation
if (evt.code === "Escape") {
- this.cancelBootstrap();
+ this.cancelBootstrapping();
}
};
}
@@ -876,23 +796,14 @@ class AboutTorConnect {
// various constants
TorStrings = Object.freeze(args.TorStrings);
- TorConnectState = Object.freeze(args.TorConnectState);
- InternetStatus = Object.freeze(args.InternetStatus);
this.locations = args.CountryNames;
this.initElements(args.Direction);
this.initObservers();
this.initKeyboardShortcuts();
- if (Object.keys(args.State.UIState).length) {
- this.uiState = args.State.UIState;
- } else {
- args.State.UIState = this.uiState;
- this.saveUIState();
- }
- this.uiStates[this.uiState.currentState](args.State);
- // populate UI based on current state
- this.updateUI(args.State);
+ this.updateStage(args.stage);
+ this.updateQuickstart(args.quickstartEnabled);
}
}
=====================================
toolkit/components/torconnect/content/torConnectTitlebarStatus.js
=====================================
@@ -38,7 +38,7 @@ var gTorConnectTitlebarStatus = {
// The title also acts as an accessible name for the role="status".
this.node.setAttribute("title", this._strings.titlebarStatusName);
- this._observeTopic = TorConnectTopics.StateChange;
+ this._observeTopic = TorConnectTopics.StageChange;
this._stateListener = {
observe: (subject, topic) => {
if (topic !== this._observeTopic) {
@@ -66,17 +66,16 @@ var gTorConnectTitlebarStatus = {
let textId;
let connected = false;
let potentiallyBlocked = false;
- switch (TorConnect.state) {
- case TorConnectState.Disabled:
+ switch (TorConnect.stageName) {
+ case TorConnectStage.Disabled:
// Hide immediately.
this.node.hidden = true;
return;
- case TorConnectState.Bootstrapped:
+ case TorConnectStage.Bootstrapped:
textId = "titlebarStatusConnected";
connected = true;
break;
- case TorConnectState.Bootstrapping:
- case TorConnectState.AutoBootstrapping:
+ case TorConnectStage.Bootstrapping:
textId = "titlebarStatusConnecting";
break;
default:
=====================================
toolkit/components/torconnect/content/torConnectUrlbarButton.js
=====================================
@@ -55,13 +55,13 @@ var gTorConnectUrlbarButton = {
this.connect();
});
- this._observeTopic = TorConnectTopics.StateChange;
+ this._observeTopic = TorConnectTopics.StageChange;
this._stateListener = {
observe: (subject, topic) => {
if (topic !== this._observeTopic) {
return;
}
- this._torConnectStateChanged();
+ this._torConnectStageChanged();
},
};
Services.obs.addObserver(this._stateListener, this._observeTopic);
@@ -84,7 +84,7 @@ var gTorConnectUrlbarButton = {
// switching selected browser.
gBrowser.addProgressListener(this._locationListener);
- this._torConnectStateChanged();
+ this._torConnectStageChanged();
},
/**
@@ -105,17 +105,17 @@ var gTorConnectUrlbarButton = {
* Begin the tor connection bootstrapping process.
*/
connect() {
- TorConnect.openTorConnect({ beginBootstrap: true });
+ TorConnect.openTorConnect({ beginBootstrapping: "soft" });
},
/**
- * Callback for when the TorConnect state changes.
+ * Callback for when the TorConnect stage changes.
*/
- _torConnectStateChanged() {
- if (TorConnect.state === TorConnectState.Disabled) {
+ _torConnectStageChanged() {
+ if (TorConnect.stageName === TorConnectStage.Disabled) {
// NOTE: We do not uninit early when we reach the
- // TorConnectState.Bootstrapped state because we can still leave the
- // Bootstrapped state if the tor process exists early and needs a restart.
+ // TorConnectStage.Bootstrapped stage because we can still leave the
+ // Bootstrapped stage if the tor process exists early and needs a restart.
this.uninit();
return;
}
=====================================
toolkit/modules/RemotePageAccessManager.sys.mjs
=====================================
@@ -239,19 +239,19 @@ export let RemotePageAccessManager = {
},
"about:torconnect": {
RPMAddMessageListener: [
- "torconnect:state-change",
- "torconnect:user-action",
+ "torconnect:stage-change",
+ "torconnect:bootstrap-progress",
+ "torconnect:quickstart-change",
],
RPMSendAsyncMessage: [
"torconnect:open-tor-preferences",
- "torconnect:begin-bootstrap",
- "torconnect:begin-autobootstrap",
- "torconnect:cancel-bootstrap",
+ "torconnect:begin-bootstrapping",
+ "torconnect:cancel-bootstrapping",
"torconnect:set-quickstart",
"torconnect:view-tor-logs",
"torconnect:restart",
- "torconnect:set-ui-state",
- "torconnect:broadcast-user-action",
+ "torconnect:start-again",
+ "torconnect:choose-region",
],
RPMSendQuery: [
"torconnect:get-init-args",
=====================================
toolkit/modules/TorAndroidIntegration.sys.mjs
=====================================
@@ -83,6 +83,7 @@ class TorAndroidIntegrationImpl {
observe(subj, topic) {
switch (topic) {
+ // TODO: Replace with StageChange.
case lazy.TorConnectTopics.StateChange:
lazy.EventDispatcher.instance.sendRequest({
type: EmittedEvents.connectStateChanged,
@@ -101,6 +102,7 @@ class TorAndroidIntegrationImpl {
type: EmittedEvents.bootstrapComplete,
});
break;
+ // TODO: Replace with StageChange stage.error.
case lazy.TorConnectTopics.Error:
lazy.EventDispatcher.instance.sendRequest({
type: EmittedEvents.connectError,
@@ -159,17 +161,23 @@ class TorAndroidIntegrationImpl {
await lazy.TorSettings.saveToPrefs();
break;
case ListenedEvents.bootstrapBegin:
- lazy.TorConnect.beginBootstrap();
+ lazy.TorConnect.beginBootstrapping();
break;
case ListenedEvents.bootstrapBeginAuto:
- lazy.TorConnect.beginAutoBootstrap(data.countryCode);
+ // TODO: The countryCode should be set to "automatic" by the caller
+ // rather than `null`, so we can just pass in `data.countryCode`
+ // directly.
+ lazy.TorConnect.beginBootstrapping(data.countryCode || "automatic");
break;
case ListenedEvents.bootstrapCancel:
- lazy.TorConnect.cancelBootstrap();
+ lazy.TorConnect.cancelBootstrapping();
break;
+ // TODO: Replace with TorConnect.stage.
case ListenedEvents.bootstrapGetState:
callback?.onSuccess(lazy.TorConnect.state);
return;
+ // TODO: Expose TorConnect.startAgain() to allow users to begin
+ // from the start again.
}
callback?.onSuccess();
} catch (e) {
=====================================
toolkit/modules/TorConnect.sys.mjs
=====================================
@@ -8,7 +8,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
- EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
MoatRPC: "resource://gre/modules/Moat.sys.mjs",
TorBootstrapRequest: "resource://gre/modules/TorBootstrapRequest.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
@@ -79,240 +78,181 @@ ChromeUtils.defineLazyGetter(lazy, "logger", () =>
})
);
-/*
- TorConnect State Transitions
-
- ┌─────────┐ ┌────────┐
- │ ▼ ▼ │
- │ ┌──────────────────────────────────────────────────────────┐ │
- ┌─┼────── │ Error │ ◀───┐ │
- │ │ └──────────────────────────────────────────────────────────┘ │ │
- │ │ ▲ │ │
- │ │ │ │ │
- │ │ │ │ │
- │ │ ┌───────────────────────┐ ┌──────────┐ │ │
- │ │ ┌──── │ Initial │ ────────────────────▶ │ Disabled │ │ │
- │ │ │ └───────────────────────┘ └──────────┘ │ │
- │ │ │ │ │ │
- │ │ │ │ beginBootstrap() │ │
- │ │ │ ▼ │ │
- │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │
- │ │ │ │ Bootstrapping │ ────┘ │
- │ │ │ └──────────────────────────────────────────────────────────┘ │
- │ │ │ │ ▲ │ │
- │ │ │ │ cancelBootstrap() │ beginBootstrap() └────┐ │
- │ │ │ ▼ │ │ │
- │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │
- │ │ └───▶ │ │ ─┼────┘
- │ │ │ │ │
- │ │ │ │ │
- │ │ │ Configuring │ │
- │ │ │ │ │
- │ │ │ │ │
- └─┼─────▶ │ │ │
- │ └──────────────────────────────────────────────────────────┘ │
- │ │ ▲ ▲ │
- │ │ beginAutoBootstrap() │ cancelBootstrap() │ │
- │ ▼ │ │ │
- │ ┌───────────────────────┐ │ │ │
- └────── │ AutoBootstrapping │ ─┘ │ │
- └───────────────────────┘ │ │
- │ │ │
- │ ┌────────────────────────────────┘ │
- ▼ │ │
- ┌───────────────────────┐ │
- │ Bootstrapped │ ◀───────────────────────────────────┘
- └───────────────────────┘
-*/
-
/* Topics Notified by the TorConnect module */
export const TorConnectTopics = Object.freeze({
+ StageChange: "torconnect:stage-change",
+ // TODO: Remove torconnect:state-change when pages have switched to stage.
StateChange: "torconnect:state-change",
BootstrapProgress: "torconnect:bootstrap-progress",
BootstrapComplete: "torconnect:bootstrap-complete",
+ // TODO: Remove torconnect:error when pages have switched to stage.
Error: "torconnect:error",
});
-// The StateCallback is the base class to implement the various states.
-// All states should extend it and implement a `run` function, which can
-// optionally be async, and define an array of valid transitions.
-// The parent class will handle everything else, including the transition to
-// other states when the run function is complete etc...
-// A system is also provided to allow this function to early-out. The runner
-// should check the transitioning getter when appropriate and return.
-// In addition to that, a state can implement a transitionRequested callback,
-// which can be used in conjunction with a mechanism like Promise.race.
-// This allows to handle, for example, users' requests to cancel a bootstrap
-// attempt.
-// A state can optionally define a cleanup function, that will be run in all
-// cases before transitioning to the next state.
-class StateCallback {
- #state;
- #promise;
- #transitioning = false;
-
- constructor(stateName) {
- this.#state = stateName;
- }
-
- async begin(...args) {
- lazy.logger.trace(`Entering ${this.#state} state`);
- // Make sure we always have an actual promise.
- try {
- this.#promise = Promise.resolve(this.run(...args));
- } catch (err) {
- this.#promise = Promise.reject(err);
- }
- try {
- // If the callback throws, transition to error as soon as possible.
- await this.#promise;
- lazy.logger.info(`${this.#state}'s run is done`);
- } catch (err) {
- if (this.transitioning) {
- lazy.logger.error(
- `A transition from ${
- this.#state
- } is already happening, silencing this exception.`,
- err
- );
- return;
- }
- lazy.logger.error(
- `${this.#state}'s run threw, transitioning to the Error state.`,
- err
- );
- this.changeState(TorConnectState.Error, err);
- }
- }
-
- async end(nextState) {
- lazy.logger.trace(
- `Ending state ${this.#state} (to transition to ${nextState})`
- );
-
- if (this.#transitioning) {
- // Should we check turn this into an error?
- // It will make dealing with the error state harder.
- lazy.logger.warn("this.#transitioning is already true.");
- }
-
- // Signal we should bail out ASAP.
- this.#transitioning = true;
- if (this.transitionRequested) {
- this.transitionRequested();
- }
-
- lazy.logger.debug(
- `Waiting for the ${
- this.#state
- }'s callback to return before the transition.`
- );
- try {
- await this.#promise;
- } finally {
- lazy.logger.debug(`Calling ${this.#state}'s cleanup, if implemented.`);
- if (this.cleanup) {
- try {
- await this.cleanup(nextState);
- lazy.logger.debug(`${this.#state}'s cleanup function done.`);
- } catch (e) {
- lazy.logger.warn(`${this.#state}'s cleanup function threw.`, e);
- }
- }
- }
- }
-
- changeState(stateName, ...args) {
- TorConnect._changeState(stateName, ...args);
- }
-
- get transitioning() {
- return this.#transitioning;
- }
-
- get state() {
- return this.#state;
- }
-}
-
-// async method to sleep for a given amount of time
-const debugSleep = async ms => {
- return new Promise(resolve => {
- setTimeout(resolve, ms);
- });
-};
-
-class InitialState extends StateCallback {
- allowedTransitions = Object.freeze([
- TorConnectState.Disabled,
- TorConnectState.Bootstrapping,
- TorConnectState.Configuring,
- TorConnectState.Error,
- ]);
-
- constructor() {
- super(TorConnectState.Initial);
- }
-
- run() {
- // TODO: Block this transition until we successfully build a TorProvider.
- }
-}
-
-class ConfiguringState extends StateCallback {
- allowedTransitions = Object.freeze([
- TorConnectState.AutoBootstrapping,
- TorConnectState.Bootstrapping,
- TorConnectState.Error,
- ]);
-
- constructor() {
- super(TorConnectState.Configuring);
- }
-
- run() {
- TorConnect._bootstrapProgress = 0;
- }
-}
-
-class BootstrappingState extends StateCallback {
+/**
+ * @callback ProgressCallback
+ *
+ * @param {integer} progress - The progress percent.
+ */
+/**
+ * @typedef {object} BootstrapOptions
+ *
+ * Options for a bootstrap attempt.
+ *
+ * @property {boolean} [options.simulateCensorship] - Whether to simulate a
+ * failing bootstrap.
+ * @property {integer} [options.simulateDelay] - The delay in microseconds to
+ * apply to simulated bootstraps.
+ * @property {object} [options.simulateMoatResponse] - Simulate a Moat response
+ * for circumvention settings. Should include a "settings" property, and
+ * optionally a "country" property. You may add a "simulateCensorship"
+ * property to some of the settings to make only their bootstrap attempts
+ * fail.
+ * @property {boolean} [options.testInternet] - Whether to also test the
+ * internet connection.
+ * @property {boolean} [options.simulateOffline] - Whether to simulate an
+ * offline test result. This will not cause the bootstrap to fail.
+ * @property {string} [options.regionCode] - The region code to use to fetch
+ * auto-bootstrap settings, or "automatic" to automatically choose the region.
+ */
+/**
+ * @typedef {object} BootstrapResult
+ *
+ * The result of a bootstrap attempt.
+ *
+ * @property {string} [result] - The bootstrap result.
+ * @property {Error} [error] - An error from the attempt.
+ */
+/**
+ * @callback ResolveBootstrap
+ *
+ * Resolve a bootstrap attempt.
+ *
+ * @param {BootstrapResult} - The result, or error.
+ */
+
+/**
+ * Each instance can be used to attempt one bootstrapping.
+ */
+class BootstrapAttempt {
+ /**
+ * The ongoing bootstrap request.
+ *
+ * @type {?TorBootstrapRequest}
+ */
#bootstrap = null;
+ /**
+ * The error returned by the bootstrap request, if any.
+ *
+ * @type {?Error}
+ */
#bootstrapError = null;
+ /**
+ * The ongoing internet test, if any.
+ *
+ * @type {?InternetTest}
+ */
#internetTest = null;
+ /**
+ * The method to call to complete the `run` promise.
+ *
+ * @type {?ResolveBootstrap}
+ */
+ #resolveRun = null;
+ /**
+ * Whether the `run` promise has been, or is about to be, resolved.
+ *
+ * @type {boolean}
+ */
+ #resolved = false;
+ /**
+ * Whether a cancel request has been started.
+ *
+ * @type {boolean}
+ */
#cancelled = false;
- allowedTransitions = Object.freeze([
- TorConnectState.Configuring,
- TorConnectState.Bootstrapped,
- TorConnectState.Error,
- ]);
+ /**
+ * Run a bootstrap attempt.
+ *
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ *
+ * @return {Promise<string, Error>} - The result of the bootstrap.
+ */
+ run(progressCallback, options) {
+ const { promise, resolve, reject } = Promise.withResolvers();
+ this.#resolveRun = arg => {
+ if (this.#resolved) {
+ // Already been called once.
+ if (arg.error) {
+ lazy.logger.error("Delayed bootstrap error", arg.error);
+ }
+ return;
+ }
+ this.#resolved = true;
+ try {
+ // Should be ok to call this twice in the case where we "cancel" the
+ // bootstrap.
+ this.#internetTest?.cancel();
+ } catch (error) {
+ lazy.logger.error("Unexpected error in bootstrap cleanup", error);
+ }
+ if (arg.error) {
+ reject(arg.error);
+ } else {
+ resolve(arg.result);
+ }
+ };
+ try {
+ this.#runInternal(progressCallback, options);
+ } catch (error) {
+ this.#resolveRun({ error });
+ }
- constructor() {
- super(TorConnectState.Bootstrapping);
+ return promise;
}
- async run() {
- if (await this.#simulateCensorship()) {
- return;
+ /**
+ * Run the attempt.
+ *
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ */
+ #runInternal(progressCallback, options) {
+ if (options.simulateCensorship) {
+ // Create a fake request.
+ this.#bootstrap = {
+ _timeout: 0,
+ bootstrap() {
+ this._timeout = setTimeout(() => {
+ const err = new Error("Censorship simulation");
+ err.phase = "conn";
+ err.reason = "noroute";
+ this.onbootstraperror(err);
+ }, options.simulateDelay || 0);
+ },
+ cancel() {
+ clearTimeout(this._timeout);
+ },
+ };
+ } else {
+ this.#bootstrap = new lazy.TorBootstrapRequest();
}
- this.#bootstrap = new lazy.TorBootstrapRequest();
- this.#bootstrap.onbootstrapstatus = (progress, status) => {
- TorConnect._updateBootstrapProgress(progress, status);
+ this.#bootstrap.onbootstrapstatus = (progress, _status) => {
+ if (!this.#resolved) {
+ progressCallback(progress);
+ }
};
this.#bootstrap.onbootstrapcomplete = () => {
- this.#internetTest.cancel();
- this.changeState(TorConnectState.Bootstrapped);
+ this.#resolveRun({ result: "complete" });
};
this.#bootstrap.onbootstraperror = error => {
- if (this.#cancelled) {
- // We ignore this error since it occurred after cancelling (by the
- // user). We assume the error is just a side effect of the cancelling.
- // E.g. If the cancelling is triggered late in the process, we get
- // "Building circuits: Establishing a Tor circuit failed".
- // TODO: Maybe move this logic deeper in the process to know when to
- // filter out such errors triggered by cancelling.
- lazy.logger.warn("Post-cancel error.", error);
+ if (this.#bootstrapError) {
+ lazy.logger.warn("Another bootstrap error", error);
return;
}
// We have to wait for the Internet test to finish before sending the
@@ -320,30 +260,40 @@ class BootstrappingState extends StateCallback {
this.#bootstrapError = error;
this.#maybeTransitionToError();
};
-
- this.#internetTest = new InternetTest();
- this.#internetTest.onResult = status => {
- TorConnect._internetStatus = status;
- this.#maybeTransitionToError();
- };
- this.#internetTest.onError = () => {
- this.#maybeTransitionToError();
- };
+ if (options.testInternet) {
+ this.#internetTest = new InternetTest(options.simulateOffline);
+ this.#internetTest.onResult = () => {
+ this.#maybeTransitionToError();
+ };
+ this.#internetTest.onError = () => {
+ this.#maybeTransitionToError();
+ };
+ }
this.#bootstrap.bootstrap();
}
- async cleanup(nextState) {
- if (nextState === TorConnectState.Configuring) {
- // stop bootstrap process if user cancelled
- this.#cancelled = true;
- this.#internetTest?.cancel();
- await this.#bootstrap?.cancel();
+ /**
+ * Callback for when we get a new bootstrap error or a change in the internet
+ * status.
+ */
+ #maybeTransitionToError() {
+ if (this.#resolved || this.#cancelled) {
+ if (this.#bootstrapError) {
+ // We ignore this error since it occurred after cancelling (by the
+ // user), or we have already resolved. We assume the error is just a
+ // side effect of the cancelling.
+ // E.g. If the cancelling is triggered late in the process, we get
+ // "Building circuits: Establishing a Tor circuit failed".
+ // TODO: Maybe move this logic deeper in the process to know when to
+ // filter out such errors triggered by cancelling.
+ lazy.logger.warn("Post-complete error.", this.#bootstrapError);
+ }
+ return;
}
- }
- #maybeTransitionToError() {
if (
+ this.#internetTest &&
this.#internetTest.status === InternetStatus.Unknown &&
this.#internetTest.error === null &&
this.#internetTest.enabled
@@ -355,356 +305,394 @@ class BootstrappingState extends StateCallback {
// us again.
return;
}
- // Do not transition to the offline error until we are sure that also the
- // bootstrap failed, in case Moat is down but the bootstrap can proceed
- // anyway.
+ // Do not transition to "offline" until we are sure that also the bootstrap
+ // failed, in case Moat is down but the bootstrap can proceed anyway.
if (!this.#bootstrapError) {
return;
}
- if (this.#internetTest.status === InternetStatus.Offline) {
- this.changeState(
- TorConnectState.Error,
- new TorConnectError(TorConnectError.Offline)
- );
- } else {
- // Give priority to the bootstrap error, in case the Internet test fails
- TorConnect._hasBootstrapEverFailed = true;
- this.changeState(
- TorConnectState.Error,
- new TorConnectError(
- TorConnectError.BootstrapError,
+ if (this.#internetTest?.status === InternetStatus.Offline) {
+ if (this.#bootstrapError) {
+ lazy.logger.info(
+ "Ignoring bootstrap error since offline.",
this.#bootstrapError
- )
- );
- }
- }
-
- async #simulateCensorship() {
- // debug hook to simulate censorship preventing bootstrapping
- const censorshipLevel = Services.prefs.getIntPref(
- TorConnectPrefs.censorship_level,
- 0
- );
- if (censorshipLevel <= 0) {
- return false;
- }
-
- await debugSleep(1500);
- if (this.transitioning) {
- // Already left this state.
- return true;
+ );
+ }
+ this.#resolveRun({ result: "offline" });
+ return;
}
- TorConnect._hasBootstrapEverFailed = true;
- if (censorshipLevel === 2) {
- const codes = Object.keys(TorConnect._countryNames);
- TorConnect._detectedLocation =
- codes[Math.floor(Math.random() * codes.length)];
- }
- const err = new Error("Censorship simulation");
- err.phase = "conn";
- err.reason = "noroute";
- this.changeState(
- TorConnectState.Error,
- new TorConnectError(TorConnectError.BootstrapError, err)
- );
- return true;
- }
-}
-
-class AutoBootstrappingState extends StateCallback {
- #moat;
- #settings;
- #changedSettings = false;
- #transitionPromise;
- #transitionResolve;
-
- allowedTransitions = Object.freeze([
- TorConnectState.Configuring,
- TorConnectState.Bootstrapped,
- TorConnectState.Error,
- ]);
-
- constructor() {
- super(TorConnectState.AutoBootstrapping);
- this.#transitionPromise = new Promise(resolve => {
- this.#transitionResolve = resolve;
+ this.#resolveRun({
+ error: new TorConnectError(
+ TorConnectError.BootstrapError,
+ this.#bootstrapError
+ ),
});
}
- async run(countryCode) {
- if (await this.#simulateCensorship(countryCode)) {
- return;
- }
- await this.#initMoat();
- if (this.transitioning) {
+ /**
+ * Cancel the bootstrap attempt.
+ */
+ async cancel() {
+ if (this.#cancelled) {
+ lazy.logger.warn(
+ "Cancelled bootstrap after it has already been cancelled"
+ );
return;
}
- await this.#fetchSettings(countryCode);
- if (this.transitioning) {
+ this.#cancelled = true;
+ if (this.#resolved) {
+ lazy.logger.warn("Cancelled bootstrap after it has already resolved");
return;
}
- await this.#trySettings();
+ // Wait until after bootstrap.cancel returns before we resolve with
+ // cancelled. In particular, there is a small chance that the bootstrap
+ // completes, in which case we want to be able to resolve with a success
+ // instead.
+ this.#internetTest?.cancel();
+ await this.#bootstrap?.cancel();
+ this.#resolveRun({ result: "cancelled" });
}
+}
+/**
+ * Each instance can be used to attempt one auto-bootstrapping sequence.
+ */
+class AutoBootstrapAttempt {
/**
- * Simulate a censorship event, if needed.
+ * The current bootstrap attempt, if any.
*
- * @param {string} countryCode The country code passed to the state
- * @returns {Promise<boolean>} true if we are simulating the censorship and
- * the bootstrap should stop immediately, or false if the bootstrap should
- * continue normally.
+ * @type {?BootstrapAttempt}
*/
- async #simulateCensorship(countryCode) {
- const censorshipLevel = Services.prefs.getIntPref(
- TorConnectPrefs.censorship_level,
- 0
- );
- if (censorshipLevel <= 0) {
- return false;
- }
+ #bootstrapAttempt = null;
+ /**
+ * The method to call to complete the `run` promise.
+ *
+ * @type {?ResolveBootstrap}
+ */
+ #resolveRun = null;
+ /**
+ * Whether the `run` promise has been, or is about to be, resolved.
+ *
+ * @type {boolean}
+ */
+ #resolved = false;
+ /**
+ * Whether a cancel request has been started.
+ *
+ * @type {boolean}
+ */
+ #cancelled = false;
+ /**
+ * The method to call when the cancelled value is set to true.
+ *
+ * @type {?Function}
+ */
+ #resolveCancelled = null;
+ /**
+ * A promise that resolves when the cancelled value is set to true. We can use
+ * this with Promise.race to end early when the user cancels.
+ *
+ * @type {?Promise}
+ */
+ #cancelledPromise = null;
+ /**
+ * The found settings from Moat.
+ *
+ * @type {?object[]}
+ */
+ #settings = null;
+ /**
+ * The last settings that have been applied to the TorProvider, if any.
+ *
+ * @type {?object}
+ */
+ #changedSetting = null;
+ /**
+ * The detected region code returned by Moat, if any.
+ *
+ * @type {?string}
+ */
+ detectedRegion = null;
- // Very severe censorship: always fail even after manually selecting
- // location specific settings.
- if (censorshipLevel === 3) {
- await debugSleep(2500);
- if (!this.transitioning) {
- this.changeState(
- TorConnectState.Error,
- new TorConnectError(TorConnectError.AllSettingsFailed)
- );
+ /**
+ * Run an auto-bootstrap attempt.
+ *
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ *
+ * @return {Promise<string, Error>} - The result of the bootstrap.
+ */
+ run(progressCallback, options) {
+ const { promise, resolve, reject } = Promise.withResolvers();
+
+ this.#resolveRun = async arg => {
+ if (this.#resolved) {
+ // Already been called once.
+ if (arg.error) {
+ lazy.logger.error("Delayed auto-bootstrap error", arg.error);
+ }
+ return;
}
- return true;
- }
-
- // Severe censorship: only fail after auto selecting, but succeed after
- // manually selecting a country.
- if (censorshipLevel === 2 && !countryCode) {
- await debugSleep(2500);
- if (!this.transitioning) {
- this.changeState(
- TorConnectState.Error,
- new TorConnectError(TorConnectError.CannotDetermineCountry)
- );
+ this.#resolved = true;
+ try {
+ // Run cleanup before we resolve the promise to ensure two instances
+ // of AutoBootstrapAttempt are not trying to change the settings at
+ // the same time.
+ if (this.#changedSetting) {
+ if (arg.result === "complete") {
+ // Persist the current settings to preferences.
+ lazy.TorSettings.setSettings(this.#changedSetting);
+ lazy.TorSettings.saveToPrefs();
+ } // else, applySettings will restore the current settings.
+ await lazy.TorSettings.applySettings();
+ }
+ } catch (error) {
+ lazy.logger.error("Unexpected error in auto-bootstrap cleanup", error);
}
- return true;
- }
+ if (arg.error) {
+ reject(arg.error);
+ } else {
+ resolve(arg.result);
+ }
+ };
- return false;
- }
+ ({ promise: this.#cancelledPromise, resolve: this.#resolveCancelled } =
+ Promise.withResolvers());
- /**
- * Initialize the MoatRPC to communicate with the backend.
- */
- async #initMoat() {
- this.#moat = new lazy.MoatRPC();
- // We need to wait Moat's initialization even when we are requested to
- // transition to another state to be sure its uninit will have its intended
- // effect. So, do not use Promise.race here.
- await this.#moat.init();
+ this.#runInternal(progressCallback, options).catch(error => {
+ this.#resolveRun({ error });
+ });
+
+ return promise;
}
/**
- * Lookup user's potential censorship circumvention settings from Moat
- * service.
+ * Run the attempt.
+ *
+ * Note, this is an async method, but should *not* be awaited by the `run`
+ * method.
+ *
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
*/
- async #fetchSettings(countryCode) {
- // For now, throw any errors we receive from the backend, except when it was
- // unable to detect user's country/region.
- // If we use specialized error objects, we could pass the original errors to
- // them.
- const maybeSettings = await Promise.race([
- this.#moat.circumvention_settings(
- [...lazy.TorSettings.builtinBridgeTypes, "vanilla"],
- countryCode
- ),
- // This might set maybeSettings to undefined.
- this.#transitionPromise,
- ]);
- if (maybeSettings?.country) {
- TorConnect._detectedLocation = maybeSettings.country;
- }
-
- if (maybeSettings?.settings && maybeSettings.settings.length) {
- this.#settings = maybeSettings.settings;
- } else if (!this.transitioning) {
- // Keep consistency with the other call.
- this.#settings = await Promise.race([
- this.#moat.circumvention_defaults([
- ...lazy.TorSettings.builtinBridgeTypes,
- "vanilla",
- ]),
- // This might set this.#settings to undefined.
- this.#transitionPromise,
- ]);
+ async #runInternal(progressCallback, options) {
+ await this.#fetchSettings(options);
+ if (this.#cancelled || this.#resolved) {
+ return;
}
- if (!this.#settings?.length && !this.transitioning) {
- if (!TorConnect._detectedLocation) {
- // unable to determine country
- throw new TorConnectError(TorConnectError.CannotDetermineCountry);
- } else {
- // no settings available for country
- throw new TorConnectError(TorConnectError.NoSettingsForCountry);
- }
+ if (!this.#settings?.length) {
+ this.#resolveRun({
+ error: new TorConnectError(
+ options.regionCode === "automatic" && !this.detectedRegion
+ ? TorConnectError.CannotDetermineCountry
+ : TorConnectError.NoSettingsForCountry
+ ),
+ });
}
- }
- /**
- * Try to apply the settings we fetched.
- */
- async #trySettings() {
- // Otherwise, apply each of our settings and try to bootstrap with each.
+ // Apply each of our settings and try to bootstrap with each.
for (const [index, currentSetting] of this.#settings.entries()) {
- if (this.transitioning) {
- break;
- }
-
lazy.logger.info(
`Attempting Bootstrap with configuration ${index + 1}/${
this.#settings.length
}`
);
- // Send the new settings directly to the provider. We will save them only
- // if the bootstrap succeeds.
- // FIXME: We should somehow signal TorSettings users that we have set
- // custom settings, and they should not apply theirs until we are done
- // with trying ours.
- // Otherwise, the new settings provided by the user while we were
- // bootstrapping could be the ones that cause the bootstrap to succeed,
- // but we overwrite them (unless we backup the original settings, and then
- // save our new settings only if they have not changed).
- // Another idea (maybe easier to implement) is to disable the settings
- // UI while *any* bootstrap is going on.
- // This is also documented in tor-browser#41921.
- const provider = await lazy.TorProviderBuilder.build();
- this.#changedSettings = true;
- // We need to merge with old settings, in case the user is using a proxy
- // or is behind a firewall.
- await provider.writeSettings({
- ...lazy.TorSettings.getSettings(),
- ...currentSetting,
- });
-
- // Build out our bootstrap request.
- const bootstrap = new lazy.TorBootstrapRequest();
- bootstrap.onbootstrapstatus = (progress, status) => {
- TorConnect._updateBootstrapProgress(progress, status);
- };
- bootstrap.onbootstraperror = error => {
- lazy.logger.error("Auto-Bootstrap error", error);
- };
+ await this.#trySetting(currentSetting, progressCallback, options);
- // Begin the bootstrap.
- const success = await Promise.race([
- bootstrap.bootstrap(),
- this.#transitionPromise,
- ]);
- // Either the bootstrap request has finished, or a transition (caused by
- // an error or by user's cancelation) started.
- // However, we cannot be already transitioning in case of success, so if
- // we are we should cancel the current bootstrap.
- // With the current TorProvider, this will set DisableNetwork=1 again,
- // which is what the user wanted if they canceled.
- if (this.transitioning) {
- if (success) {
- lazy.logger.warn(
- "We were already transitioning after a success, we were not expecting this."
- );
- }
- bootstrap.cancel();
- return;
- }
- if (success) {
- // Persist the current settings to preferences.
- lazy.TorSettings.setSettings(currentSetting);
- lazy.TorSettings.saveToPrefs();
- // Do not await `applySettings`. Otherwise this opens up a window of
- // time where the user can still "Cancel" the bootstrap.
- // We are calling `applySettings` just to be on the safe side, but the
- // settings we are passing now should be exactly the same we already
- // passed earlier.
- lazy.TorSettings.applySettings().catch(e =>
- lazy.logger.error("TorSettings.applySettings threw unexpectedly.", e)
- );
- this.changeState(TorConnectState.Bootstrapped);
+ if (this.#cancelled || this.#resolved) {
return;
}
}
- // Only explicitly change state here if something else has not transitioned
- // us.
- if (!this.transitioning) {
- throw new TorConnectError(TorConnectError.AllSettingsFailed);
- }
- }
-
- transitionRequested() {
- this.#transitionResolve();
+ this.#resolveRun({
+ error: new TorConnectError(TorConnectError.AllSettingsFailed),
+ });
}
- async cleanup(nextState) {
- // No need to await.
- this.#moat?.uninit();
- this.#moat = null;
+ /**
+ * Lookup user's potential censorship circumvention settings from Moat
+ * service.
+ *
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ */
+ async #fetchSettings(options) {
+ if (options.simulateMoatResponse) {
+ await Promise.race([
+ new Promise(res => setTimeout(res, options.simulateDelay || 0)),
+ this.#cancelledPromise,
+ ]);
- if (this.#changedSettings && nextState !== TorConnectState.Bootstrapped) {
- try {
- await lazy.TorSettings.applySettings();
- } catch (e) {
- // We cannot do much if the original settings were bad or
- // if the connection closed, so just report it in the
- // console.
- lazy.logger.warn("Failed to restore original settings.", e);
+ if (this.#cancelled || this.#resolved) {
+ return;
}
+
+ this.detectedRegion = options.simulateMoatResponse.country || null;
+ this.#settings = options.simulateMoatResponse.settings ?? null;
+
+ return;
}
- }
-}
-class BootstrappedState extends StateCallback {
- // We may need to leave the bootstrapped state if the tor daemon
- // exits (if it is restarted, we will have to bootstrap again).
- allowedTransitions = Object.freeze([TorConnectState.Configuring]);
+ const moat = new lazy.MoatRPC();
+ try {
+ // We need to wait Moat's initialization even when we are requested to
+ // transition to another state to be sure its uninit will have its
+ // intended effect. So, do not use Promise.race here.
+ await moat.init();
- constructor() {
- super(TorConnectState.Bootstrapped);
- }
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
- run() {
- // Notify observers of bootstrap completion.
- Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete);
+ // For now, throw any errors we receive from the backend, except when it
+ // was unable to detect user's country/region.
+ // If we use specialized error objects, we could pass the original errors
+ // to them.
+ const maybeSettings = await Promise.race([
+ moat.circumvention_settings(
+ [...lazy.TorSettings.builtinBridgeTypes, "vanilla"],
+ options.regionCode === "automatic" ? null : options.regionCode
+ ),
+ // This might set maybeSettings to undefined.
+ this.#cancelledPromise,
+ ]);
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
+
+ this.detectedRegion = maybeSettings?.country || null;
+
+ if (maybeSettings?.settings?.length) {
+ this.#settings = maybeSettings.settings;
+ } else {
+ // Keep consistency with the other call.
+ this.#settings = await Promise.race([
+ moat.circumvention_defaults([
+ ...lazy.TorSettings.builtinBridgeTypes,
+ "vanilla",
+ ]),
+ // This might set this.#settings to undefined.
+ this.#cancelledPromise,
+ ]);
+ }
+ } finally {
+ // Do not await the uninit.
+ moat.uninit();
+ }
}
-}
-class ErrorState extends StateCallback {
- allowedTransitions = Object.freeze([TorConnectState.Configuring]);
+ /**
+ * Try to apply the settings we fetched.
+ *
+ * @param {object} setting - The setting to try.
+ * @param {ProgressCallback} progressCallback - The callback to invoke with
+ * the bootstrap progress.
+ * @param {BootstrapOptions} options - Options to apply to the bootstrap.
+ */
+ async #trySetting(setting, progressCallback, options) {
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
- static #hasEverHappened = false;
+ if (options.simulateMoatResponse && setting.simulateCensorship) {
+ // Move the simulateCensorship option to the options for the next
+ // BootstrapAttempt.
+ setting = structuredClone(setting);
+ delete setting.simulateCensorship;
+ options = { ...options, simulateCensorship: true };
+ }
- constructor() {
- super(TorConnectState.Error);
- ErrorState.#hasEverHappened = true;
- }
+ // Send the new settings directly to the provider. We will save them only
+ // if the bootstrap succeeds.
+ // FIXME: We should somehow signal TorSettings users that we have set
+ // custom settings, and they should not apply theirs until we are done
+ // with trying ours.
+ // Otherwise, the new settings provided by the user while we were
+ // bootstrapping could be the ones that cause the bootstrap to succeed,
+ // but we overwrite them (unless we backup the original settings, and then
+ // save our new settings only if they have not changed).
+ // Another idea (maybe easier to implement) is to disable the settings
+ // UI while *any* bootstrap is going on.
+ // This is also documented in tor-browser#41921.
+ const provider = await lazy.TorProviderBuilder.build();
+ this.#changedSetting = setting;
+ // We need to merge with old settings, in case the user is using a proxy
+ // or is behind a firewall.
+ await provider.writeSettings({
+ ...lazy.TorSettings.getSettings(),
+ ...setting,
+ });
- run(_error) {
- this.changeState(TorConnectState.Configuring);
- }
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
- static get hasEverHappened() {
- return ErrorState.#hasEverHappened;
- }
-}
+ let result;
+ try {
+ this.#bootstrapAttempt = new BootstrapAttempt();
+ // At this stage, cancelling AutoBootstrap will also cancel this
+ // bootstrapAttempt.
+ result = await this.#bootstrapAttempt.run(progressCallback, options);
+ } catch (error) {
+ // Only re-try with the next settings *if* we have a BootstrapError.
+ // Other errors will end this auto-bootstrap attempt entirely.
+ if (
+ error instanceof TorConnectError &&
+ error.code === TorConnectError.BootstrapError
+ ) {
+ lazy.logger.info("TorConnect setting failed", setting, error);
+ // Try with the next settings.
+ // NOTE: We do not restore the user settings in between these runs.
+ // Instead we wait for #resolveRun callback to do so.
+ // This means there is a window of time where the setting is applied, but
+ // no bootstrap is running.
+ return;
+ }
+ // Pass error up.
+ throw error;
+ } finally {
+ this.#bootstrapAttempt = null;
+ }
-class DisabledState extends StateCallback {
- // Trap state: no way to leave the Disabled state.
- allowedTransitions = Object.freeze([]);
+ if (this.#cancelled || this.#resolved) {
+ return;
+ }
- constructor() {
- super(TorConnectState.Disabled);
+ // Pass the BootstrapAttempt result up.
+ this.#resolveRun({ result });
}
- async run() {
- lazy.logger.debug("Entered the disabled state.");
+ /**
+ * Cancel the bootstrap attempt.
+ */
+ async cancel() {
+ if (this.#cancelled) {
+ lazy.logger.warn(
+ "Cancelled auto-bootstrap after it has already been cancelled"
+ );
+ return;
+ }
+ this.#cancelled = true;
+ this.#resolveCancelled();
+ if (this.#resolved) {
+ lazy.logger.warn(
+ "Cancelled auto-bootstrap after it has already resolved"
+ );
+ return;
+ }
+
+ // Wait until after bootstrap.cancel returns before we resolve with
+ // cancelled. In particular, there is a small chance that the bootstrap
+ // completes, in which case we want to be able to resolve with a success
+ // instead.
+ if (this.#bootstrapAttempt) {
+ this.#bootstrapAttempt.cancel();
+ await this.#bootstrapAttempt;
+ }
+ // In case no bootstrap is running, we resolve with "cancelled".
+ this.#resolveRun({ result: "cancelled" });
}
}
@@ -721,8 +709,11 @@ class InternetTest {
#pending = false;
#canceled = false;
#timeout = 0;
+ #simulateOffline = false;
+
+ constructor(simulateOffline) {
+ this.#simulateOffline = simulateOffline;
- constructor() {
this.#enabled = Services.prefs.getBoolPref(
TorConnectPrefs.allow_internet_test,
true
@@ -752,6 +743,19 @@ class InternetTest {
this.#canceled = false;
lazy.logger.info("Starting the Internet test");
+
+ if (this.#simulateOffline) {
+ await new Promise(res => setTimeout(res, 500));
+
+ this.#status = InternetStatus.Offline;
+
+ if (this.#canceled) {
+ return;
+ }
+ this.onResult(this.#status);
+ return;
+ }
+
const mrpc = new lazy.MoatRPC();
try {
await mrpc.init();
@@ -792,27 +796,173 @@ class InternetTest {
return this.#status;
}
- get error() {
- return this.#error;
- }
+ get error() {
+ return this.#error;
+ }
+
+ get enabled() {
+ return this.#enabled;
+ }
+
+ // We randomize the Internet test timeout to make fingerprinting it harder, at
+ // least a little bit...
+ #timeoutRand() {
+ const offset = 30000;
+ const randRange = 5000;
+ return offset + randRange * (Math.random() * 2 - 1);
+ }
+}
+
+export const TorConnectStage = Object.freeze({
+ Disabled: "Disabled",
+ Loading: "Loading",
+ Start: "Start",
+ Bootstrapping: "Bootstrapping",
+ Offline: "Offline",
+ ChooseRegion: "ChooseRegion",
+ RegionNotFound: "RegionNotFound",
+ ConfirmRegion: "ConfirmRegion",
+ FinalError: "FinalError",
+ Bootstrapped: "Bootstrapped",
+});
+
+/**
+ * @typedef {object} ConnectStage
+ *
+ * A summary of the user stage.
+ *
+ * @property {string} name - The name of the stage.
+ * @property {string} defaultRegion - The default region to show in the UI.
+ * @property {?string} bootstrapTrigger - The TorConnectStage prior to this
+ * bootstrap attempt. Only set during the "Bootstrapping" stage.
+ * @property {?BootstrapError} error - The last bootstrapping error.
+ * @property {boolean} tryAgain - Whether a bootstrap attempt has failed, so
+ * that a normal bootstrap should be shown as "Try Again" instead of
+ * "Connect". NOTE: to be removed when about:torconnect no longer uses
+ * breadcrumbs.
+ * @property {boolean} potentiallyBlocked - Whether bootstrapping has ever
+ * failed, not including being cancelled or being offline. I.e. whether we
+ * have reached an error stage at some point before being bootstrapped.
+ * @property {BootstrappingStatus} bootstrappingStatus - The current
+ * bootstrapping status.
+ */
+
+/**
+ * @typedef {object} BootstrappingStatus
+ *
+ * The status of a bootstrap.
+ *
+ * @property {number} progress - The percent progress.
+ * @property {boolean} hasWarning - Whether this bootstrap has a warning in the
+ * Tor log.
+ */
+
+/**
+ * @typedef {object} BootstrapError
+ *
+ * Details about the error that caused bootstrapping to fail.
+ *
+ * @property {string} code - The error code type.
+ * @property {string} message - The error message.
+ * @property {?string} phase - The bootstrapping phase that failed.
+ * @property {?string} reason - The bootstrapping failure reason.
+ */
+
+export const TorConnect = {
+ /**
+ * Default bootstrap options for simulation.
+ *
+ * @type {BootstrapOptions}
+ */
+ simulateBootstrapOptions: {},
+
+ /**
+ * The name of the current stage the user is in.
+ *
+ * @type {string}
+ */
+ _stageName: TorConnectStage.Loading,
+
+ get stageName() {
+ return this._stageName;
+ },
+
+ /**
+ * The stage that triggered bootstrapping.
+ *
+ * @type {?string}
+ */
+ _bootstrapTrigger: null,
+
+ /**
+ * The alternative stage that we should move to after bootstrapping completes.
+ *
+ * @type {?string}
+ */
+ _requestedStage: null,
+
+ /**
+ * The default region to show in the UI for auto-bootstrapping.
+ *
+ * @type {string}
+ */
+ _defaultRegion: "automatic",
+
+ /**
+ * The current bootstrap attempt, if any.
+ *
+ * @type {?(BootstrapAttempt|AutoBootstrapAttempt)}
+ */
+ _bootstrapAttempt: null,
+
+ /**
+ * The bootstrap error that was last generated.
+ *
+ * @type {?TorConnectError}
+ */
+ _errorDetails: null,
+
+ /**
+ * Whether a bootstrap attempt has failed, so that a normal bootstrap should
+ * be shown as "Try Again" instead of "Connect".
+ *
+ * @type {boolean}
+ */
+ // TODO: Drop tryAgain when we remove breadcrumbs and use "Start again"
+ // instead.
+ _tryAgain: false,
- get enabled() {
- return this.#enabled;
- }
+ /**
+ * Whether bootstrapping has ever returned an error.
+ *
+ * @type {boolean}
+ */
+ _potentiallyBlocked: false,
- // We randomize the Internet test timeout to make fingerprinting it harder, at
- // least a little bit...
- #timeoutRand() {
- const offset = 30000;
- const randRange = 5000;
- return offset + randRange * (Math.random() * 2 - 1);
- }
-}
+ /**
+ * Get a summary of the current user stage.
+ *
+ * @type {ConnectStage}
+ */
+ get stage() {
+ return {
+ name: this._stageName,
+ defaultRegion: this._defaultRegion,
+ bootstrapTrigger: this._bootstrapTrigger,
+ error: this._errorDetails
+ ? {
+ code: this._errorDetails.code,
+ message: String(this._errorDetails.message ?? ""),
+ phase: this._errorDetails.cause?.phase ?? null,
+ reason: this._errorDetails.cause?.reason ?? null,
+ }
+ : null,
+ tryAgain: this._tryAgain,
+ potentiallyBlocked: this._potentiallyBlocked,
+ bootstrappingStatus: structuredClone(this._bootstrappingStatus),
+ };
+ },
-export const TorConnect = {
- _stateHandler: new InitialState(),
- _bootstrapProgress: 0,
- _internetStatus: InternetStatus.Unknown,
// list of country codes Moat has settings for
_countryCodes: [],
_countryNames: Object.freeze(
@@ -826,109 +976,28 @@ export const TorConnect = {
return codesNames;
})()
),
- _detectedLocation: "",
- _errorCode: null,
- _errorDetails: null,
- _logHasWarningOrError: false,
- _hasBootstrapEverFailed: false,
- _transitionPromise: null,
// This is used as a helper to make the state of about:torconnect persistent
// during a session, but TorConnect does not use this data at all.
_uiState: {},
- _stateCallbacks: Object.freeze(
- new Map([
- // Initial is never transitioned to
- [TorConnectState.Initial, InitialState],
- [TorConnectState.Configuring, ConfiguringState],
- [TorConnectState.Bootstrapping, BootstrappingState],
- [TorConnectState.AutoBootstrapping, AutoBootstrappingState],
- [TorConnectState.Bootstrapped, BootstrappedState],
- [TorConnectState.Error, ErrorState],
- [TorConnectState.Disabled, DisabledState],
- ])
- ),
-
- _makeState(state) {
- const klass = this._stateCallbacks.get(state);
- if (!klass) {
- throw new Error(`${state} is not a valid state.`);
- }
- return new klass();
- },
-
- async _changeState(newState, ...args) {
- if (this._stateHandler.transitioning) {
- // Avoid an exception to prevent it to be propagated to the original
- // begin call.
- lazy.logger.warn("Already transitioning");
- return;
- }
- const prevState = this._stateHandler;
-
- // ensure this is a valid state transition
- if (!prevState.allowedTransitions.includes(newState)) {
- throw Error(
- `TorConnect: Attempted invalid state transition from ${prevState.state} to ${newState}`
- );
- }
-
- lazy.logger.trace(
- `Try transitioning from ${prevState.state} to ${newState}`,
- args
- );
- try {
- await prevState.end(newState);
- } catch (e) {
- // We take for granted that the begin of this state will call us again,
- // to request the transition to the error state.
- if (newState !== TorConnectState.Error) {
- lazy.logger.debug(
- `Refusing the transition from ${prevState.state} to ${newState} because the previous state threw.`
- );
- return;
- }
- }
-
- // Set our new state first so that state transitions can themselves
- // trigger a state transition.
- this._stateHandler = this._makeState(newState);
-
- // Error signal needs to be sent out before we enter the Error state.
- // Expected on android `onBootstrapError` to set lastKnownError.
- // Expected in about:torconnect to set the error codes and internet status
- // *before* the StateChange signal.
- if (newState === TorConnectState.Error) {
- let error = args[0];
- if (!(error instanceof TorConnectError)) {
- error = new TorConnectError(TorConnectError.ExternalError, error);
- }
- TorConnect._errorCode = error.code;
- TorConnect._errorDetails = error;
- lazy.logger.error(`Entering error state (${error.code})`, error);
-
- Services.obs.notifyObservers(error, TorConnectTopics.Error);
- }
-
- Services.obs.notifyObservers(
- { state: newState },
- TorConnectTopics.StateChange
- );
- this._stateHandler.begin(...args);
+ /**
+ * The status of the most recent bootstrap attempt.
+ *
+ * @type {BootstrappingStatus}
+ */
+ _bootstrappingStatus: {
+ progress: 0,
+ hasWarning: false,
},
- _updateBootstrapProgress(progress, status) {
- this._bootstrapProgress = progress;
-
- lazy.logger.info(
- `Bootstrapping ${this._bootstrapProgress}% complete (${status})`
- );
+ /**
+ * Notify the bootstrap progress.
+ */
+ _notifyBootstrapProgress() {
+ lazy.logger.debug("BootstrappingStatus", this._bootstrappingStatus);
Services.obs.notifyObservers(
- {
- progress: TorConnect._bootstrapProgress,
- hasWarnings: TorConnect._logHasWarningOrError,
- },
+ this._bootstrappingStatus,
TorConnectTopics.BootstrapProgress
);
},
@@ -936,62 +1005,54 @@ export const TorConnect = {
// init should be called by TorStartupService
init() {
lazy.logger.debug("TorConnect.init()");
- this._stateHandler.begin();
if (!this.enabled) {
// Disabled
- this._changeState(TorConnectState.Disabled);
- } else {
- let observeTopic = addTopic => {
- Services.obs.addObserver(this, addTopic);
- lazy.logger.debug(`Observing topic '${addTopic}'`);
- };
+ this._setStage(TorConnectStage.Disabled);
+ return;
+ }
- // Wait for TorSettings, as we will need it.
- // We will wait for a TorProvider only after TorSettings is ready,
- // because the TorProviderBuilder initialization might not have finished
- // at this point, and TorSettings initialization is a prerequisite for
- // having a provider.
- // So, we prefer initializing TorConnect as soon as possible, so that
- // the UI will be able to detect it is in the Initializing state and act
- // consequently.
- lazy.TorSettings.initializedPromise.then(() =>
- this._settingsInitialized()
- );
+ let observeTopic = addTopic => {
+ Services.obs.addObserver(this, addTopic);
+ lazy.logger.debug(`Observing topic '${addTopic}'`);
+ };
- // register the Tor topics we always care about
- observeTopic(lazy.TorProviderTopics.ProcessExited);
- observeTopic(lazy.TorProviderTopics.HasWarnOrErr);
- }
+ // Wait for TorSettings, as we will need it.
+ // We will wait for a TorProvider only after TorSettings is ready,
+ // because the TorProviderBuilder initialization might not have finished
+ // at this point, and TorSettings initialization is a prerequisite for
+ // having a provider.
+ // So, we prefer initializing TorConnect as soon as possible, so that
+ // the UI will be able to detect it is in the Initializing state and act
+ // consequently.
+ lazy.TorSettings.initializedPromise.then(() => this._settingsInitialized());
+
+ // register the Tor topics we always care about
+ observeTopic(lazy.TorProviderTopics.ProcessExited);
+ observeTopic(lazy.TorProviderTopics.HasWarnOrErr);
},
async observe(subject, topic) {
lazy.logger.debug(`Observed ${topic}`);
switch (topic) {
- case lazy.TorProviderTopics.HasWarnOrErr: {
- this._logHasWarningOrError = true;
+ case lazy.TorProviderTopics.HasWarnOrErr:
+ if (this._bootstrappingStatus.hasWarning) {
+ // No change.
+ return;
+ }
+ if (this._stageName === "Bootstrapping") {
+ this._bootstrappingStatus.hasWarning = true;
+ this._notifyBootstrapProgress();
+ }
break;
- }
- case lazy.TorProviderTopics.ProcessExited: {
+ case lazy.TorProviderTopics.ProcessExited:
+ lazy.logger.info("Starting again since the tor process exited");
// Treat a failure as a possibly broken configuration.
// So, prevent quickstart at the next start.
Services.prefs.setBoolPref(TorLauncherPrefs.prompt_at_startup, true);
- switch (this.state) {
- case TorConnectState.Bootstrapping:
- case TorConnectState.AutoBootstrapping:
- case TorConnectState.Bootstrapped:
- // If we are in the bootstrap or auto bootstrap, we could go
- // through the error phase (and eventually we might do it, if some
- // transition calls fail). However, this would start the
- // connection assist, so we go directly to configuring.
- // FIXME: Find a better way to handle this.
- this._changeState(TorConnectState.Configuring);
- break;
- // Other states naturally resolve in configuration.
- }
+ this._makeStageRequest(TorConnectStage.Start, true);
break;
- }
default:
// ignore
break;
@@ -1003,29 +1064,47 @@ export const TorConnect = {
// daemon when it exits (tor-browser#21053, tor-browser#41921).
await lazy.TorProviderBuilder.build();
- // tor-browser#41907: This is only a workaround to avoid users being
- // bounced back to the initial panel without any explanation.
- // Longer term we should disable the clickable elements, or find a UX
- // to prevent this from happening (e.g., allow buttons to be clicked,
- // but show an intermediate starting state, or a message that tor is
- // starting while the butons are disabled, etc...).
- // Notice that currently the initial state does not do anything.
- // Instead of just waiting, we could move this code in its callback.
- // See also tor-browser#41921.
- if (this.state !== TorConnectState.Initial) {
- lazy.logger.warn(
- "The TorProvider was built after the state had already changed."
- );
- return;
- }
lazy.logger.debug("The TorProvider is ready, changing state.");
+ // NOTE: If the tor process exits before this point, then
+ // shouldQuickStart would be `false`.
+ // NOTE: At this point, _requestedStage should still be `null`.
+ this._setStage(TorConnectStage.Start);
if (this.shouldQuickStart) {
// Quickstart
- this._changeState(TorConnectState.Bootstrapping);
- } else {
- // Configuring
- this._changeState(TorConnectState.Configuring);
+ this.beginBootstrapping();
+ }
+ },
+
+ /**
+ * Set the user stage.
+ *
+ * @param {string} name - The name of the stage to move to.
+ */
+ _setStage(name) {
+ if (this._bootstrapAttempt) {
+ throw new Error(`Trying to set the stage to ${name} during a bootstrap`);
+ }
+
+ lazy.logger.info(`Entering stage ${name}`);
+ const prevState = this.state;
+ this._stageName = name;
+ this._bootstrappingStatus.hasWarning = false;
+ this._bootstrappingStatus.progress =
+ name === TorConnectStage.Bootstrapped ? 100 : 0;
+
+ Services.obs.notifyObservers(this.stage, TorConnectTopics.StageChange);
+
+ // TODO: Remove when all pages have switched to stage.
+ const newState = this.state;
+ if (prevState !== newState) {
+ Services.obs.notifyObservers(
+ { state: newState },
+ TorConnectTopics.StateChange
+ );
}
+
+ // Update the progress after the stage has changed.
+ this._notifyBootstrapProgress();
},
/*
@@ -1049,33 +1128,41 @@ export const TorConnect = {
return (
this.enabled &&
// if we have succesfully bootstraped, then no need to show TorConnect
- this.state !== TorConnectState.Bootstrapped
+ this._stageName !== TorConnectStage.Bootstrapped
);
},
/**
- * Whether bootstrapping can currently begin.
+ * Whether we are in a stage that can lead into the Bootstrapping stage. I.e.
+ * whether we can make a "normal" or "auto" bootstrapping request.
*
- * The value may change with TorConnectTopics.StateChanged.
+ * The value may change with TorConnectTopics.StageChanged.
*
* @param {boolean}
*/
get canBeginBootstrap() {
- return this._stateHandler.allowedTransitions.includes(
- TorConnectState.Bootstrapping
+ return (
+ this._stageName === TorConnectStage.Start ||
+ this._stageName === TorConnectStage.Offline ||
+ this._stageName === TorConnectStage.ChooseRegion ||
+ this._stageName === TorConnectStage.RegionNotFound ||
+ this._stageName === TorConnectStage.ConfirmRegion
);
},
/**
- * Whether auto-bootstrapping can currently begin.
+ * Whether we are in an error stage that can lead into the Bootstrapping
+ * stage. I.e. whether we can make an "auto" bootstrapping request.
*
- * The value may change with TorConnectTopics.StateChanged.
+ * The value may change with TorConnectTopics.StageChanged.
*
* @param {boolean}
*/
get canBeginAutoBootstrap() {
- return this._stateHandler.allowedTransitions.includes(
- TorConnectState.AutoBootstrapping
+ return (
+ this._stageName === TorConnectStage.ChooseRegion ||
+ this._stageName === TorConnectStage.RegionNotFound ||
+ this._stageName === TorConnectStage.ConfirmRegion
);
},
@@ -1088,16 +1175,39 @@ export const TorConnect = {
);
},
+ // TODO: Remove when all pages have switched to "stage".
get state() {
- return this._stateHandler.state;
- },
-
- get bootstrapProgress() {
- return this._bootstrapProgress;
- },
-
- get internetStatus() {
- return this._internetStatus;
+ // There is no "Error" stage, but about:torconnect relies on receiving the
+ // Error state to update its display. So we temporarily set the stage for a
+ // StateChange signal.
+ if (this._isErrorState) {
+ return TorConnectState.Error;
+ }
+ switch (this._stageName) {
+ case TorConnectStage.Disabled:
+ return TorConnectState.Disabled;
+ case TorConnectStage.Loading:
+ return TorConnectState.Initial;
+ case TorConnectStage.Start:
+ case TorConnectStage.Offline:
+ case TorConnectStage.ChooseRegion:
+ case TorConnectStage.RegionNotFound:
+ case TorConnectStage.ConfirmRegion:
+ case TorConnectStage.FinalError:
+ return TorConnectState.Configuring;
+ case TorConnectStage.Bootstrapping:
+ if (
+ this._bootstrapTrigger === TorConnectStage.Start ||
+ this._bootstrapTrigger === TorConnectStage.Offline
+ ) {
+ return TorConnectState.Bootstrapping;
+ }
+ return TorConnectState.AutoBootstrapping;
+ case TorConnectStage.Bootstrapped:
+ return TorConnectState.Bootstrapped;
+ }
+ lazy.logger.error(`Unknown state at stage ${this._stageName}`);
+ return null;
},
get countryCodes() {
@@ -1108,92 +1218,414 @@ export const TorConnect = {
return this._countryNames;
},
- get detectedLocation() {
- return this._detectedLocation;
+ /**
+ * Whether the Bootstrapping process has ever failed, not including being
+ * cancelled or being offline.
+ *
+ * The value may change with TorConnectTopics.StageChanged.
+ *
+ * @type {boolean}
+ */
+ get potentiallyBlocked() {
+ return this._potentiallyBlocked;
},
- get errorCode() {
- return this._errorCode;
+ /**
+ * Ensure that we are not disabled.
+ */
+ _ensureEnabled() {
+ if (!this.enabled || this._stageName === TorConnectStage.Disabled) {
+ throw new Error("Unexpected Disabled stage for user method");
+ }
},
- get errorDetails() {
- return this._errorDetails;
- },
+ /**
+ * Signal an error to listeners.
+ *
+ * @param {Error} error - The error.
+ */
+ _signalError(error) {
+ // TODO: Replace this method with _setError without any signalling when
+ // pages have switched to stage.
+ // Currently it simulates the old behaviour for about:torconnect.
+ lazy.logger.debug("Signalling error", error);
+
+ if (!(error instanceof TorConnectError)) {
+ error = new TorConnectError(TorConnectError.ExternalError, error);
+ }
+ this._errorDetails = error;
- get logHasWarningOrError() {
- return this._logHasWarningOrError;
+ // Temporarily set an error state for listeners.
+ // We send the Error signal before the "StateChange" signal.
+ // Expected on android `onBootstrapError` to set lastKnownError.
+ // Expected in about:torconnect to set the error codes and internet status
+ // *before* the StateChange signal.
+ this._isErrorState = true;
+ Services.obs.notifyObservers(error, TorConnectTopics.Error);
+ Services.obs.notifyObservers(
+ { state: this.state },
+ TorConnectTopics.StateChange
+ );
+ this._isErrorState = false;
},
/**
- * Whether we have ever entered the Error state.
+ * Add simulation options to the bootstrap request.
*
- * @type {boolean}
+ * @param {BootstrapOptions} bootstrapOptions - The options to add to.
+ * @param {string} [regionCode] - The region code being used.
*/
- get hasEverFailed() {
- return ErrorState.hasEverHappened;
+ _addSimulateOptions(bootstrapOptions, regionCode) {
+ if (this.simulateBootstrapOptions.simulateCensorship) {
+ bootstrapOptions.simulateCensorship = true;
+ }
+ if (this.simulateBootstrapOptions.simulateDelay) {
+ bootstrapOptions.simulateDelay =
+ this.simulateBootstrapOptions.simulateDelay;
+ }
+ if (this.simulateBootstrapOptions.simulateOffline) {
+ bootstrapOptions.simulateOffline = true;
+ }
+ if (this.simulateBootstrapOptions.simulateMoatResponse) {
+ bootstrapOptions.simulateMoatResponse =
+ this.simulateBootstrapOptions.simulateMoatResponse;
+ }
+
+ const censorshipLevel = Services.prefs.getIntPref(
+ TorConnectPrefs.censorship_level,
+ 0
+ );
+ if (censorshipLevel > 0 && !bootstrapOptions.simulateDelay) {
+ bootstrapOptions.simulateDelay = 1500;
+ }
+ if (censorshipLevel === 1) {
+ // Bootstrap fails, but auto-bootstrap does not.
+ if (!regionCode) {
+ bootstrapOptions.simulateCensorship = true;
+ }
+ } else if (censorshipLevel === 2) {
+ // Bootstrap fails. Auto-bootstrap fails with ConfirmRegion when using
+ // auto-detect region, but succeeds otherwise.
+ if (!regionCode) {
+ bootstrapOptions.simulateCensorship = true;
+ }
+ if (regionCode === "automatic") {
+ bootstrapOptions.simulateCensorship = true;
+ bootstrapOptions.simulateMoatResponse = {
+ country: "fi",
+ settings: [{}, {}],
+ };
+ }
+ } else if (censorshipLevel === 3) {
+ // Bootstrap and auto-bootstrap fail.
+ bootstrapOptions.simulateCensorship = true;
+ bootstrapOptions.simulateMoatResponse = {
+ country: null,
+ settings: [],
+ };
+ }
},
/**
- * Whether the Bootstrapping process has ever failed, not including when it
- * failed due to not being connected to the internet.
+ * Confirm that a bootstrapping can take place, and whether the given values
+ * are valid.
*
- * This does not include a failure in AutoBootstrapping.
+ * @param {string} [regionCode] - The region code passed in.
*
- * @type {boolean}
+ * @return {boolean} whether bootstrapping can proceed.
*/
- get potentiallyBlocked() {
- return this._hasBootstrapEverFailed;
- },
+ _confirmBootstrapping(regionCode) {
+ this._ensureEnabled();
+
+ if (this._bootstrapAttempt) {
+ lazy.logger.warn(
+ "Already have an ongoing bootstrap attempt." +
+ ` Ignoring request with ${regionCode}.`
+ );
+ return false;
+ }
+
+ const currentStage = this._stageName;
+
+ if (regionCode) {
+ if (!this.canBeginAutoBootstrap) {
+ lazy.logger.warn(
+ `Cannot begin auto bootstrap in stage ${currentStage}`
+ );
+ return false;
+ }
+ if (
+ regionCode === "automatic" &&
+ currentStage !== TorConnectStage.ChooseRegion
+ ) {
+ lazy.logger.warn("Auto bootstrap is missing an explicit regionCode");
+ return false;
+ }
+ return true;
+ }
+
+ if (!this.canBeginBootstrap) {
+ lazy.logger.warn(`Cannot begin bootstrap in stage ${currentStage}`);
+ return false;
+ }
+ if (this.canBeginAutoBootstrap) {
+ // Only expect "auto" bootstraps to be triggered when in an error stage.
+ lazy.logger.warn(
+ `Expected a regionCode to bootstrap in stage ${currentStage}`
+ );
+ return false;
+ }
- get uiState() {
- return this._uiState;
+ return true;
},
- set uiState(newState) {
- this._uiState = newState;
+
+ /**
+ * Begin a bootstrap attempt.
+ *
+ * @param {string} [regionCode] - An optional region code string to use, or
+ * "automatic" to automatically determine the region. If given, will start
+ * an auto-bootstrap attempt.
+ */
+ async beginBootstrapping(regionCode) {
+ lazy.logger.debug("TorConnect.beginBootstrapping()");
+
+ if (!this._confirmBootstrapping(regionCode)) {
+ return;
+ }
+
+ const beginStage = this._stageName;
+ const bootstrapOptions = { regionCode };
+ const bootstrapAttempt = regionCode
+ ? new AutoBootstrapAttempt()
+ : new BootstrapAttempt();
+
+ if (!regionCode) {
+ // Only test internet for the first bootstrap attempt.
+ // TODO: Remove this since we do not have user consent. tor-browser#42605.
+ bootstrapOptions.testInternet = true;
+ }
+
+ this._addSimulateOptions(bootstrapOptions, regionCode);
+
+ // NOTE: The only `await` in this method is for `bootstrapAttempt.run`.
+ // Moreover, we returned early if `_bootstrapAttempt` was non-`null`.
+ // Therefore, the method is effectively "locked" by `_bootstrapAttempt`, so
+ // there should only ever be one caller at a time.
+
+ if (regionCode) {
+ // Set the default to what the user chose.
+ this._defaultRegion = regionCode;
+ } else {
+ // Reset the default region to show in the UI.
+ this._defaultRegion = "automatic";
+ }
+ this._requestedStage = null;
+ this._bootstrapTrigger = beginStage;
+ this._setStage(TorConnectStage.Bootstrapping);
+ this._bootstrapAttempt = bootstrapAttempt;
+
+ let error = null;
+ let result = null;
+ try {
+ result = await bootstrapAttempt.run(progress => {
+ this._bootstrappingStatus.progress = progress;
+ lazy.logger.info(`Bootstrapping ${progress}% complete`);
+ this._notifyBootstrapProgress();
+ }, bootstrapOptions);
+ } catch (err) {
+ error = err;
+ }
+
+ const requestedStage = this._requestedStage;
+ this._requestedStage = null;
+ this._bootstrapTrigger = null;
+ this._bootstrapAttempt = null;
+
+ if (bootstrapAttempt.detectedRegion) {
+ this._defaultRegion = bootstrapAttempt.detectedRegion;
+ }
+
+ if (result === "complete") {
+ // Reset tryAgain, potentiallyBlocked and errorDetails in case the tor
+ // process exists later on.
+ this._tryAgain = false;
+ this._potentiallyBlocked = false;
+ this._errorDetails = null;
+
+ if (requestedStage) {
+ lazy.logger.warn(
+ `Ignoring ${requestedStage} request since we are bootstrapped`
+ );
+ }
+ this._setStage(TorConnectStage.Bootstrapped);
+ Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete);
+ return;
+ }
+
+ if (requestedStage) {
+ lazy.logger.debug("Ignoring bootstrap result", result, error);
+ this._setStage(requestedStage);
+ return;
+ }
+
+ if (
+ result === "offline" &&
+ (beginStage === TorConnectStage.Start ||
+ beginStage === TorConnectStage.Offline)
+ ) {
+ this._tryAgain = true;
+ this._signalError(new TorConnectError(TorConnectError.Offline));
+
+ this._setStage(TorConnectStage.Offline);
+ return;
+ }
+
+ if (error) {
+ lazy.logger.info("Bootstrap attempt error", error);
+
+ this._tryAgain = true;
+ this._potentiallyBlocked = true;
+
+ this._signalError(error);
+
+ switch (beginStage) {
+ case TorConnectStage.Start:
+ case TorConnectStage.Offline:
+ this._setStage(TorConnectStage.ChooseRegion);
+ return;
+ case TorConnectStage.ChooseRegion:
+ // TODO: Uncomment for behaviour in tor-browser#42550.
+ /*
+ if (regionCode !== "automatic") {
+ // Not automatic. Go straight to the final error.
+ this._setStage(TorConnectStage.FinalError);
+ return;
+ }
+ */
+ if (regionCode !== "automatic" || bootstrapAttempt.detectedRegion) {
+ this._setStage(TorConnectStage.ConfirmRegion);
+ return;
+ }
+ this._setStage(TorConnectStage.RegionNotFound);
+ return;
+ }
+ this._setStage(TorConnectStage.FinalError);
+ return;
+ }
+
+ // Bootstrap was cancelled.
+ if (result !== "cancelled") {
+ lazy.logger.error(`Unexpected bootstrap result`, result);
+ }
+
+ // TODO: Remove this Offline hack when pages use "stage".
+ if (beginStage === TorConnectStage.Offline) {
+ // Re-send the "Offline" error to push the pages back to "Offline".
+ this._signalError(new TorConnectError(TorConnectError.Offline));
+ }
+
+ // Return to the previous stage.
+ this._setStage(beginStage);
},
- /*
- These functions allow external consumers to tell TorConnect to transition states
+ /**
+ * Cancel an ongoing bootstrap attempt.
*/
+ cancelBootstrapping() {
+ lazy.logger.debug("TorConnect.cancelBootstrapping()");
+
+ this._ensureEnabled();
+
+ if (!this._bootstrapAttempt) {
+ lazy.logger.warn("No bootstrap attempt to cancel");
+ return;
+ }
- beginBootstrap() {
- lazy.logger.debug("TorConnect.beginBootstrap()");
- this._changeState(TorConnectState.Bootstrapping);
+ this._bootstrapAttempt.cancel();
},
- cancelBootstrap() {
- lazy.logger.debug("TorConnect.cancelBootstrap()");
+ /**
+ * Request the transition to the given stage.
+ *
+ * If we are bootstrapping, it will be cancelled and the stage will be
+ * transitioned to when it resolves. Otherwise, we will switch to the stage
+ * immediately.
+ *
+ * @param {string} stage - The stage to request.
+ * @param {boolean} [overideBootstrapped=false] - Whether the request can
+ * override the "Bootstrapped" stage.
+ */
+ _makeStageRequest(stage, overrideBootstrapped = false) {
+ lazy.logger.debug(`Request for stage ${stage}`);
+
+ this._ensureEnabled();
+
+ if (stage === this._stageName) {
+ lazy.logger.info(`Ignoring request for current stage ${stage}`);
+ return;
+ }
if (
- this.state !== TorConnectState.AutoBootstrapping &&
- this.state !== TorConnectState.Bootstrapping
+ !overrideBootstrapped &&
+ this._stageName === TorConnectStage.Bootstrapped
) {
+ lazy.logger.warn(`Cannot move to ${stage} when bootstrapped`);
+ return;
+ }
+ if (this._stageName === TorConnectStage.Loading) {
+ if (stage === TorConnectStage.Start) {
+ // Will transition to "Start" stage when loading completes.
+ lazy.logger.info("Still in the Loading stage");
+ } else {
+ lazy.logger.warn(`Cannot move to ${stage} when Loading`);
+ }
+ return;
+ }
+
+ if (!this._bootstrapAttempt) {
+ // Transition immediately.
+ this._setStage(stage);
+ return;
+ }
+
+ if (this._requestedStage === stage) {
+ lazy.logger.info(`Already requesting stage ${stage}`);
+ return;
+ }
+ if (this._requestedStage) {
lazy.logger.warn(
- `Cannot cancel bootstrapping in the ${this.state} state`
+ `Overriding request for ${this._requestedStage} with ${stage}`
);
- return;
}
- this._changeState(TorConnectState.Configuring);
+ // Move to stage *after* bootstrap completes.
+ this._requestedStage = stage;
+ this._bootstrapAttempt?.cancel();
},
- beginAutoBootstrap(countryCode) {
- lazy.logger.debug("TorConnect.beginAutoBootstrap()");
- this._changeState(TorConnectState.AutoBootstrapping, countryCode);
+ /**
+ * Restart the TorConnect stage to the start.
+ */
+ startAgain() {
+ this._makeStageRequest(TorConnectStage.Start);
},
- /*
- Further external commands and helper methods
+ /**
+ * Set the stage to be "ChooseRegion".
*/
- openTorPreferences() {
- if (lazy.TorLauncherUtil.isAndroid) {
- lazy.EventDispatcher.instance.sendRequest({
- type: "GeckoView:Tor:OpenSettings",
- });
+ chooseRegion() {
+ if (!this._potentiallyBlocked) {
+ lazy.logger.error("chooseRegion request before getting an error");
return;
}
- const win = lazy.BrowserWindowTracker.getTopWindow();
- win.switchToTabHavingURI("about:preferences#connection", true);
+ // NOTE: The ChooseRegion stage needs _errorDetails to be displayed in
+ // about:torconnect. The _potentiallyBlocked condition should be
+ // sufficient to ensure this.
+ this._makeStageRequest(TorConnectStage.ChooseRegion);
},
+ /*
+ Further external commands and helper methods
+ */
+
/**
* Open the "about:torconnect" tab.
*
@@ -1204,10 +1636,11 @@ export const TorConnect = {
* potentially blocked.
*
* @param {object} [options] - extra options.
- * @property {boolean} [options.beginBootstrap=false] - Whether to try and
- * begin Bootstrapping.
- * @property {string} [options.beginAutoBootstrap] - The location to use to
- * begin AutoBootstrapping, if possible.
+ * @property {"soft"|"hard"} [options.beginBootstrapping] - Whether to try and
+ * begin bootstrapping. "soft" will only trigger the bootstrap if we are not
+ * `potentiallyBlocked`. "hard" will try begin the bootstrap regardless.
+ * @property {string} [options.regionCode] - A region to pass in for
+ * auto-bootstrapping.
*/
openTorConnect(options) {
// FIXME: Should we move this to the about:torconnect actor?
@@ -1215,25 +1648,23 @@ export const TorConnect = {
win.switchToTabHavingURI("about:torconnect", true, {
ignoreQueryString: true,
});
- if (
- options?.beginBootstrap &&
- this.canBeginBootstrap &&
- !this.potentiallyBlocked
- ) {
- this.beginBootstrap();
+
+ if (!options?.beginBootstrapping || !this.canBeginBootstrap) {
+ return;
}
- // options.beginAutoBootstrap can be an empty string.
- if (
- options?.beginAutoBootstrap !== undefined &&
- this.canBeginAutoBootstrap
- ) {
- this.beginAutoBootstrap(options.beginAutoBootstrap);
+
+ if (options.beginBootstrapping === "hard") {
+ if (this.canBeginAutoBootstrap && !options.regionCode) {
+ // Treat as an addition startAgain request to first move back to the
+ // "Start" stage before bootstrapping.
+ this.startAgain();
+ }
+ } else if (this.potentiallyBlocked) {
+ // Do not trigger the bootstrap if we have ever had an error.
+ return;
}
- },
- viewTorLogs() {
- const win = lazy.BrowserWindowTracker.getTopWindow();
- win.switchToTabHavingURI("about:preferences#connection-viewlogs", true);
+ this.beginBootstrapping(options.regionCode);
},
async getCountryCodes() {
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/bb3cd9…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/bb3cd9…
You're receiving this email because of your account on gitlab.torproject.org.
Pier Angelo Vendrame pushed to branch tor-browser-128.4.0esr-14.5-1 at The Tor Project / Applications / Tor Browser
Commits:
82705e37 by Pier Angelo Vendrame at 2024-11-12T17:32:04+01:00
fixup! Bug 40933: Add tor-launcher functionality
Make optional members in JSDoc comments more consistent with Firefox's
codebase.
- - - - -
163c91b6 by Pier Angelo Vendrame at 2024-11-12T17:32:05+01:00
fixup! Bug 40933: Add tor-launcher functionality
Bug 10439: Ask the the SOCKS port to the tor process.
Allow specifying a negative port as TOR_SOCKS_PORT to let the tor
process choose one for us.
This does not play too well with DisableNetwork, which we use to start
and stop the bootstrap, so we have to query the port every time we
change this setting.
Also, currently we use Firefox's preferences for SOCKS port.
This prevents us from keeping a negative number saved in them.
See also tor-browser#42062.
- - - - -
f7e4e221 by Pier Angelo Vendrame at 2024-11-12T17:32:06+01:00
fixup! Bug 40933: Add tor-launcher functionality
Bug 42714: Allow to optionally use a TCP listener on Android.
- - - - -
bb3cd92e by Pier Angelo Vendrame at 2024-11-12T17:32:06+01:00
fixup! Bug 42247: Android helpers for the TorProvider
Bug 42714: Allow to optionally use a TCP listener on Android.
- - - - -
6 changed files:
- mobile/android/geckoview/src/main/java/org/mozilla/geckoview/TorIntegrationAndroid.java
- toolkit/components/tor-launcher/TorControlPort.sys.mjs
- toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs
- toolkit/components/tor-launcher/TorProcess.sys.mjs
- toolkit/components/tor-launcher/TorProcessAndroid.sys.mjs
- toolkit/components/tor-launcher/TorProvider.sys.mjs
Changes:
=====================================
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/TorIntegrationAndroid.java
=====================================
@@ -220,12 +220,18 @@ public class TorIntegrationAndroid implements BundleEventListener {
if (previousProcess != null) {
Log.w(TAG, "We still have a running process: " + previousProcess.getHandle());
}
- mTorProcess = new TorProcess(handle);
+
+ boolean tcpSocks = message.getBoolean("tcpSocks", false);
+ mTorProcess = new TorProcess(handle, tcpSocks);
GeckoBundle bundle = new GeckoBundle(3);
bundle.putString("controlPortPath", mIpcDirectory + CONTROL_PORT_FILE);
- bundle.putString("socksPath", mIpcDirectory + SOCKS_FILE);
bundle.putString("cookieFilePath", mIpcDirectory + COOKIE_AUTH_FILE);
+ if (tcpSocks) {
+ bundle.putInt("socksPort", 0);
+ } else {
+ bundle.putString("socksPath", mIpcDirectory + SOCKS_FILE);
+ }
callback.sendSuccess(bundle);
}
@@ -254,10 +260,12 @@ public class TorIntegrationAndroid implements BundleEventListener {
private static final String EVENT_TOR_START_FAILED = "GeckoView:Tor:TorStartFailed";
private static final String EVENT_TOR_EXITED = "GeckoView:Tor:TorExited";
private final String mHandle;
+ private final boolean mTcpSocks;
private Process mProcess = null;
- TorProcess(String handle) {
+ TorProcess(String handle, boolean tcpSocks) {
mHandle = handle;
+ mTcpSocks = tcpSocks;
setName("tor-process-" + handle);
start();
}
@@ -273,8 +281,13 @@ public class TorIntegrationAndroid implements BundleEventListener {
args.add("1");
args.add("+__ControlPort");
args.add("unix:" + ipcDir + CONTROL_PORT_FILE);
+ final String socksFlags = " IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth";
args.add("+__SocksPort");
- args.add("unix:" + ipcDir + SOCKS_FILE + " IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth");
+ args.add("unix:" + ipcDir + SOCKS_FILE + socksFlags);
+ if (mTcpSocks) {
+ args.add("+__SocksPort");
+ args.add("auto " + socksFlags);
+ }
args.add("CookieAuthentication");
args.add("1");
args.add("CookieAuthFile");
=====================================
toolkit/components/tor-launcher/TorControlPort.sys.mjs
=====================================
@@ -298,6 +298,12 @@ class AsyncSocket {
* @property {string} [options] Optional options passed to the binary (only for
* exec)
*/
+/**
+ * @typedef {object} SocksListener
+ * @property {string} [ipcPath] path to a Unix socket to use for an IPC proxy
+ * @property {string} [host] The host to connect for a TCP proxy
+ * @property {number} [port] The port number to use for a TCP proxy
+ */
/**
* @typedef {object} OnionAuthKeyInfo
* @property {string} address The address of the onion service
@@ -746,6 +752,32 @@ export class TorController {
return this.#getInfo(`ip-to-country/${ip}`);
}
+ /**
+ * Ask tor which ports it is listening to for SOCKS connections.
+ *
+ * @returns {Promise<SocksListener[]>} An array of addresses. It might be
+ * empty (e.g., when DisableNetwork is set)
+ */
+ async getSocksListeners() {
+ const listeners = await this.#getInfo("net/listeners/socks");
+ return Array.from(
+ listeners.matchAll(/\s*("(?:[^"\\]|\\.)*"|\S+)\s*/g),
+ m => {
+ const listener = TorParsers.unescapeString(m[1]);
+ if (listener.startsWith("unix:/")) {
+ return { ipcPath: listener.substring(5) };
+ }
+ const idx = listener.lastIndexOf(":");
+ const host = listener.substring(0, idx);
+ const port = parseInt(listener.substring(idx + 1));
+ if (isNaN(port) || port <= 0 || port > 65535 || !host || !port) {
+ throw new Error(`Could not parse the SOCKS listener ${listener}.`);
+ }
+ return { host, port };
+ }
+ );
+ }
+
/**
* Ask Tor a list of circuits.
*
=====================================
toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs
=====================================
@@ -449,7 +449,7 @@ export const TorLauncherUtil = Object.freeze({
* If network.proxy.socks contains a file: URL, a default value of
* "127.0.0.1" is used instead.
* If the network.proxy.socks_port value is not valid (outside the
- * (0; 65535] range), a default value of 9150 is used instead.
+ * (0; 65535] range), we will let the tor daemon choose a port.
*
* The SOCKS configuration will not influence the launch of a tor daemon and
* the configuration of the control port in any way.
@@ -458,13 +458,6 @@ export const TorLauncherUtil = Object.freeze({
* This also applies to TOR_TRANSPROXY (at least for now): tor will be
* launched with its defaults.
*
- * TODO: add a preference to ignore the current configuration, and let tor
- * listen on any free port. Then, the browser will prompt the daemon the port
- * to use through the control port (even though this is quite dangerous at the
- * moment, because with network disabled tor will disable also the SOCKS
- * listeners, so it means that we will have to check it every time we change
- * the network status).
- *
* @returns {SocksSettings}
*/
getPreferredSocksConfiguration() {
@@ -491,7 +484,7 @@ export const TorLauncherUtil = Object.freeze({
}
if (Services.env.exists("TOR_SOCKS_PORT")) {
const port = parseInt(Services.env.get("TOR_SOCKS_PORT"), 10);
- if (Number.isInteger(port) && port > 0 && port <= 65535) {
+ if (Number.isInteger(port) && port >= 0 && port <= 65535) {
socksPortInfo.port = port;
useIPC = false;
}
@@ -522,20 +515,32 @@ export const TorLauncherUtil = Object.freeze({
socksPortInfo.host = socksAddrHasHost ? socksAddr : "127.0.0.1";
}
- if (!socksPortInfo.port) {
+ if (socksPortInfo.port === undefined) {
let socksPort = Services.prefs.getIntPref(
"network.proxy.socks_port",
- 0
+ 9150
);
- // This pref is set as 0 by default in Firefox, use 9150 if we get 0.
- socksPortInfo.port =
- socksPort > 0 && socksPort <= 65535 ? socksPort : 9150;
+ if (socksPort > 0 && socksPort <= 65535) {
+ socksPortInfo.port = socksPort;
+ } else {
+ // Automatic port number, we have to query tor over the control port
+ // every time we change DisableNetwork.
+ socksPortInfo.port = 0;
+ }
}
}
return socksPortInfo;
},
+ /**
+ * Apply our proxy configuration to the browser.
+ *
+ * Currently, we try to configure the Tor daemon to match the browser's
+ * configuration, but this might change in the future (tor-browser#42062).
+ *
+ * @param {SocksSettings} socksPortInfo The configuration to apply
+ */
setProxyConfiguration(socksPortInfo) {
if (socksPortInfo.transproxy) {
Services.prefs.setBoolPref("network.proxy.socks_remote_dns", false);
@@ -556,7 +561,7 @@ export const TorLauncherUtil = Object.freeze({
if (socksPortInfo.host) {
Services.prefs.setCharPref("network.proxy.socks", socksPortInfo.host);
}
- if (socksPortInfo.port) {
+ if (socksPortInfo.port > 0 && socksPortInfo.port <= 65535) {
Services.prefs.setIntPref(
"network.proxy.socks_port",
socksPortInfo.port
=====================================
toolkit/components/tor-launcher/TorProcess.sys.mjs
=====================================
@@ -53,13 +53,16 @@ export class TorProcess {
throw new Error("Unauthenticated control port is not supported");
}
- const checkPort = port =>
+ const checkPort = (port, allowZero) =>
port === undefined ||
- (Number.isInteger(port) && port > 0 && port < 65535);
- if (!checkPort(controlSettings?.port)) {
+ (Number.isInteger(port) &&
+ port < 65535 &&
+ (port > 0 || (allowZero && port === 0)));
+ if (!checkPort(controlSettings?.port, false)) {
throw new Error("Invalid control port");
}
- if (!checkPort(socksSettings.port)) {
+ // Port 0 for SOCKS means automatic port.
+ if (!checkPort(socksSettings.port, true)) {
throw new Error("Invalid port specified for the SOCKS port");
}
@@ -296,10 +299,12 @@ export class TorProcess {
let socksPortArg;
if (this.#socksSettings.ipcFile) {
socksPortArg = this.#socksSettings.ipcFile;
- } else if (this.#socksSettings.port != 0) {
+ } else if (this.#socksSettings.port > 0) {
socksPortArg = this.#socksSettings.host
? `${this.#socksSettings.host}:${this.#socksSettings.port}`
: this.#socksSettings.port.toString();
+ } else {
+ socksPortArg = "auto";
}
if (socksPortArg) {
const socksPortFlags = Services.prefs.getCharPref(
=====================================
toolkit/components/tor-launcher/TorProcessAndroid.sys.mjs
=====================================
@@ -77,6 +77,10 @@ export class TorProcessAndroid {
config = await lazy.EventDispatcher.instance.sendRequestForResult({
type: TorOutgoingEvents.start,
handle: this.#processHandle,
+ tcpSocks: Services.prefs.getBoolPref(
+ "extensions.torlauncher.socks_port_use_tcp",
+ false
+ ),
});
logger.debug("Sent the start event.");
} catch (e) {
=====================================
toolkit/components/tor-launcher/TorProvider.sys.mjs
=====================================
@@ -27,23 +27,23 @@ const logger = console.createInstance({
* @typedef {object} ControlPortSettings An object with the settings to use for
* the control port. All the entries are optional, but an authentication
* mechanism and a communication method must be specified.
- * @property {Uint8Array=} password The clear text password as an array of
+ * @property {Uint8Array} [password] The clear text password as an array of
* bytes. It must always be defined, unless cookieFilePath is
- * @property {string=} cookieFilePath The path to the cookie file to use for
+ * @property {string} [cookieFilePath] The path to the cookie file to use for
* authentication
- * @property {nsIFile=} ipcFile The nsIFile object with the path to a Unix
+ * @property {nsIFile} [ipcFile] The nsIFile object with the path to a Unix
* socket to use for control socket
- * @property {string=} host The host to connect for a TCP control port
- * @property {number=} port The port number to use for a TCP control port
+ * @property {string} [host] The host to connect for a TCP control port
+ * @property {number} [port] The port number to use for a TCP control port
*/
/**
* @typedef {object} SocksSettings An object that includes the proxy settings to
* be configured in the browser.
- * @property {boolean=} transproxy If true, no proxy is configured
- * @property {nsIFile=} ipcFile The nsIFile object with the path to a Unix
+ * @property {boolean} [transproxy] If true, no proxy is configured
+ * @property {nsIFile} [ipcFile] The nsIFile object with the path to a Unix
* socket to use for an IPC proxy
- * @property {string=} host The host to connect for a TCP proxy
- * @property {number=} port The port number to use for a TCP proxy
+ * @property {string} [host] The host to connect for a TCP proxy
+ * @property {number} [port] The port number to use for a TCP proxy
*/
/**
* @typedef {object} LogEntry An object with a log message
@@ -345,6 +345,25 @@ export class TorProvider {
*/
async connect() {
await this.#controller.setNetworkEnabled(true);
+ if (this.#socksSettings.port === 0) {
+ // Enablign/disabling network resets also the SOCKS listener.
+ // So, every time we do it, we need to update the browser's configuration
+ // to use the updated port.
+ const settings = structuredClone(this.#socksSettings);
+ for (const listener of await this.#controller.getSocksListeners()) {
+ // When set to automatic port, ignore any IPC listener, as the intention
+ // was to use TCP.
+ if (listener.ipcPath) {
+ continue;
+ }
+ // The tor daemon can have any number of SOCKS listeners (see SocksPort
+ // in man 1 tor). We take for granted that any TCP one will work for us.
+ settings.host = listener.host;
+ settings.port = listener.port;
+ break;
+ }
+ TorLauncherUtil.setProxyConfiguration(settings);
+ }
this.#lastWarning = {};
this.retrieveBootstrapStatus();
}
@@ -569,14 +588,24 @@ export class TorProvider {
logger.debug("Trying to start the tor process.");
const res = await this.#torProcess.start();
if (TorLauncherUtil.isAndroid) {
+ logger.debug("Configuration from TorProcessAndriod", res);
this.#controlPortSettings = {
ipcFile: new lazy.FileUtils.File(res.controlPortPath),
cookieFilePath: res.cookieFilePath,
};
this.#socksSettings = {
transproxy: false,
- ipcFile: new lazy.FileUtils.File(res.socksPath),
};
+ if (res.socksPath) {
+ this.#socksSettings.ipcFile = new lazy.FileUtils.File(res.socksPath);
+ } else if (res.socksPort !== undefined) {
+ this.#socksSettings.host = res.socksHost ?? "127.0.0.1";
+ this.#socksSettings.port = res.socksPort;
+ } else {
+ throw new Error(
+ "TorProcessAndroid did not return a valid SOCKS configuration."
+ );
+ }
}
logger.info("Started a tor process");
}
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/d4097f…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/d4097f…
You're receiving this email because of your account on gitlab.torproject.org.
Pier Angelo Vendrame pushed to branch tor-browser-128.4.0esr-14.5-1 at The Tor Project / Applications / Tor Browser
Commits:
eb5aaf7e by Henry Wilkes at 2024-11-12T15:43:54+00:00
fixup! Lox integration
Bug 42492: Ensure operations that change lox credentials do not overlap.
Not linted to improve readability.
- - - - -
d4097f9c by Henry Wilkes at 2024-11-12T15:45:39+00:00
fixup! Lox integration
Bug 42492: Lint Lox.sys.mjs
- - - - -
1 changed file:
- toolkit/components/lox/Lox.sys.mjs
Changes:
=====================================
toolkit/components/lox/Lox.sys.mjs
=====================================
@@ -144,9 +144,9 @@ class LoxImpl {
/**
* The latest credentials for a given lox id.
*
- * @type {Object<string, string>}
+ * @type {Map<string, string>}
*/
- #credentials = {};
+ #credentials = new Map();
/**
* The list of accumulated blockage or upgrade events.
*
@@ -257,25 +257,73 @@ class LoxImpl {
}
/**
- * Change some existing credentials for an ID to a new value.
+ * Stores a promise for the last task that was performed to change
+ * credentials for a given lox ID. This promise completes when the task
+ * completes and it is safe to perform a new action on the credentials.
+ *
+ * This essentially acts as a lock on the credential, so that only one task
+ * acts on the credentials at any given time. See tor-browser#42492.
+ *
+ * @type {Map<string, Promise>}
+ */
+ #credentialsTasks = new Map();
+
+ /**
+ * Attempt to change some existing credentials for an ID to a new value.
+ *
+ * Each call for the same lox ID must await the previous call. As such, this
+ * should *never* be called recursively.
*
* @param {string} loxId - The ID to change the credentials for.
- * @param {string} newCredentials - The new credentials to set.
+ * @param {Function} task - The task that performs the change in credentials.
+ * The method is given the current credentials. It should either return the
+ * new credentials as a string, or null if the credentials should not
+ * change, or throw an error which will fall through to the caller.
+ *
+ * @returns {?string} - The credentials returned by the task, if any.
*/
- #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}`);
+ async #changeCredentials(loxId, task) {
+ // Read and replace #credentialsTasks before we do any async operations.
+ // I.e. this is effectively atomic read and replace.
+ const prevTask = this.#credentialsTasks.get(loxId);
+ let taskComplete;
+ this.#credentialsTasks.set(
+ loxId,
+ new Promise(res => {
+ taskComplete = res;
+ })
+ );
+
+ // Wait for any previous task to complete first, to avoid making conflicting
+ // changes to the credentials. See tor-browser#42492.
+ // prevTask is either undefined or a promise that should not throw.
+ await prevTask;
+
+ // Future calls now await us.
+
+ const cred = this.#getCredentials(loxId);
+ let newCred = null;
+ try {
+ // This task may throw, in which case we do not set new credentials.
+ newCred = await task(cred);
+ if (newCred) {
+ this.#credentials.set(loxId, newCred);
+ // Store the new credentials.
+ this.#store();
+ lazy.logger.debug("Changed credentials");
+ }
+ } finally {
+ // Stop awaiting us.
+ taskComplete();
}
- 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`);
+
+ if (!newCred) {
+ return null;
}
- this.#credentials[loxId] = newCredentials;
- this.#store();
+ // Let listeners know we have new credentials. We do this *after* calling
+ // taskComplete to avoid a recursive call to await this.#changeCredentials,
+ // which would cause us to hang.
// NOTE: In principle we could determine within this module whether the
// bridges, remaining invites, or next unlock changes in value when
@@ -289,6 +337,8 @@ class LoxImpl {
// Let UI know about changes.
Services.obs.notifyObservers(null, LoxTopics.UpdateRemainingInvites);
Services.obs.notifyObservers(null, LoxTopics.UpdateNextUnlock);
+
+ return newCred;
}
/**
@@ -299,7 +349,7 @@ class LoxImpl {
* @returns {string} - The credentials.
*/
#getCredentials(loxId) {
- const cred = loxId ? this.#credentials[loxId] : undefined;
+ const cred = loxId ? this.#credentials.get(loxId) : undefined;
if (!cred) {
throw new LoxError(`No credentials for ${loxId}`);
}
@@ -376,7 +426,7 @@ class LoxImpl {
Services.prefs.setStringPref(LoxSettingsPrefs.constants, this.#constants);
Services.prefs.setStringPref(
LoxSettingsPrefs.credentials,
- JSON.stringify(this.#credentials)
+ JSON.stringify(Object.fromEntries(this.#credentials))
);
Services.prefs.setStringPref(
LoxSettingsPrefs.invites,
@@ -390,7 +440,7 @@ class LoxImpl {
#load() {
const cred = Services.prefs.getStringPref(LoxSettingsPrefs.credentials, "");
- this.#credentials = cred ? JSON.parse(cred) : {};
+ this.#credentials = new Map(cred ? Object.entries(JSON.parse(cred)) : []);
const invites = Services.prefs.getStringPref(LoxSettingsPrefs.invites, "");
this.#invites = invites ? JSON.parse(invites) : [];
const events = Services.prefs.getStringPref(LoxSettingsPrefs.events, "");
@@ -421,17 +471,15 @@ class LoxImpl {
if (prevKeys !== null) {
// check if the lox pubkeys have changed and update the lox
// credentials if so.
- //
- // The UpdateCredOption rust struct serializes to "req" rather than
- // "request".
- const { updated, req: request } = JSON.parse(
- lazy.check_lox_pubkeys_update(
- pubKeys,
- prevKeys,
- this.#getCredentials(this.#activeLoxId)
- )
- );
- if (updated) {
+ await this.#changeCredentials(this.#activeLoxId, async cred => {
+ // The UpdateCredOption rust struct serializes to "req" rather than
+ // "request".
+ const { updated, req: request } = JSON.parse(
+ lazy.check_lox_pubkeys_update(pubKeys, prevKeys, cred)
+ );
+ if (!updated) {
+ return null;
+ }
// Try update credentials.
// NOTE: This should be re-callable if any step fails.
// TODO: Verify this.
@@ -444,9 +492,8 @@ class LoxImpl {
// is refactored to send repeat responses:
// https://gitlab.torproject.org/tpo/anti-censorship/lox/-/issues/74)
let response = await this.#makeRequest("updatecred", request);
- let cred = lazy.handle_update_cred(request, response, pubKeys);
- this.#changeCredentials(this.#activeLoxId, cred);
- }
+ return lazy.handle_update_cred(request, response, pubKeys);
+ });
}
// If we arrive here we haven't had other errors before, we can actually
// store the new public key.
@@ -648,7 +695,7 @@ class LoxImpl {
this.#pubKeyPromise = null;
this.#encTablePromise = null;
this.#constantsPromise = null;
- this.#credentials = {};
+ this.#credentials = new Map();
this.#events = [];
if (this.#backgroundInterval) {
clearInterval(this.#backgroundInterval);
@@ -712,9 +759,9 @@ class LoxImpl {
let loxId;
do {
loxId = this.#genLoxId();
- } while (Object.hasOwn(this.#credentials, loxId));
+ } while (this.#credentials.has(loxId));
// Set new credentials.
- this.#credentials[loxId] = cred;
+ this.#credentials.set(loxId, cred);
this.#store();
return loxId;
}
@@ -760,21 +807,17 @@ class LoxImpl {
if (level < 1) {
throw new LoxError(`Cannot generate invites at level ${level}`);
}
- let request = lazy.issue_invite(
- this.#getCredentials(loxId),
- this.#encTable,
- this.#pubKeys
- );
- let response = await this.#makeRequest("issueinvite", request);
- // TODO: Do we ever expect handle_issue_invite to fail (beyond
- // implementation bugs)?
- // TODO: What happens if #pubkeys for `issue_invite` differs from the value
- // when calling `handle_issue_invite`? Should we cache the value at the
- // start of this method?
- let cred = lazy.handle_issue_invite(request, response, this.#pubKeys);
- // Store the new credentials as a priority.
- this.#changeCredentials(loxId, cred);
+ const cred = await this.#changeCredentials(loxId, async cred => {
+ let request = lazy.issue_invite(cred, this.#encTable, this.#pubKeys);
+ let response = await this.#makeRequest("issueinvite", request);
+ // TODO: Do we ever expect handle_issue_invite to fail (beyond
+ // implementation bugs)?
+ // TODO: What happens if #pubkeys for `issue_invite` differs from the value
+ // when calling `handle_issue_invite`? Should we cache the value at the
+ // start of this method?
+ return lazy.handle_issue_invite(request, response, this.#pubKeys);
+ });
const invite = lazy.prepare_invite(cred);
this.#invites.push(invite);
@@ -804,35 +847,26 @@ class LoxImpl {
}
async #blockageMigration(loxId) {
- let request;
- try {
- request = lazy.check_blockage(this.#getCredentials(loxId), this.#pubKeys);
- } catch {
- lazy.logger.log("Not ready for blockage migration");
- return false;
- }
- let response = await this.#makeRequest("checkblockage", request);
- // NOTE: If a later method fails, we should be ok to re-call "checkblockage"
- // from the Lox authority. So there shouldn't be any adverse side effects to
- // loosing migrationCred.
- // TODO: Confirm this is safe to lose.
- const migrationCred = lazy.handle_check_blockage(
- this.#getCredentials(loxId),
- response
- );
- request = lazy.blockage_migration(
- this.#getCredentials(loxId),
- migrationCred,
- this.#pubKeys
- );
- response = await this.#makeRequest("blockagemigration", request);
- const cred = lazy.handle_blockage_migration(
- this.#getCredentials(loxId),
- response,
- this.#pubKeys
+ return Boolean(
+ await this.#changeCredentials(loxId, async cred => {
+ let request;
+ try {
+ request = lazy.check_blockage(cred, this.#pubKeys);
+ } catch {
+ lazy.logger.log("Not ready for blockage migration");
+ return null;
+ }
+ let response = await this.#makeRequest("checkblockage", request);
+ // NOTE: If a later method fails, we should be ok to re-call "checkblockage"
+ // from the Lox authority. So there shouldn't be any adverse side effects to
+ // loosing migrationCred.
+ // TODO: Confirm this is safe to lose.
+ const migrationCred = lazy.handle_check_blockage(cred, response);
+ request = lazy.blockage_migration(cred, migrationCred, this.#pubKeys);
+ response = await this.#makeRequest("blockagemigration", request);
+ return lazy.handle_blockage_migration(cred, response, this.#pubKeys);
+ })
);
- this.#changeCredentials(loxId, cred);
- return true;
}
/**
@@ -850,25 +884,26 @@ class LoxImpl {
// attempt trust promotion instead
return this.#trustMigration(loxId);
}
- let request = lazy.level_up(
- this.#getCredentials(loxId),
- this.#encTable,
- this.#pubKeys
+ return Boolean(
+ await this.#changeCredentials(loxId, async cred => {
+ let request = lazy.level_up(cred, this.#encTable, this.#pubKeys);
+ let response;
+ try {
+ response = await this.#makeRequest("levelup", request);
+ } catch (error) {
+ if (
+ error instanceof LoxError &&
+ error.code === LoxError.ErrorResponse
+ ) {
+ // Not an error.
+ lazy.logger.debug("Not ready for level up", error);
+ return null;
+ }
+ throw error;
+ }
+ return lazy.handle_level_up(request, response, this.#pubKeys);
+ })
);
- let response;
- try {
- response = await this.#makeRequest("levelup", request);
- } catch (error) {
- if (error instanceof LoxError && error.code === LoxError.ErrorResponse) {
- // Not an error.
- lazy.logger.debug("Not ready for level up", error);
- return false;
- }
- throw error;
- }
- const cred = lazy.handle_level_up(request, response, this.#pubKeys);
- this.#changeCredentials(loxId, cred);
- return true;
}
/**
@@ -884,42 +919,37 @@ class LoxImpl {
this.#getPubKeys();
return false;
}
- let request;
- try {
- request = lazy.trust_promotion(
- this.#getCredentials(loxId),
- this.#pubKeys
- );
- } catch (err) {
- // This function is called routinely during the background tasks without
- // previous checks on whether an upgrade is possible, so it is expected to
- // fail with a certain frequency. Therefore, do not relay the error to the
- // caller and just log the message for debugging.
- lazy.logger.debug("Not ready to upgrade", err);
- return false;
- }
+ return Boolean(
+ await this.#changeCredentials(loxId, async cred => {
+ let request;
+ try {
+ request = lazy.trust_promotion(cred, this.#pubKeys);
+ } catch (err) {
+ // This function is called routinely during the background tasks without
+ // previous checks on whether an upgrade is possible, so it is expected to
+ // fail with a certain frequency. Therefore, do not relay the error to the
+ // caller and just log the message for debugging.
+ lazy.logger.debug("Not ready to upgrade", err);
+ return null;
+ }
- let response = await this.#makeRequest("trustpromo", request);
- // FIXME: Store response to "trustpromo" in case handle_trust_promotion
- // or "trustmig" fails. The Lox authority will not accept a re-request
- // to "trustpromo" with the same credentials.
- let promoCred = lazy.handle_trust_promotion(request, response);
- lazy.logger.debug("Formatted promotion cred: ", promoCred);
-
- request = lazy.trust_migration(
- this.#getCredentials(loxId),
- promoCred,
- this.#pubKeys
+ let response = await this.#makeRequest("trustpromo", request);
+ // FIXME: Store response to "trustpromo" in case handle_trust_promotion
+ // or "trustmig" fails. The Lox authority will not accept a re-request
+ // to "trustpromo" with the same credentials.
+ let promoCred = lazy.handle_trust_promotion(request, response);
+ lazy.logger.debug("Formatted promotion cred: ", promoCred);
+
+ request = lazy.trust_migration(cred, promoCred, this.#pubKeys);
+ response = await this.#makeRequest("trustmig", request);
+ lazy.logger.debug("Got new credential: ", response);
+
+ // FIXME: Store response to "trustmig" in case handle_trust_migration
+ // fails. The Lox authority will not accept a re-request to "trustmig" with
+ // the same credentials.
+ return lazy.handle_trust_migration(request, response);
+ })
);
- response = await this.#makeRequest("trustmig", request);
- lazy.logger.debug("Got new credential: ", response);
-
- // FIXME: Store response to "trustmig" in case handle_trust_migration
- // fails. The Lox authority will not accept a re-request to "trustmig" with
- // the same credentials.
- let cred = lazy.handle_trust_migration(request, response);
- this.#changeCredentials(loxId, cred);
- return true;
}
/**
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/8a4eb9…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/8a4eb9…
You're receiving this email because of your account on gitlab.torproject.org.