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/bb3cd92...
tor-commits@lists.torproject.org