[tbb-commits] [Git][tpo/applications/tor-browser][tor-browser-115.9.0esr-13.5-1] 20 commits: fixup! Bug 40597: Implement TorSettings module

Pier Angelo Vendrame (@pierov) git at gitlab.torproject.org
Tue Mar 19 07:21:12 UTC 2024



Pier Angelo Vendrame pushed to branch tor-browser-115.9.0esr-13.5-1 at The Tor Project / Applications / Tor Browser


Commits:
b8fbdc2b by Pier Angelo Vendrame at 2024-03-19T08:19:47+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Use actual private members in InternetTest, and unify the test and
testAsync methods.

- - - - -
2b75d115 by Pier Angelo Vendrame at 2024-03-19T08:19:54+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

The purpose of this commit is just to make the review easier.
It moves the various state callbacks in another part of the file (and
gives them names, to refer to them).
They are not linted/formatted on purpose, to allow checking that the
changes are minimal with the `--color-moved` option.

- - - - -
0c5e98ab by Pier Angelo Vendrame at 2024-03-19T08:19:54+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

The purpose of this commit is just to make the review easier.
It lints the previous commit to make it closer to the next form it will
take.
To review this commit, you can lint TorConnect.sys.mjs and check you
obtain the same result.

- - - - -
8708b47a by Pier Angelo Vendrame at 2024-03-19T08:19:54+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Add a specialized class for every state.
Removed TorConnectStateTransitions and store the valid transitions
inside the new classes.

- - - - -
d5d15e38 by Pier Angelo Vendrame at 2024-03-19T08:19:55+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Removes the context from StateCallback.
Instead, we will always initialize new objects.

- - - - -
929972af by Pier Angelo Vendrame at 2024-03-19T08:19:55+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Fix no-async-promise-executor on TorConnect.

Move the responsibility of starting the next state callback and broadcat
the state change from the begin function to the transition function.
This is what will actually empower us to remove the async promise
executor, because they will not have to block the state change anymore.

- - - - -
f9d3e162 by Pier Angelo Vendrame at 2024-03-19T08:19:55+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Fix no-async-promise-executor on TorConnect.

Now that callbacks have been lifted from the responsibility of blocking
the transition, they do not have to explicitly resolve anymore.
So, get rid of the various promises, and make the old callbacks regular
methods of the corresponding classes.

Also, updated the documentation.

- - - - -
d214f39b by Pier Angelo Vendrame at 2024-03-19T08:19:56+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Add the changeState method to StateCallback, other minor changes.

- - - - -
0e97bf55 by Pier Angelo Vendrame at 2024-03-19T08:19:56+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Move some parts of BootstrappingState.run to methods on their own, to
make it easier to understand this function.

- - - - -
1243f925 by Pier Angelo Vendrame at 2024-03-19T08:19:56+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Various refactors to AutoBootstrappingState.

- - - - -
34868388 by Pier Angelo Vendrame at 2024-03-19T08:19:57+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

TorConnect does not need to be defined in a function.

- - - - -
7218d77c by Pier Angelo Vendrame at 2024-03-19T08:19:57+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

First batch of changes requested in the review.

- - - - -
546449ce by Pier Angelo Vendrame at 2024-03-19T08:19:57+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

More changes on the InternetTest class.

- - - - -
5fe62195 by Pier Angelo Vendrame at 2024-03-19T08:19:58+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Additional refactor to the AutoBootstrappingState: split the run in
more methods, removed some layers of try-catch.

- - - - -
becb5e4b by Pier Angelo Vendrame at 2024-03-19T08:19:58+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Additional refactor to the AutoBootstrappingState: be consistent in
initializing private members and document that in case of race some
variables might be set to undefined.
Also, do not store the bootstrap as a member, to avoid possible race
conditions.

- - - - -
1343ed70 by Pier Angelo Vendrame at 2024-03-19T08:19:58+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

When simulating censorship, do not trigger an additional change of state
if one is already happening.

- - - - -
6bdd9dcd by Pier Angelo Vendrame at 2024-03-19T08:19:59+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Check if the current state is already transitioning, and refusing any
additional transition request.

- - - - -
22b986b8 by Pier Angelo Vendrame at 2024-03-19T08:19:59+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Do not await TorSettings.applySettings after a successfull bootstrap.
Instead, we use .catch and log any unexpected results.

- - - - -
f59ddd26 by Pier Angelo Vendrame at 2024-03-19T08:19:59+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Ignore cancel requests when in non-bootstrapping states.

- - - - -
3788b1ae by Pier Angelo Vendrame at 2024-03-19T08:20:00+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 41114: Refactor TorConnect.

Pass only the new state's name to StateCallback.end.

- - - - -


2 changed files:

- toolkit/modules/Moat.sys.mjs
- toolkit/modules/TorConnect.sys.mjs


Changes:

=====================================
toolkit/modules/Moat.sys.mjs
=====================================
@@ -85,9 +85,13 @@ export class MoatRPC {
       TorLauncherPrefs.bridgedb_reflector
     );
     const front = Services.prefs.getStringPref(TorLauncherPrefs.bridgedb_front);
-    const builder = new lazy.DomainFrontRequestBuilder();
-    await builder.init(reflector, front);
-    this.#requestBuilder = builder;
+    this.#requestBuilder = new lazy.DomainFrontRequestBuilder();
+    try {
+      await this.#requestBuilder.init(reflector, front);
+    } catch (e) {
+      this.#requestBuilder = null;
+      throw e;
+    }
   }
 
   async uninit() {


=====================================
toolkit/modules/TorConnect.sys.mjs
=====================================
@@ -117,52 +117,6 @@ XPCOMUtils.defineLazyGetter(
             └───────────────────────┘
 */
 
-/* Maps allowed state transitions
-   TorConnectStateTransitions[state] maps to an array of allowed states to transition to
-   This is just an encoding of the above transition diagram that we verify at runtime
-*/
-const TorConnectStateTransitions = Object.freeze(
-  new Map([
-    [
-      TorConnectState.Initial,
-      [
-        TorConnectState.Disabled,
-        TorConnectState.Bootstrapping,
-        TorConnectState.Configuring,
-        TorConnectState.Error,
-      ],
-    ],
-    [
-      TorConnectState.Configuring,
-      [
-        TorConnectState.AutoBootstrapping,
-        TorConnectState.Bootstrapping,
-        TorConnectState.Error,
-      ],
-    ],
-    [
-      TorConnectState.AutoBootstrapping,
-      [
-        TorConnectState.Configuring,
-        TorConnectState.Bootstrapped,
-        TorConnectState.Error,
-      ],
-    ],
-    [
-      TorConnectState.Bootstrapping,
-      [
-        TorConnectState.Configuring,
-        TorConnectState.Bootstrapped,
-        TorConnectState.Error,
-      ],
-    ],
-    [TorConnectState.Error, [TorConnectState.Configuring]],
-    [TorConnectState.Bootstrapped, [TorConnectState.Configuring]],
-    // terminal states
-    [TorConnectState.Disabled, []],
-  ])
-);
-
 /* Topics Notified by the TorConnect module */
 export const TorConnectTopics = Object.freeze({
   StateChange: "torconnect:state-change",
@@ -171,81 +125,612 @@ export const TorConnectTopics = Object.freeze({
   BootstrapError: "torconnect:bootstrap-error",
 });
 
-// The StateCallback is a wrapper around an async function which executes during
-// the lifetime of a TorConnect State. A system is also provided to allow this
-// ongoing function to early-out via a per StateCallback on_transition callback
-// which may be called externally when we need to early-out and move on to another
-// state (for example, from Bootstrapping to Configuring in the event the user
-// cancels a bootstrap attempt)
+// 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 {
-  constructor(state, callback) {
-    this._state = state;
-    this._callback = callback;
-    this._init();
-  }
+  #state;
+  #promise;
+  #transitioning = false;
 
-  _init() {
-    // this context object is bound to the callback each time transition is
-    // attempted via begin()
-    this._context = {
-      // This callback may be overwritten in the _callback for each state
-      // States may have various pieces of work which need to occur
-      // before they can be exited (eg resource cleanup)
-      // See the _stateCallbacks map for examples
-      on_transition: nextState => {},
-
-      // flag used to determine if a StateCallback should early-out
-      // its work
-      _transitioning: false,
-
-      // may be called within the StateCallback to determine if exit is possible
-      get transitioning() {
-        return this._transitioning;
-      },
-    };
+  constructor(stateName) {
+    this.#state = stateName;
   }
 
   async begin(...args) {
-    lazy.logger.trace(`Entering ${this._state} state`);
-    this._init();
+    lazy.logger.trace(`Entering ${this.#state} state`);
+    // Make sure we always have an actual promise.
     try {
-      // this Promise will block until this StateCallback has completed its work
-      await Promise.resolve(this._callback.call(this._context, ...args));
-      lazy.logger.info(`Exited ${this._state} state`);
-
-      // handled state transition
-      Services.obs.notifyObservers(
-        { state: this._nextState },
-        TorConnectTopics.StateChange
-      );
-      TorConnect._callback(this._nextState).begin(...this._nextStateArgs);
-    } catch (obj) {
-      TorConnect._changeState(
-        TorConnectState.Error,
-        obj?.message,
-        obj?.details
+      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?.message, err?.details);
+    }
+  }
+
+  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);
   }
 
-  transition(nextState, ...args) {
-    this._nextState = nextState;
-    this._nextStateArgs = [...args];
+  get transitioning() {
+    return this.#transitioning;
+  }
 
-    // calls the on_transition callback to resolve any async work or do per-state cleanup
-    // this call to on_transition should resolve the async work currentlying going on in this.begin()
-    this._context.on_transition(nextState);
-    this._context._transitioning = true;
+  get state() {
+    return this.#state;
   }
 }
 
 // async method to sleep for a given amount of time
-const debug_sleep = async ms => {
+const debugSleep = async ms => {
   return new Promise((resolve, reject) => {
     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() {
+    // The configuring state does not do anything.
+  }
+}
+
+class BootstrappingState extends StateCallback {
+  #bootstrap = null;
+  #bootstrapError = "";
+  #bootstrapErrorDetails = "";
+  #internetTest = null;
+  #cancelled = false;
+
+  allowedTransitions = Object.freeze([
+    TorConnectState.Configuring,
+    TorConnectState.Bootstrapped,
+    TorConnectState.Error,
+  ]);
+
+  constructor() {
+    super(TorConnectState.Bootstrapping);
+  }
+
+  async run() {
+    if (await this.#simulateCensorship()) {
+      return;
+    }
+
+    this.#bootstrap = new lazy.TorBootstrapRequest();
+    this.#bootstrap.onbootstrapstatus = (progress, status) => {
+      TorConnect._updateBootstrapStatus(progress, status);
+    };
+    this.#bootstrap.onbootstrapcomplete = () => {
+      this.#internetTest.cancel();
+      this.changeState(TorConnectState.Bootstrapped);
+    };
+    this.#bootstrap.onbootstraperror = (message, details) => {
+      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 => ${message}; ${details}`);
+        return;
+      }
+      // We have to wait for the Internet test to finish before sending the
+      // bootstrap error
+      this.#bootstrapError = message;
+      this.#bootstrapErrorDetails = details;
+      this.#maybeTransitionToError();
+    };
+
+    this.#internetTest = new InternetTest();
+    this.#internetTest.onResult = status => {
+      TorConnect._internetStatus = status;
+      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();
+    }
+  }
+
+  #maybeTransitionToError() {
+    if (
+      this.#internetTest.status === InternetStatus.Unknown &&
+      this.#internetTest.error === null &&
+      this.#internetTest.enabled
+    ) {
+      // We have been called by a failed bootstrap, but the internet test has
+      // not run yet - force it to run immediately!
+      this.#internetTest.test();
+      // Return from this call, because the Internet test's callback will call
+      // 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.
+    if (this.#bootstrapError === "") {
+      return;
+    }
+    if (this.#internetTest.status === InternetStatus.Offline) {
+      this.changeState(
+        TorConnectState.Error,
+        TorStrings.torConnect.offline,
+        ""
+      );
+    } else {
+      // Give priority to the bootstrap error, in case the Internet test fails
+      TorConnect._hasBootstrapEverFailed = true;
+      this.changeState(
+        TorConnectState.Error,
+        this.#bootstrapError,
+        this.#bootstrapErrorDetails
+      );
+    }
+  }
+
+  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;
+    }
+    TorConnect._hasBootstrapEverFailed = true;
+    if (censorshipLevel === 2) {
+      const codes = Object.keys(TorConnect._countryNames);
+      TorConnect._detectedLocation =
+        codes[Math.floor(Math.random() * codes.length)];
+    }
+    this.changeState(
+      TorConnectState.Error,
+      "Bootstrap failed (for debugging purposes)",
+      "Error: Censorship simulation"
+    );
+    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;
+    });
+  }
+
+  async run(countryCode) {
+    if (await this.#simulateCensorship(countryCode)) {
+      return;
+    }
+    await this.#initMoat();
+    if (this.transitioning) {
+      return;
+    }
+    await this.#fetchSettings(countryCode);
+    if (this.transitioning) {
+      return;
+    }
+    await this.#trySettings();
+  }
+
+  /**
+   * Simulate a censorship event, if needed.
+   *
+   * @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.
+   */
+  async #simulateCensorship(countryCode) {
+    const censorshipLevel = Services.prefs.getIntPref(
+      TorConnectPrefs.censorship_level,
+      0
+    );
+    if (censorshipLevel <= 0) {
+      return false;
+    }
+
+    // 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,
+          "Error: censorship simulation",
+          ""
+        );
+      }
+      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,
+          "Error: Severe Censorship simulation",
+          ""
+        );
+      }
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * 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();
+  }
+
+  /**
+   * Lookup user's potential censorship circumvention settings from Moat
+   * service.
+   */
+  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(
+        [...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([
+          ...TorSettings.builtinBridgeTypes,
+          "vanilla",
+        ]),
+        // This might set this.#settings to undefined.
+        this.#transitionPromise,
+      ]);
+    }
+
+    if (!this.#settings?.length && !this.transitioning) {
+      // Both localized and fallback have, we can just throw to transition to
+      // the error state (but only if we aren't already transitioning).
+      // TODO: Let the UI layer localize the strings.
+
+      if (!TorConnect._detectedLocation) {
+        // unable to determine country
+        this.#throwError(
+          TorStrings.torConnect.autoBootstrappingFailed,
+          TorStrings.torConnect.cannotDetermineCountry
+        );
+      } else {
+        // no settings available for country
+        this.#throwError(
+          TorStrings.torConnect.autoBootstrappingFailed,
+          TorStrings.torConnect.noSettingsForCountry
+        );
+      }
+    }
+  }
+
+  /**
+   * Try to apply the settings we fetched.
+   */
+  async #trySettings() {
+    // Otherwise, 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({
+        ...TorSettings.getSettings(),
+        ...currentSetting,
+      });
+
+      // Build out our bootstrap request.
+      const bootstrap = new lazy.TorBootstrapRequest();
+      bootstrap.onbootstrapstatus = (progress, status) => {
+        TorConnect._updateBootstrapStatus(progress, status);
+      };
+      bootstrap.onbootstraperror = (message, details) => {
+        lazy.logger.error(`Auto-Bootstrap error => ${message}; ${details}`);
+      };
+
+      // 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.
+        TorSettings.setSettings(currentSetting);
+        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.
+        TorSettings.applySettings().catch(e =>
+          lazy.logger.error("TorSettings.applySettings threw unexpectedly.", e)
+        );
+        this.changeState(TorConnectState.Bootstrapped);
+        return;
+      }
+    }
+
+    // Only explicitly change state here if something else has not transitioned
+    // us.
+    if (!this.transitioning) {
+      this.#throwError(
+        TorStrings.torConnect.autoBootstrappingFailed,
+        TorStrings.torConnect.autoBootstrappingAllFailed
+      );
+    }
+  }
+
+  #throwError(message, details) {
+    let err = new Error(message);
+    err.details = details;
+    throw err;
+  }
+
+  transitionRequested() {
+    this.#transitionResolve();
+  }
+
+  async cleanup(nextState) {
+    // No need to await.
+    this.#moat?.uninit();
+    this.#moat = null;
+
+    if (this.#changedSettings && nextState !== TorConnectState.Bootstrapped) {
+      try {
+        await 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);
+      }
+    }
+  }
+}
+
+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]);
+
+  constructor() {
+    super(TorConnectState.Bootstrapped);
+  }
+
+  run() {
+    // Notify observers of bootstrap completion.
+    Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete);
+  }
+}
+
+class ErrorState extends StateCallback {
+  allowedTransitions = Object.freeze([TorConnectState.Configuring]);
+
+  static #hasEverHappened = false;
+
+  constructor() {
+    super(TorConnectState.Error);
+    ErrorState.#hasEverHappened = true;
+  }
+
+  run(errorMessage, errorDetails) {
+    TorConnect._errorMessage = errorMessage;
+    TorConnect._errorDetails = errorDetails;
+    lazy.logger.error(
+      `Entering error state (${errorMessage}, ${errorDetails})`
+    );
+
+    Services.obs.notifyObservers(
+      { message: errorMessage, details: errorDetails },
+      TorConnectTopics.BootstrapError
+    );
+
+    this.changeState(TorConnectState.Configuring);
+  }
+
+  static get hasEverHappened() {
+    return ErrorState.#hasEverHappened;
+  }
+}
+
+class DisabledState extends StateCallback {
+  // Trap state: no way to leave the Disabled state.
+  allowedTransitions = Object.freeze([]);
+
+  constructor() {
+    super(TorConnectState.DisabledState);
+  }
+
+  async run() {
+    lazy.logger.debug("Entered the disabled state.");
+  }
+}
+
 export const InternetStatus = Object.freeze({
   Unknown: -1,
   Offline: 0,
@@ -253,1005 +738,581 @@ export const InternetStatus = Object.freeze({
 });
 
 class InternetTest {
+  #enabled;
+  #status = InternetStatus.Unknown;
+  #error = null;
+  #pending = false;
+  #canceled = false;
+  #timeout = 0;
+
   constructor() {
-    this._enabled = Services.prefs.getBoolPref(
+    this.#enabled = Services.prefs.getBoolPref(
       TorConnectPrefs.allow_internet_test,
       true
     );
-
-    this._status = InternetStatus.Unknown;
-    this._error = null;
-    this._pending = false;
-    if (this._enabled) {
-      this._timeout = setTimeout(() => {
-        this._timeout = null;
+    if (this.#enabled) {
+      this.#timeout = setTimeout(() => {
+        this.#timeout = 0;
         this.test();
-      }, this.timeoutRand());
+      }, this.#timeoutRand());
     }
-    this.onResult = (online, date) => {};
+    this.onResult = online => {};
     this.onError = err => {};
   }
 
-  test() {
-    if (this._pending || !this._enabled) {
+  /**
+   * Perform the internet test.
+   *
+   * While this is an async method, the callers are not expected to await it,
+   * as we are also using callbacks.
+   */
+  async test() {
+    if (this.#pending || !this.#enabled) {
       return;
     }
     this.cancel();
-    this._pending = true;
+    this.#pending = true;
+    this.#canceled = false;
 
     lazy.logger.info("Starting the Internet test");
-    this._testAsync()
-      .then(status => {
-        this._pending = false;
-        this._status = status.successful
-          ? InternetStatus.Online
-          : InternetStatus.Offline;
-        lazy.logger.info(`Performed Internet test, outcome ${this._status}`);
-        this.onResult(this.status, status.date);
-      })
-      .catch(error => {
-        this._error = error;
-        this._pending = false;
-        this.onError(error);
-      });
-  }
-
-  cancel() {
-    if (this._timeout !== null) {
-      clearTimeout(this._timeout);
-      this._timeout = null;
-    }
-  }
-
-  async _testAsync() {
-    // Callbacks for the Internet test are desirable, because we will be
-    // waiting both for the bootstrap, and for the Internet test.
-    // However, managing Moat with async/await is much easier as it avoids a
-    // callback hell, and it makes extra explicit that we are uniniting it.
     const mrpc = new lazy.MoatRPC();
-    let status = null;
-    let error = null;
     try {
       await mrpc.init();
-      status = await mrpc.testInternetConnection();
+      const status = await mrpc.testInternetConnection();
+      this.#status = status.successful
+        ? InternetStatus.Online
+        : InternetStatus.Offline;
+      // TODO: We could consume the date we got from the HTTP request to detect
+      // big clock skews that might prevent a successfull bootstrap.
+      lazy.logger.info(`Performed Internet test, outcome ${this.#status}`);
     } catch (err) {
       lazy.logger.error("Error while checking the Internet connection", err);
-      error = err;
+      this.#error = err;
+      this.#pending = false;
     } finally {
       mrpc.uninit();
     }
-    if (error !== null) {
-      throw error;
+
+    if (this.#canceled) {
+      return;
+    }
+    if (this.#error) {
+      this.onError(this.#error);
+    } else {
+      this.onResult(this.#status);
+    }
+  }
+
+  cancel() {
+    this.#canceled = true;
+    if (this.#timeout) {
+      clearTimeout(this.#timeout);
+      this.#timeout = 0;
     }
-    return status;
   }
 
   get status() {
-    return this._status;
+    return this.#status;
   }
 
   get error() {
-    return this._error;
+    return this.#error;
   }
 
   get enabled() {
-    return this._enabled;
+    return this.#enabled;
   }
 
-  // We randomize the Internet test timeout to make fingerprinting it harder, at least a little bit...
-  timeoutRand() {
+  // 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 TorConnect = (() => {
-  let retval = {
-    _state: TorConnectState.Initial,
-    _bootstrapProgress: 0,
-    _bootstrapStatus: null,
-    _internetStatus: InternetStatus.Unknown,
-    // list of country codes Moat has settings for
-    _countryCodes: [],
-    _countryNames: Object.freeze(
-      (() => {
-        const codes = Services.intl.getAvailableLocaleDisplayNames("region");
-        const names = Services.intl.getRegionDisplayNames(undefined, codes);
-        let codesNames = {};
-        for (let i = 0; i < codes.length; i++) {
-          codesNames[codes[i]] = names[i];
-        }
-        return codesNames;
-      })()
-    ),
-    _detectedLocation: "",
-    _errorMessage: null,
-    _errorDetails: null,
-    _logHasWarningOrError: false,
-    _hasEverFailed: 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: {},
-
-    /* These functions represent ongoing work associated with one of our states
-           Some of these functions are mostly empty, apart from defining an
-           on_transition function used to resolve their Promise */
-    _stateCallbacks: Object.freeze(
-      new Map([
-        /* Initial is never transitioned to */
-        [
-          TorConnectState.Initial,
-          new StateCallback(TorConnectState.Initial, async function () {
-            // The initial state doesn't actually do anything, so here is a skeleton for other
-            // states which do perform work
-            await new Promise(async (resolve, reject) => {
-              // This function is provided to signal to the callback that it is complete.
-              // It is called as a result of _changeState and at the very least must
-              // resolve the root Promise object within the StateCallback function
-              // The on_transition callback may also perform necessary cleanup work
-              this.on_transition = nextState => {
-                resolve();
-              };
-
-              try {
-                // each state may have a sequence of async work to do
-                let asyncWork = async () => {};
-                await asyncWork();
-
-                // after each block we may check for an opportunity to early-out
-                if (this.transitioning) {
-                  return;
-                }
-
-                // repeat the above pattern as necessary
-              } catch (err) {
-                // any thrown exceptions here will trigger a transition to the Error state
-                TorConnect._changeState(
-                  TorConnectState.Error,
-                  err?.message,
-                  err?.details
-                );
-              }
-            });
-          }),
-        ],
-        /* Configuring */
-        [
-          TorConnectState.Configuring,
-          new StateCallback(TorConnectState.Configuring, async function () {
-            await new Promise(async (resolve, reject) => {
-              this.on_transition = nextState => {
-                resolve();
-              };
-            });
-          }),
-        ],
-        /* Bootstrapping */
-        [
-          TorConnectState.Bootstrapping,
-          new StateCallback(TorConnectState.Bootstrapping, async function () {
-            // wait until bootstrap completes or we get an error
-            await new Promise(async (resolve, reject) => {
-              // debug hook to simulate censorship preventing bootstrapping
-              if (
-                Services.prefs.getIntPref(TorConnectPrefs.censorship_level, 0) >
-                0
-              ) {
-                this.on_transition = nextState => {
-                  resolve();
-                };
-                await debug_sleep(1500);
-                TorConnect._hasBootstrapEverFailed = true;
-                if (
-                  Services.prefs.getIntPref(
-                    TorConnectPrefs.censorship_level,
-                    0
-                  ) === 2
-                ) {
-                  const codes = Object.keys(TorConnect._countryNames);
-                  TorConnect._detectedLocation =
-                    codes[Math.floor(Math.random() * codes.length)];
-                }
-                TorConnect._changeState(
-                  TorConnectState.Error,
-                  "Bootstrap failed (for debugging purposes)",
-                  "Error: Censorship simulation",
-                  true
-                );
-                return;
-              }
-
-              const tbr = new lazy.TorBootstrapRequest();
-              const internetTest = new InternetTest();
-              let cancelled = false;
-
-              let bootstrapError = "";
-              let bootstrapErrorDetails = "";
-              const maybeTransitionToError = () => {
-                if (
-                  internetTest.status === InternetStatus.Unknown &&
-                  internetTest.error === null &&
-                  internetTest.enabled
-                ) {
-                  // We have been called by a failed bootstrap, but the internet test has not run yet - force
-                  // it to run immediately!
-                  internetTest.test();
-                  // Return from this call, because the Internet test's callback will call 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.
-                if (bootstrapError === "") {
-                  return;
-                }
-                if (internetTest.status === InternetStatus.Offline) {
-                  TorConnect._changeState(
-                    TorConnectState.Error,
-                    TorStrings.torConnect.offline,
-                    "",
-                    true
-                  );
-                } else {
-                  // Give priority to the bootstrap error, in case the Internet test fails
-                  TorConnect._hasBootstrapEverFailed = true;
-                  TorConnect._changeState(
-                    TorConnectState.Error,
-                    bootstrapError,
-                    bootstrapErrorDetails,
-                    true
-                  );
-                }
-              };
-
-              this.on_transition = async nextState => {
-                if (nextState === TorConnectState.Configuring) {
-                  // stop bootstrap process if user cancelled
-                  cancelled = true;
-                  internetTest.cancel();
-                  await tbr.cancel();
-                }
-                resolve();
-              };
-
-              tbr.onbootstrapstatus = (progress, status) => {
-                TorConnect._updateBootstrapStatus(progress, status);
-              };
-              tbr.onbootstrapcomplete = () => {
-                internetTest.cancel();
-                TorConnect._changeState(TorConnectState.Bootstrapped);
-              };
-              tbr.onbootstraperror = (message, details) => {
-                if (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 => ${message}; ${details}`
-                  );
-                  return;
-                }
-                // We have to wait for the Internet test to finish before sending the bootstrap error
-                bootstrapError = message;
-                bootstrapErrorDetails = details;
-                maybeTransitionToError();
-              };
-
-              internetTest.onResult = (status, date) => {
-                // TODO: Use the date to save the clock skew?
-                TorConnect._internetStatus = status;
-                maybeTransitionToError();
-              };
-              internetTest.onError = () => {
-                maybeTransitionToError();
-              };
-
-              tbr.bootstrap();
-            });
-          }),
-        ],
-        /* AutoBootstrapping */
-        [
-          TorConnectState.AutoBootstrapping,
-          new StateCallback(TorConnectState.AutoBootstrapping, async function (
-            countryCode
-          ) {
-            await new Promise(async (resolve, reject) => {
-              this.on_transition = nextState => {
-                resolve();
-              };
-
-              // debug hook to simulate censorship preventing bootstrapping
-              {
-                const censorshipLevel = Services.prefs.getIntPref(
-                  TorConnectPrefs.censorship_level,
-                  0
-                );
-                if (censorshipLevel > 1) {
-                  this.on_transition = nextState => {
-                    resolve();
-                  };
-                  // always fail even after manually selecting location specific settings
-                  if (censorshipLevel == 3) {
-                    await debug_sleep(2500);
-                    TorConnect._changeState(
-                      TorConnectState.Error,
-                      "Error: censorship simulation",
-                      "",
-                      true
-                    );
-                    return;
-                    // only fail after auto selecting, manually selecting succeeds
-                  } else if (censorshipLevel == 2 && !countryCode) {
-                    await debug_sleep(2500);
-                    TorConnect._changeState(
-                      TorConnectState.Error,
-                      "Error: Severe Censorship simulation",
-                      "",
-                      true
-                    );
-                    return;
-                  }
-                }
-              }
-
-              const throw_error = (message, details) => {
-                let err = new Error(message);
-                err.details = details;
-                throw err;
-              };
-
-              // lookup user's potential censorship circumvention settings from Moat service
-              try {
-                this.mrpc = new lazy.MoatRPC();
-                await this.mrpc.init();
-
-                if (this.transitioning) {
-                  return;
-                }
-
-                const settings = await this.mrpc.circumvention_settings(
-                  [...TorSettings.builtinBridgeTypes, "vanilla"],
-                  countryCode
-                );
-
-                if (this.transitioning) {
-                  return;
-                }
-
-                if (settings?.country) {
-                  TorConnect._detectedLocation = settings.country;
-                }
-                if (settings?.settings && settings.settings.length) {
-                  this.settings = settings.settings;
-                } else {
-                  try {
-                    this.settings = await this.mrpc.circumvention_defaults([
-                      ...TorSettings.builtinBridgeTypes,
-                      "vanilla",
-                    ]);
-                  } catch (err) {
-                    lazy.logger.error(
-                      "We did not get localized settings, and default settings failed as well",
-                      err
-                    );
-                  }
-                }
-                if (this.settings === null || this.settings.length === 0) {
-                  // The fallback has failed as well, so throw the original error
-                  if (!TorConnect._detectedLocation) {
-                    // unable to determine country
-                    throw_error(
-                      TorStrings.torConnect.autoBootstrappingFailed,
-                      TorStrings.torConnect.cannotDetermineCountry
-                    );
-                  } else {
-                    // no settings available for country
-                    throw_error(
-                      TorStrings.torConnect.autoBootstrappingFailed,
-                      TorStrings.torConnect.noSettingsForCountry
-                    );
-                  }
-                }
-
-                const restoreOriginalSettings = async () => {
-                  try {
-                    await 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);
-                  }
-                };
-
-                // apply each of our settings and try to bootstrap with each
-                try {
-                  for (const [
-                    index,
-                    currentSetting,
-                  ] of this.settings.entries()) {
-                    // we want to break here so we can fall through and restore original settings
-                    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();
-                    // We need to merge with old settings, in case the user is
-                    // using a proxy or is behind a firewall.
-                    await provider.writeSettings({
-                      ...TorSettings.getSettings(),
-                      ...currentSetting,
-                    });
-
-                    // build out our bootstrap request
-                    const tbr = new lazy.TorBootstrapRequest();
-                    tbr.onbootstrapstatus = (progress, status) => {
-                      TorConnect._updateBootstrapStatus(progress, status);
-                    };
-                    tbr.onbootstraperror = (message, details) => {
-                      lazy.logger.error(
-                        `Auto-Bootstrap error => ${message}; ${details}`
-                      );
-                    };
-
-                    // update transition callback for user cancel
-                    this.on_transition = async nextState => {
-                      if (nextState === TorConnectState.Configuring) {
-                        await tbr.cancel();
-                        await restoreOriginalSettings();
-                      }
-                      resolve();
-                    };
-
-                    // begin bootstrap
-                    if (await tbr.bootstrap()) {
-                      // persist the current settings to preferences
-                      TorSettings.setSettings(currentSetting);
-                      TorSettings.saveToPrefs();
-                      await TorSettings.applySettings();
-                      TorConnect._changeState(TorConnectState.Bootstrapped);
-                      return;
-                    }
-                  }
-
-                  // Bootstrap failed for all potential settings, so restore the
-                  // original settings the provider.
-                  await restoreOriginalSettings();
-
-                  // Only explicitly change state here if something else has not
-                  // transitioned us.
-                  if (!this.transitioning) {
-                    throw_error(
-                      TorStrings.torConnect.autoBootstrappingFailed,
-                      TorStrings.torConnect.autoBootstrappingAllFailed
-                    );
-                  }
-                  return;
-                } catch (err) {
-                  await restoreOriginalSettings();
-                  // throw to outer catch to transition us.
-                  throw err;
-                }
-              } catch (err) {
-                if (this.mrpc?.inited) {
-                  // lookup countries which have settings available
-                  TorConnect._countryCodes =
-                    await this.mrpc.circumvention_countries();
-                }
-                if (!this.transitioning) {
-                  TorConnect._changeState(
-                    TorConnectState.Error,
-                    err?.message,
-                    err?.details,
-                    true
-                  );
-                } else {
-                  lazy.logger.error(
-                    "Received AutoBootstrapping error after transitioning",
-                    err
-                  );
-                }
-              } finally {
-                // important to uninit MoatRPC object or else the pt process will live as long as tor-browser
-                this.mrpc?.uninit();
-              }
-            });
-          }),
-        ],
-        /* Bootstrapped */
-        [
-          TorConnectState.Bootstrapped,
-          new StateCallback(TorConnectState.Bootstrapped, async function () {
-            await new Promise((resolve, reject) => {
-              // We may need to leave the bootstrapped state if the tor daemon
-              // exits (if it is restarted, we will have to bootstrap again).
-              this.on_transition = nextState => {
-                resolve();
-              };
-              // notify observers of bootstrap completion
-              Services.obs.notifyObservers(
-                null,
-                TorConnectTopics.BootstrapComplete
-              );
-            });
-          }),
-        ],
-        /* Error */
-        [
-          TorConnectState.Error,
-          new StateCallback(TorConnectState.Error, async function (
-            errorMessage,
-            errorDetails,
-            bootstrappingFailure
-          ) {
-            await new Promise((resolve, reject) => {
-              this.on_transition = async nextState => {
-                resolve();
-              };
-
-              TorConnect._errorMessage = errorMessage;
-              TorConnect._errorDetails = errorDetails;
-              lazy.logger.error(
-                `Entering error state (${errorMessage}, ${errorDetails})`
-              );
-
-              Services.obs.notifyObservers(
-                { message: errorMessage, details: errorDetails },
-                TorConnectTopics.BootstrapError
-              );
-
-              TorConnect._changeState(TorConnectState.Configuring);
-            });
-          }),
-        ],
-        /* Disabled */
-        [
-          TorConnectState.Disabled,
-          new StateCallback(TorConnectState.Disabled, async function () {
-            await new Promise((resolve, reject) => {
-              // no-op, on_transition not defined because no way to leave Disabled state
-            });
-          }),
-        ],
-      ])
-    ),
-
-    _callback(state) {
-      return this._stateCallbacks.get(state);
-    },
-
-    _changeState(newState, ...args) {
-      if (newState === TorConnectState.Error) {
-        this._hasEverFailed = true;
+export const TorConnect = {
+  _stateHandler: new InitialState(),
+  _bootstrapProgress: 0,
+  _bootstrapStatus: null,
+  _internetStatus: InternetStatus.Unknown,
+  // list of country codes Moat has settings for
+  _countryCodes: [],
+  _countryNames: Object.freeze(
+    (() => {
+      const codes = Services.intl.getAvailableLocaleDisplayNames("region");
+      const names = Services.intl.getRegionDisplayNames(undefined, codes);
+      let codesNames = {};
+      for (let i = 0; i < codes.length; i++) {
+        codesNames[codes[i]] = names[i];
       }
-      const prevState = this._state;
+      return codesNames;
+    })()
+  ),
+  _detectedLocation: "",
+  _errorMessage: 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}`
+      );
+    }
 
-      // ensure this is a valid state transition
-      if (!TorConnectStateTransitions.get(prevState)?.includes(newState)) {
-        throw Error(
-          `TorConnect: Attempted invalid state transition from ${prevState} 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;
       }
+    }
 
-      lazy.logger.trace(`Try transitioning from ${prevState} to ${newState}`);
-
-      // set our new state first so that state transitions can themselves trigger
-      // a state transition
-      this._state = newState;
-
-      // call our state function and forward any args
-      this._callback(prevState).transition(newState, ...args);
-    },
+    // Set our new state first so that state transitions can themselves
+    // trigger a state transition.
+    this._stateHandler = this._makeState(newState);
+    Services.obs.notifyObservers(
+      { state: newState },
+      TorConnectTopics.StateChange
+    );
+    this._stateHandler.begin(...args);
+  },
 
-    _updateBootstrapStatus(progress, status) {
-      this._bootstrapProgress = progress;
-      this._bootstrapStatus = status;
+  _updateBootstrapStatus(progress, status) {
+    this._bootstrapProgress = progress;
+    this._bootstrapStatus = status;
 
-      lazy.logger.info(
-        `Bootstrapping ${this._bootstrapProgress}% complete (${this._bootstrapStatus})`
-      );
-      Services.obs.notifyObservers(
-        {
-          progress: TorConnect._bootstrapProgress,
-          status: TorConnect._bootstrapStatus,
-          hasWarnings: TorConnect._logHasWarningOrError,
-        },
-        TorConnectTopics.BootstrapProgress
-      );
-    },
+    lazy.logger.info(
+      `Bootstrapping ${this._bootstrapProgress}% complete (${this._bootstrapStatus})`
+    );
+    Services.obs.notifyObservers(
+      {
+        progress: TorConnect._bootstrapProgress,
+        status: TorConnect._bootstrapStatus,
+        hasWarnings: TorConnect._logHasWarningOrError,
+      },
+      TorConnectTopics.BootstrapProgress
+    );
+  },
+
+  // 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}'`);
+      };
+
+      // 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.
+      TorSettings.initializedPromise.then(() => this._settingsInitialized());
+
+      // register the Tor topics we always care about
+      observeTopic(TorTopics.ProcessExited);
+      observeTopic(TorTopics.LogHasWarnOrErr);
+    }
+  },
 
-    // init should be called by TorStartupService
-    init() {
-      lazy.logger.debug("TorConnect.init()");
-      this._callback(TorConnectState.Initial).begin();
+  async observe(subject, topic, data) {
+    lazy.logger.debug(`Observed ${topic}`);
 
-      if (!this.enabled) {
-        // Disabled
-        this._changeState(TorConnectState.Disabled);
-      } else {
-        let observeTopic = addTopic => {
-          Services.obs.addObserver(this, addTopic);
-          lazy.logger.debug(`Observing topic '${addTopic}'`);
-        };
-
-        // 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.
-        TorSettings.initializedPromise.then(() => this._settingsInitialized());
-
-        // register the Tor topics we always care about
-        observeTopic(TorTopics.ProcessExited);
-        observeTopic(TorTopics.LogHasWarnOrErr);
+    switch (topic) {
+      case TorTopics.LogHasWarnOrErr: {
+        this._logHasWarningOrError = true;
+        break;
       }
-    },
-
-    async observe(subject, topic, data) {
-      lazy.logger.debug(`Observed ${topic}`);
-
-      switch (topic) {
-        case TorTopics.LogHasWarnOrErr: {
-          this._logHasWarningOrError = true;
-          break;
+      case TorTopics.ProcessExited: {
+        // 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.
         }
-        case TorTopics.ProcessExited: {
-          // 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.
-          }
-          break;
-        }
-        default:
-          // ignore
-          break;
-      }
-    },
-
-    async _settingsInitialized() {
-      // TODO: Handle failures here, instead of the prompt to restart the
-      // 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...).
-      // 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.");
-      if (this.shouldQuickStart) {
-        // Quickstart
-        this._changeState(TorConnectState.Bootstrapping);
-      } else {
-        // Configuring
-        this._changeState(TorConnectState.Configuring);
+        break;
       }
-    },
-
-    /*
-        Various getters
-        */
-
-    /**
-     * Whether TorConnect is enabled.
-     *
-     * @type {boolean}
-     */
-    get enabled() {
-      // FIXME: This is called before the TorProvider is ready.
-      // As a matter of fact, at the moment it is equivalent to the following
-      // line, but this might become a problem in the future.
-      return TorLauncherUtil.shouldStartAndOwnTor;
-    },
-
-    get shouldShowTorConnect() {
-      // TorBrowser must control the daemon
-      return (
-        this.enabled &&
-        // if we have succesfully bootstraped, then no need to show TorConnect
-        this.state !== TorConnectState.Bootstrapped
-      );
-    },
-
-    /**
-     * Whether bootstrapping can currently begin.
-     *
-     * The value may change with TorConnectTopics.StateChanged.
-     *
-     * @param {boolean}
-     */
-    get canBeginBootstrap() {
-      return TorConnectStateTransitions.get(this.state).includes(
-        TorConnectState.Bootstrapping
-      );
-    },
-
-    /**
-     * Whether auto-bootstrapping can currently begin.
-     *
-     * The value may change with TorConnectTopics.StateChanged.
-     *
-     * @param {boolean}
-     */
-    get canBeginAutoBootstrap() {
-      return TorConnectStateTransitions.get(this.state).includes(
-        TorConnectState.AutoBootstrapping
-      );
-    },
-
-    get shouldQuickStart() {
-      // quickstart must be enabled
-      return (
-        TorSettings.quickstart.enabled &&
-        // and the previous bootstrap attempt must have succeeded
-        !Services.prefs.getBoolPref(TorLauncherPrefs.prompt_at_startup, true)
+      default:
+        // ignore
+        break;
+    }
+  },
+
+  async _settingsInitialized() {
+    // TODO: Handle failures here, instead of the prompt to restart the
+    // 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."
       );
-    },
-
-    get state() {
-      return this._state;
-    },
-
-    get bootstrapProgress() {
-      return this._bootstrapProgress;
-    },
+      return;
+    }
+    lazy.logger.debug("The TorProvider is ready, changing state.");
+    if (this.shouldQuickStart) {
+      // Quickstart
+      this._changeState(TorConnectState.Bootstrapping);
+    } else {
+      // Configuring
+      this._changeState(TorConnectState.Configuring);
+    }
+  },
 
-    get bootstrapStatus() {
-      return this._bootstrapStatus;
-    },
+  /*
+    Various getters
+   */
 
-    get internetStatus() {
-      return this._internetStatus;
-    },
+  /**
+   * Whether TorConnect is enabled.
+   *
+   * @type {boolean}
+   */
+  get enabled() {
+    // FIXME: This is called before the TorProvider is ready.
+    // As a matter of fact, at the moment it is equivalent to the following
+    // line, but this might become a problem in the future.
+    return TorLauncherUtil.shouldStartAndOwnTor;
+  },
+
+  get shouldShowTorConnect() {
+    // TorBrowser must control the daemon
+    return (
+      this.enabled &&
+      // if we have succesfully bootstraped, then no need to show TorConnect
+      this.state !== TorConnectState.Bootstrapped
+    );
+  },
+
+  /**
+   * Whether bootstrapping can currently begin.
+   *
+   * The value may change with TorConnectTopics.StateChanged.
+   *
+   * @param {boolean}
+   */
+  get canBeginBootstrap() {
+    return this._stateHandler.allowedTransitions.includes(
+      TorConnectState.Bootstrapping
+    );
+  },
+
+  /**
+   * Whether auto-bootstrapping can currently begin.
+   *
+   * The value may change with TorConnectTopics.StateChanged.
+   *
+   * @param {boolean}
+   */
+  get canBeginAutoBootstrap() {
+    return this._stateHandler.allowedTransitions.includes(
+      TorConnectState.AutoBootstrapping
+    );
+  },
+
+  get shouldQuickStart() {
+    // quickstart must be enabled
+    return (
+      TorSettings.quickstart.enabled &&
+      // and the previous bootstrap attempt must have succeeded
+      !Services.prefs.getBoolPref(TorLauncherPrefs.prompt_at_startup, true)
+    );
+  },
+
+  get state() {
+    return this._stateHandler.state;
+  },
+
+  get bootstrapProgress() {
+    return this._bootstrapProgress;
+  },
+
+  get bootstrapStatus() {
+    return this._bootstrapStatus;
+  },
+
+  get internetStatus() {
+    return this._internetStatus;
+  },
+
+  get countryCodes() {
+    return this._countryCodes;
+  },
+
+  get countryNames() {
+    return this._countryNames;
+  },
+
+  get detectedLocation() {
+    return this._detectedLocation;
+  },
+
+  get errorMessage() {
+    return this._errorMessage;
+  },
+
+  get errorDetails() {
+    return this._errorDetails;
+  },
+
+  get logHasWarningOrError() {
+    return this._logHasWarningOrError;
+  },
+
+  /**
+   * Whether we have ever entered the Error state.
+   *
+   * @type {boolean}
+   */
+  get hasEverFailed() {
+    return ErrorState.hasEverHappened;
+  },
+
+  /**
+   * Whether the Bootstrapping process has ever failed, not including when it
+   * failed due to not being connected to the internet.
+   *
+   * This does not include a failure in AutoBootstrapping.
+   *
+   * @type {boolean}
+   */
+  get potentiallyBlocked() {
+    return this._hasBootstrapEverFailed;
+  },
+
+  get uiState() {
+    return this._uiState;
+  },
+  set uiState(newState) {
+    this._uiState = newState;
+  },
+
+  /*
+    These functions allow external consumers to tell TorConnect to transition states
+   */
+
+  beginBootstrap() {
+    lazy.logger.debug("TorConnect.beginBootstrap()");
+    this._changeState(TorConnectState.Bootstrapping);
+  },
+
+  cancelBootstrap() {
+    lazy.logger.debug("TorConnect.cancelBootstrap()");
+    if (
+      this.state !== TorConnectState.AutoBootstrapping &&
+      this.state !== TorConnectState.Bootstrapping
+    ) {
+      lazy.logger.warn(
+        `Cannot cancel bootstrapping in the ${this.state} state`
+      );
+      return;
+    }
+    this._changeState(TorConnectState.Configuring);
+  },
+
+  beginAutoBootstrap(countryCode) {
+    lazy.logger.debug("TorConnect.beginAutoBootstrap()");
+    this._changeState(TorConnectState.AutoBootstrapping, countryCode);
+  },
+
+  /*
+    Further external commands and helper methods
+   */
+  openTorPreferences() {
+    if (TorLauncherUtil.isAndroid) {
+      lazy.EventDispatcher.instance.sendRequest({
+        type: "GeckoView:Tor:OpenSettings",
+      });
+      return;
+    }
+    const win = lazy.BrowserWindowTracker.getTopWindow();
+    win.switchToTabHavingURI("about:preferences#connection", true);
+  },
+
+  /**
+   * Open the "about:torconnect" tab.
+   *
+   * Bootstrapping or AutoBootstrapping can also be automatically triggered at
+   * the same time, if the current state allows for it.
+   *
+   * Bootstrapping will not be triggered if the connection is
+   * 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.
+   */
+  openTorConnect(options) {
+    const win = lazy.BrowserWindowTracker.getTopWindow();
+    win.switchToTabHavingURI("about:torconnect", true, {
+      ignoreQueryString: true,
+    });
+    if (
+      options?.beginBootstrap &&
+      this.canBeginBootstrap &&
+      !this.potentiallyBlocked
+    ) {
+      this.beginBootstrap();
+    }
+    // options.beginAutoBootstrap can be an empty string.
+    if (
+      options?.beginAutoBootstrap !== undefined &&
+      this.canBeginAutoBootstrap
+    ) {
+      this.beginAutoBootstrap(options.beginAutoBootstrap);
+    }
+  },
 
-    get countryCodes() {
-      return this._countryCodes;
-    },
-
-    get countryNames() {
-      return this._countryNames;
-    },
-
-    get detectedLocation() {
-      return this._detectedLocation;
-    },
-
-    get errorMessage() {
-      return this._errorMessage;
-    },
-
-    get errorDetails() {
-      return this._errorDetails;
-    },
-
-    get logHasWarningOrError() {
-      return this._logHasWarningOrError;
-    },
-
-    /**
-     * Whether we have ever entered the Error state.
-     *
-     * @type {boolean}
-     */
-    get hasEverFailed() {
-      return this._hasEverFailed;
-    },
-
-    /**
-     * Whether the Bootstrapping process has ever failed, not including when it
-     * failed due to not being connected to the internet.
-     *
-     * This does not include a failure in AutoBootstrapping.
-     *
-     * @type {boolean}
-     */
-    get potentiallyBlocked() {
-      return this._hasBootstrapEverFailed;
-    },
-
-    get uiState() {
-      return this._uiState;
-    },
-    set uiState(newState) {
-      this._uiState = newState;
-    },
-
-    /*
-        These functions allow external consumers to tell TorConnect to transition states
-        */
-
-    beginBootstrap() {
-      lazy.logger.debug("TorConnect.beginBootstrap()");
-      this._changeState(TorConnectState.Bootstrapping);
-    },
+  viewTorLogs() {
+    const win = lazy.BrowserWindowTracker.getTopWindow();
+    win.switchToTabHavingURI("about:preferences#connection-viewlogs", true);
+  },
 
-    cancelBootstrap() {
-      lazy.logger.debug("TorConnect.cancelBootstrap()");
-      this._changeState(TorConnectState.Configuring);
-    },
-
-    beginAutoBootstrap(countryCode) {
-      lazy.logger.debug("TorConnect.beginAutoBootstrap()");
-      this._changeState(TorConnectState.AutoBootstrapping, countryCode);
-    },
-
-    /*
-        Further external commands and helper methods
-        */
-    openTorPreferences() {
-      if (TorLauncherUtil.isAndroid) {
-        lazy.EventDispatcher.instance.sendRequest({
-          type: "GeckoView:Tor:OpenSettings",
-        });
-        return;
-      }
-      const win = lazy.BrowserWindowTracker.getTopWindow();
-      win.switchToTabHavingURI("about:preferences#connection", true);
-    },
-
-    /**
-     * Open the "about:torconnect" tab.
-     *
-     * Bootstrapping or AutoBootstrapping can also be automatically triggered at
-     * the same time, if the current state allows for it.
-     *
-     * Bootstrapping will not be triggered if the connection is
-     * 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.
-     */
-    openTorConnect(options) {
-      const win = lazy.BrowserWindowTracker.getTopWindow();
-      win.switchToTabHavingURI("about:torconnect", true, {
-        ignoreQueryString: true,
-      });
-      if (
-        options?.beginBootstrap &&
-        this.canBeginBootstrap &&
-        !this.potentiallyBlocked
-      ) {
-        this.beginBootstrap();
-      }
-      // options.beginAutoBootstrap can be an empty string.
-      if (
-        options?.beginAutoBootstrap !== undefined &&
-        this.canBeginAutoBootstrap
-      ) {
-        this.beginAutoBootstrap(options.beginAutoBootstrap);
-      }
-    },
-
-    viewTorLogs() {
-      const win = lazy.BrowserWindowTracker.getTopWindow();
-      win.switchToTabHavingURI("about:preferences#connection-viewlogs", true);
-    },
-
-    async getCountryCodes() {
-      // Difference with the getter: this is to be called by TorConnectParent, and downloads
-      // the country codes if they are not already in cache.
-      if (this._countryCodes.length) {
-        return this._countryCodes;
-      }
-      const mrpc = new lazy.MoatRPC();
-      try {
-        await mrpc.init();
-        this._countryCodes = await mrpc.circumvention_countries();
-      } catch (err) {
-        lazy.logger.error(
-          "An error occurred while fetching country codes",
-          err
-        );
-      } finally {
-        mrpc.uninit();
-      }
+  async getCountryCodes() {
+    // Difference with the getter: this is to be called by TorConnectParent, and
+    // downloads the country codes if they are not already in cache.
+    if (this._countryCodes.length) {
       return this._countryCodes;
-    },
-
-    getRedirectURL(url) {
-      return `about:torconnect?redirect=${encodeURIComponent(url)}`;
-    },
-
-    /**
-     * Convert the given object into a list of valid URIs.
-     *
-     * The object is either from the user's homepage preference (which may
-     * contain multiple domains separated by "|") or uris passed to the browser
-     * via command-line.
-     *
-     * @param {string|string[]} uriVariant - The string to extract uris from.
-     *
-     * @return {string[]} - The array of uris found.
-     */
-    fixupURIs(uriVariant) {
-      let uriArray;
-      if (typeof uriVariant === "string") {
-        uriArray = uriVariant.split("|");
-      } else if (
-        Array.isArray(uriVariant) &&
-        uriVariant.every(entry => typeof entry === "string")
-      ) {
-        uriArray = uriVariant;
-      } else {
-        // about:tor as safe fallback
-        lazy.logger.error(
-          `Received unknown variant '${JSON.stringify(uriVariant)}'`
-        );
-        uriArray = ["about:tor"];
-      }
-
-      // Attempt to convert user-supplied string to a uri, fallback to
-      // about:tor if cannot convert to valid uri object
-      return uriArray.map(
-        uriString =>
-          Services.uriFixup.getFixupURIInfo(
-            uriString,
-            Ci.nsIURIFixup.FIXUP_FLAG_NONE
-          ).preferredURI?.spec ?? "about:tor"
+    }
+    const mrpc = new lazy.MoatRPC();
+    try {
+      await mrpc.init();
+      this._countryCodes = await mrpc.circumvention_countries();
+    } catch (err) {
+      lazy.logger.error("An error occurred while fetching country codes", err);
+    } finally {
+      mrpc.uninit();
+    }
+    return this._countryCodes;
+  },
+
+  getRedirectURL(url) {
+    return `about:torconnect?redirect=${encodeURIComponent(url)}`;
+  },
+
+  /**
+   * Convert the given object into a list of valid URIs.
+   *
+   * The object is either from the user's homepage preference (which may
+   * contain multiple domains separated by "|") or uris passed to the browser
+   * via command-line.
+   *
+   * @param {string|string[]} uriVariant - The string to extract uris from.
+   *
+   * @return {string[]} - The array of uris found.
+   */
+  fixupURIs(uriVariant) {
+    let uriArray;
+    if (typeof uriVariant === "string") {
+      uriArray = uriVariant.split("|");
+    } else if (
+      Array.isArray(uriVariant) &&
+      uriVariant.every(entry => typeof entry === "string")
+    ) {
+      uriArray = uriVariant;
+    } else {
+      // about:tor as safe fallback
+      lazy.logger.error(
+        `Received unknown variant '${JSON.stringify(uriVariant)}'`
       );
-    },
-
-    // called from browser.js on browser startup, passed in either the user's homepage(s)
-    // or uris passed via command-line; we want to replace them with about:torconnect uris
-    // which redirect after bootstrapping
-    getURIsToLoad(uriVariant) {
-      const uris = this.fixupURIs(uriVariant);
-      lazy.logger.debug(`Will load after bootstrap => [${uris.join(", ")}]`);
-      return uris.map(uri => this.getRedirectURL(uri));
-    },
-  };
-  return retval;
-})(); /* TorConnect */
+      uriArray = ["about:tor"];
+    }
+
+    // Attempt to convert user-supplied string to a uri, fallback to
+    // about:tor if cannot convert to valid uri object
+    return uriArray.map(
+      uriString =>
+        Services.uriFixup.getFixupURIInfo(
+          uriString,
+          Ci.nsIURIFixup.FIXUP_FLAG_NONE
+        ).preferredURI?.spec ?? "about:tor"
+    );
+  },
+
+  // called from browser.js on browser startup, passed in either the user's homepage(s)
+  // or uris passed via command-line; we want to replace them with about:torconnect uris
+  // which redirect after bootstrapping
+  getURIsToLoad(uriVariant) {
+    const uris = this.fixupURIs(uriVariant);
+    lazy.logger.debug(`Will load after bootstrap => [${uris.join(", ")}]`);
+    return uris.map(uri => this.getRedirectURL(uri));
+  },
+};



View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/52233769c0b98cc8e1d415d6bd26ed893184e7b8...3788b1aec7f2f95540e0b3185608cd8c8d2a4222

-- 
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/52233769c0b98cc8e1d415d6bd26ed893184e7b8...3788b1aec7f2f95540e0b3185608cd8c8d2a4222
You're receiving this email because of your account on gitlab.torproject.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.torproject.org/pipermail/tbb-commits/attachments/20240319/989d4547/attachment-0001.htm>


More information about the tbb-commits mailing list