Loading toolkit/modules/TorConnect.sys.mjs +1 −656 Original line number Diff line number Diff line Loading @@ -133,581 +133,6 @@ export const TorConnectTopics = Object.freeze({ 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 { #bootstrap = null; #bootstrapError = null; #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._updateBootstrapProgress(progress, status); }; this.#bootstrap.onbootstrapcomplete = () => { this.#internetTest.cancel(); this.changeState(TorConnectState.Bootstrapped); }; 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); return; } // We have to wait for the Internet test to finish before sending the // bootstrap error this.#bootstrapError = error; 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, 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, 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; } 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; }); } 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, new TorConnectError(TorConnectError.AllSettingsFailed) ); } 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) ); } 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( [...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, ]); } 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); } } } /** * 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({ ...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); }; // 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); return; } } // Only explicitly change state here if something else has not transitioned // us. if (!this.transitioning) { throw new TorConnectError(TorConnectError.AllSettingsFailed); } } transitionRequested() { this.#transitionResolve(); } async cleanup(nextState) { // No need to await. this.#moat?.uninit(); this.#moat = null; 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); } } } } 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(_error) { 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.Disabled); } async run() { lazy.logger.debug("Entered the disabled state."); } } /** * @callback ProgressCallback * Loading Loading @@ -1433,7 +858,6 @@ class InternetTest { } export const TorConnect = { _stateHandler: new InitialState(), _bootstrapProgress: 0, _internetStatus: InternetStatus.Unknown, // list of country codes Moat has settings for Loading @@ -1454,91 +878,13 @@ export const TorConnect = { _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); // TODO: Remove. }, _updateBootstrapProgress(progress, status) { Loading @@ -1559,7 +905,6 @@ export const TorConnect = { // init should be called by TorStartupService init() { lazy.logger.debug("TorConnect.init()"); this._stateHandler.begin(); if (!this.enabled) { // Disabled Loading Loading
toolkit/modules/TorConnect.sys.mjs +1 −656 Original line number Diff line number Diff line Loading @@ -133,581 +133,6 @@ export const TorConnectTopics = Object.freeze({ 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 { #bootstrap = null; #bootstrapError = null; #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._updateBootstrapProgress(progress, status); }; this.#bootstrap.onbootstrapcomplete = () => { this.#internetTest.cancel(); this.changeState(TorConnectState.Bootstrapped); }; 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); return; } // We have to wait for the Internet test to finish before sending the // bootstrap error this.#bootstrapError = error; 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, 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, 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; } 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; }); } 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, new TorConnectError(TorConnectError.AllSettingsFailed) ); } 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) ); } 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( [...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, ]); } 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); } } } /** * 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({ ...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); }; // 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); return; } } // Only explicitly change state here if something else has not transitioned // us. if (!this.transitioning) { throw new TorConnectError(TorConnectError.AllSettingsFailed); } } transitionRequested() { this.#transitionResolve(); } async cleanup(nextState) { // No need to await. this.#moat?.uninit(); this.#moat = null; 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); } } } } 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(_error) { 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.Disabled); } async run() { lazy.logger.debug("Entered the disabled state."); } } /** * @callback ProgressCallback * Loading Loading @@ -1433,7 +858,6 @@ class InternetTest { } export const TorConnect = { _stateHandler: new InitialState(), _bootstrapProgress: 0, _internetStatus: InternetStatus.Unknown, // list of country codes Moat has settings for Loading @@ -1454,91 +878,13 @@ export const TorConnect = { _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); // TODO: Remove. }, _updateBootstrapProgress(progress, status) { Loading @@ -1559,7 +905,6 @@ export const TorConnect = { // init should be called by TorStartupService init() { lazy.logger.debug("TorConnect.init()"); this._stateHandler.begin(); if (!this.enabled) { // Disabled Loading