commit 7f8276faefc398180879e755a2cf1c87eb28b715 Author: Richard Pospesel richard@torproject.org Date: Thu Nov 11 13:03:23 2021 +0100
fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser --- .../torconnect/content/aboutTorConnect.js | 6 +- browser/modules/TorConnect.jsm | 541 +++++++++++++-------- browser/modules/TorProtocolService.jsm | 125 ++++- 3 files changed, 466 insertions(+), 206 deletions(-)
diff --git a/browser/components/torconnect/content/aboutTorConnect.js b/browser/components/torconnect/content/aboutTorConnect.js index b53f8b13cb80..26b17afb6938 100644 --- a/browser/components/torconnect/content/aboutTorConnect.js +++ b/browser/components/torconnect/content/aboutTorConnect.js @@ -144,7 +144,7 @@ class AboutTorConnect { this.hide(this.elements.cancelButton); }
- update_AutoConfiguring(state) { + update_AutoBootstrapping(state) { // TODO: noop until this state is used }
@@ -180,10 +180,6 @@ class AboutTorConnect { this.hide(this.elements.cancelButton); }
- update_FatalError(state) { - // TODO: noop until this state is used - } - update_Bootstrapped(state) { const hasError = false; const showProgressbar = true; diff --git a/browser/modules/TorConnect.jsm b/browser/modules/TorConnect.jsm index ddc14148eb88..7c8580b5d8a9 100644 --- a/browser/modules/TorConnect.jsm +++ b/browser/modules/TorConnect.jsm @@ -10,7 +10,7 @@ const { BrowserWindowTracker } = ChromeUtils.import( "resource:///modules/BrowserWindowTracker.jsm" );
-const { TorProtocolService, TorProcessStatus } = ChromeUtils.import( +const { TorProtocolService, TorProcessStatus, TorTopics, TorBootstrapRequest } = ChromeUtils.import( "resource:///modules/TorProtocolService.jsm" );
@@ -18,23 +18,17 @@ const { TorLauncherUtil } = ChromeUtils.import( "resource://torlauncher/modules/tl-util.jsm" );
-const { TorSettings, TorSettingsTopics } = ChromeUtils.import( +const { TorSettings, TorSettingsTopics, TorBridgeSource, TorBuiltinBridgeTypes, TorProxyType } = ChromeUtils.import( "resource:///modules/TorSettings.jsm" );
+const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm"); + /* Browser observer topis */ const BrowserTopics = Object.freeze({ ProfileAfterChange: "profile-after-change", });
-/* tor-launcher observer topics */ -const TorTopics = Object.freeze({ - BootstrapStatus: "TorBootstrapStatus", - BootstrapError: "TorBootstrapError", - ProcessExited: "TorProcessExited", - LogHasWarnOrErr: "TorLogHasWarnOrErr", -}); - /* Relevant prefs used by tor-launcher */ const TorLauncherPrefs = Object.freeze({ prompt_at_startup: "extensions.torlauncher.prompt_at_startup", @@ -45,14 +39,12 @@ const TorConnectState = Object.freeze({ Initial: "Initial", /* In-between initial boot and bootstrapping, users can change tor network settings during this state */ Configuring: "Configuring", - /* Geo-location and setting bridges/etc */ - AutoConfiguring: "AutoConfiguring", + /* Tor is attempting to bootstrap with settings from censorship-circumvention db */ + AutoBootstrapping: "AutoBootstrapping", /* Tor is bootstrapping */ Bootstrapping: "Bootstrapping", - /* Passthrough state back to Configuring or Fatal */ + /* Passthrough state back to Configuring */ Error: "Error", - /* An unrecoverable error */ - FatalError: "FatalError", /* Final state, after successful bootstrap */ Bootstrapped: "Bootstrapped", /* If we are using System tor or the legacy Tor-Launcher */ @@ -60,60 +52,54 @@ const TorConnectState = Object.freeze({ });
/* - - TorConnect State Transitions - - ┌──────────────────────┐ - │ Disabled │ - └──────────────────────┘ - ▲ - │ legacyOrSystemTor() - │ - ┌──────────────────────┐ - ┌────────────────────── │ Initial │ ───────────────────────────┐ - │ └──────────────────────┘ │ - │ │ │ - │ │ beginBootstrap() │ - │ ▼ │ -┌────────────────┐ │ bootstrapComplete() ┌────────────────────────────────────────────────┐ │ beginBootstrap() -│ Bootstrapped │ ◀──┼────────────────────── │ Bootstrapping │ ◀┼─────────────────┐ -└────────────────┘ │ └────────────────────────────────────────────────┘ │ │ - │ │ ▲ │ │ │ - │ │ cancelBootstrap() │ beginBootstrap() └────┼─────────────┐ │ - │ ▼ │ │ │ │ - │ beginConfigure() ┌────────────────────────────────────────────────┐ │ │ │ - └─────────────────────▶ │ │ │ │ │ - │ │ │ │ │ - beginConfigure() │ │ │ │ │ - ┌──────────────────────────▶ │ Configuring │ │ │ │ - │ │ │ │ │ │ - │ │ │ │ │ │ - │ ┌─────────────────────▶ │ │ │ │ │ - │ │ └────────────────────────────────────────────────┘ │ │ │ - │ │ │ │ │ │ │ - │ │ cancelAutoconfigure() │ autoConfigure() │ ┌────┼─────────────┼───┘ - │ │ ▼ │ │ │ │ - │ │ ┌──────────────────────┐ │ │ │ │ - │ └────────────────────── │ AutoConfiguring │ ─┼────────────────────┘ │ │ - │ └──────────────────────┘ │ │ │ - │ │ │ │ onError() │ - │ │ onError() │ onError() │ │ - │ ▼ ▼ │ │ - │ ┌────────────────────────────────────────────────┐ │ │ - └─────────────────────────── │ Error │ ◀┘ │ - └────────────────────────────────────────────────┘ │ - │ ▲ onError() │ - │ onFatalError() └──────────────────┘ - ▼ - ┌──────────────────────┐ - │ FatalError │ - └──────────────────────┘ - + TorConnect State Transitions + + ┌─────────┐ ┌────────┐ + │ ▼ ▼ │ + │ ┌──────────────────────────────────────────────────────────┐ │ + ┌─┼────── │ Error │ ◀───┐ │ + │ │ └──────────────────────────────────────────────────────────┘ │ │ + │ │ ▲ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ ┌───────────────────────┐ ┌──────────┐ │ │ + │ │ ┌──── │ Initial │ ────────────────────▶ │ Disabled │ │ │ + │ │ │ └───────────────────────┘ └──────────┘ │ │ + │ │ │ │ │ │ + │ │ │ │ beginBootstrap() │ │ + │ │ │ ▼ │ │ + │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ + │ │ │ │ Bootstrapping │ ────┘ │ + │ │ │ └──────────────────────────────────────────────────────────┘ │ + │ │ │ │ ▲ │ │ + │ │ │ │ cancelBootstrap() │ beginBootstrap() └────┐ │ + │ │ │ ▼ │ │ │ + │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ + │ │ └───▶ │ │ ─┼────┘ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ Configuring │ │ + │ │ │ │ │ + │ │ │ │ │ + └─┼─────▶ │ │ │ + │ └──────────────────────────────────────────────────────────┘ │ + │ │ ▲ │ + │ │ beginAutoBootstrap() │ cancelAutoBootstrap() │ + │ ▼ │ │ + │ ┌───────────────────────┐ │ │ + └────── │ AutoBootstrapping │ ─┘ │ + └───────────────────────┘ │ + │ │ + │ │ + ▼ │ + ┌───────────────────────┐ │ + │ Bootstrapped │ ◀───────────────────────────────────┘ + └───────────────────────┘ */
- /* 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([ @@ -123,22 +109,20 @@ const TorConnectStateTransitions = TorConnectState.Configuring, TorConnectState.Error]], [TorConnectState.Configuring, - [TorConnectState.AutoConfiguring, + [TorConnectState.AutoBootstrapping, TorConnectState.Bootstrapping, TorConnectState.Error]], - [TorConnectState.AutoConfiguring, + [TorConnectState.AutoBootstrapping, [TorConnectState.Configuring, - TorConnectState.Bootstrapping, + TorConnectState.Bootstrapped, TorConnectState.Error]], [TorConnectState.Bootstrapping, [TorConnectState.Configuring, TorConnectState.Bootstrapped, TorConnectState.Error]], [TorConnectState.Error, - [TorConnectState.Configuring, - TorConnectState.FatalError]], + [TorConnectState.Configuring]], // terminal states - [TorConnectState.FatalError, []], [TorConnectState.Bootstrapped, []], [TorConnectState.Disabled, []], ])); @@ -149,9 +133,70 @@ const TorConnectTopics = Object.freeze({ BootstrapProgress: "torconnect:bootstrap-progress", BootstrapComplete: "torconnect:bootstrap-complete", BootstrapError: "torconnect:bootstrap-error", - FatalError: "torconnect:fatal-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) +class StateCallback { + + constructor(state, callback) { + this._state = state; + this._callback = callback; + this._init(); + } + + _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; + } + }; + } + + async begin(...args) { + console.log(`TorConnect: Entering ${this._state} state`); + this._init(); + try { + // this Promise will block until this StateCallback has completed its work + await Promise.resolve(this._callback.call(this._context, ...args)); + console.log(`TorConnect: 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); + } + } + + transition(nextState, ...args) { + this._nextState = nextState; + this._nextStateArgs = [...args]; + + // 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; + } +} + const TorConnect = (() => { let retval = {
@@ -161,59 +206,207 @@ const TorConnect = (() => { _errorMessage: null, _errorDetails: null, _logHasWarningOrError: false, + _transitionPromise: null,
- /* These functions are called after transitioning to a new state */ - _transitionCallbacks: Object.freeze(new Map([ + /* 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, null], - /* Configuring */ - [TorConnectState.Configuring, async (self, prevState) => { - // TODO move this to the transition function - if (prevState === TorConnectState.Bootstrapping) { - await TorProtocolService.torStopBootstrap(); - } - }], - /* AutoConfiguring */ - [TorConnectState.AutoConfiguring, async (self, prevState) => { + [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, async (self, prevState) => { - let error = await TorProtocolService.connect(); - if (error) { - self.onError(error.message, error.details); - } else { - self._errorMessage = self._errorDetails = null; - } - }], + [TorConnectState.Bootstrapping, new StateCallback(TorConnectState.Bootstrapping, async function() { + // wait until bootstrap completes or we get an error + await new Promise(async (resolve, reject) => { + const tbr = new TorBootstrapRequest(); + this.on_transition = async (nextState) => { + if (nextState === TorConnectState.Configuring) { + // stop bootstrap process if user cancelled + await tbr.cancel(); + } + resolve(); + }; + + tbr.onbootstrapstatus = (progress, status) => { + TorConnect._updateBootstrapStatus(progress, status); + }; + tbr.onbootstrapcomplete = () => { + TorConnect._changeState(TorConnectState.Bootstrapped); + }; + tbr.onbootstraperror = (message, details) => { + TorConnect._changeState(TorConnectState.Error, message, details); + }; + + tbr.bootstrap(); + }); + })], + /* AutoBootstrapping */ + [TorConnectState.AutoBootstrapping, new StateCallback(TorConnectState.AutoBootstrapping, async function(countryCode) { + await new Promise(async (resolve, reject) => { + this.on_transition = (nextState) => { + resolve(); + }; + + // lookup user's potential censorship circumvention settings from Moat service + try { + this.mrpc = new MoatRPC(); + await this.mrpc.init(); + + this.settings = await this.mrpc.circumvention_settings([...TorBuiltinBridgeTypes, "vanilla"], countryCode); + + if (this.transitioning) return; + + if (this.settings === null) { + // unable to determine country + TorConnect._changeState(TorConnectState.Error, "Unable to determine user country", "DETAILS_STRING"); + return; + } else if (this.settings.length === 0) { + // no settings available for country + TorConnect._changeState(TorConnectState.Error, "No settings available for your location", "DETAILS_STRING"); + return; + } + } catch (err) { + TorConnect._changeState(TorConnectState.Error, err?.message, err?.details); + return; + } finally { + // important to uninit MoatRPC object or else the pt process will live as long as tor-browser + this.mrpc?.uninit(); + } + + // apply each of our settings and try to bootstrap with each + try { + this.originalSettings = TorSettings.getSettings(); + + let index = 0; + for (let currentSetting of this.settings) { + // let us early out if user cancels + if (this.transitioning) return; + + console.log(`TorConnect: Attempting Bootstrap with configuration ${++index}/${this.settings.length}`); + + TorSettings.setSettings(currentSetting); + await TorSettings.applySettings(); + + // build out our bootstrap request + const tbr = new TorBootstrapRequest(); + tbr.onbootstrapstatus = (progress, status) => { + TorConnect._updateBootstrapStatus(progress, status); + }; + tbr.onbootstraperror = (message, details) => { + console.log(`TorConnect: Auto-Bootstrap error => ${message}; ${details}`); + }; + + // update transition callback for user cancel + this.on_transition = async (nextState) => { + if (nextState === TorConnectState.Configuring) { + await tbr.cancel(); + } + resolve(); + }; + + // begin bootstrap + if (await tbr.bootstrap()) { + // persist the current settings to preferences + TorSettings.saveToPrefs(); + TorConnect._changeState(TorConnectState.Bootstrapped); + return; + } + } + // bootstrapped failed for all potential settings, so reset daemon to use original + TorSettings.setSettings(this.originalSettings); + await TorSettings.applySettings(); + TorSettings.saveToPrefs(); + + // only explicitly change state here if something else has not transitioned us + if (!this.transitioning) { + TorConnect._changeState(TorConnectState.Error, "AutoBootstrapping failed", "DETAILS_STRING"); + } + return; + } catch (err) { + // restore original settings in case of error + try { + TorSettings.setSettings(this.originalSettings); + await TorSettings.applySettings(); + } catch(err) { + console.log(`TorConnect: Failed to restore original settings => ${err}`); + } + TorConnect._changeState(TorConnectState.Error, err?.message, err?.details); + return; + } + }); + })], /* Bootstrapped */ - [TorConnectState.Bootstrapped, async (self,prevState) => { - // notify observers of bootstrap completion - Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete); - }], + [TorConnectState.Bootstrapped, new StateCallback(TorConnectState.Bootstrapped, async function() { + await new Promise((resolve, reject) => { + // on_transition not defined because no way to leave Bootstrapped state + // notify observers of bootstrap completion + Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete); + }); + })], /* Error */ - [TorConnectState.Error, async (self, prevState, errorMessage, errorDetails, fatal) => { - self._errorMessage = errorMessage; - self._errorDetails = errorDetails; + [TorConnectState.Error, new StateCallback(TorConnectState.Error, async function(errorMessage, errorDetails) { + await new Promise((resolve, reject) => { + this.on_transition = async(nextState) => { + resolve(); + };
- Services.obs.notifyObservers({message: errorMessage, details: errorDetails}, TorConnectTopics.BootstrapError); - if (fatal) { - self.onFatalError(); - } else { - self.beginConfigure(); - } - }], - /* FatalError */ - [TorConnectState.FatalError, async (self, prevState) => { - Services.obs.notifyObservers(null, TorConnectTopics.FatalError); - }], - /* Disabled */ - [TorConnectState.Disabled, (self, prevState) => { + TorConnect._errorMessage = errorMessage; + TorConnect._errorDetails = 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 + }); + })], ])),
- _changeState: async function(newState, ...args) { + _callback: function(state) { + return this._stateCallbacks.get(state); + }, + + _changeState: function(newState, ...args) { const prevState = this._state;
// ensure this is a valid state transition @@ -221,28 +414,40 @@ const TorConnect = (() => { throw Error(`TorConnect: Attempted invalid state transition from ${prevState} to ${newState}`); }
- console.log(`TorConnect: transitioning state from ${prevState} to ${newState}`); + console.log(`TorConnect: 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 transition function and forward any args - await this._transitionCallbacks.get(newState)(this, prevState, ...args); + // call our state function and forward any args + this._callback(prevState).transition(newState, ...args); + }, + + _updateBootstrapStatus: function(progress, status) { + this._bootstrapProgress= progress; + this._bootstrapStatus = status;
- Services.obs.notifyObservers({state: newState}, TorConnectTopics.StateChange); + console.log(`TorConnect: Bootstrapping ${this._bootstrapProgress}% complete (${this._bootstrapStatus})`); + Services.obs.notifyObservers({ + progress: TorConnect._bootstrapProgress, + status: TorConnect._bootstrapStatus, + hasWarnings: TorConnect._logHasWarningOrError + }, TorConnectTopics.BootstrapProgress); },
// init should be called on app-startup in MainProcessingSingleton.jsm - init : function() { - console.log("TorConnect: Init"); + init: function() { + console.log("TorConnect: init()");
// delay remaining init until after profile-after-change Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange); + + this._callback(TorConnectState.Initial).begin(); },
observe: async function(subject, topic, data) { - console.log(`TorConnect: observed ${topic}`); + console.log(`TorConnect: Observed ${topic}`);
switch(topic) {
@@ -250,19 +455,17 @@ const TorConnect = (() => { case BrowserTopics.ProfileAfterChange: { if (TorLauncherUtil.useLegacyLauncher || !TorProtocolService.ownsTorDaemon) { // Disabled - this.legacyOrSystemTor(); + this._changeState(TorConnectState.Disabled); } else { let observeTopic = (topic) => { Services.obs.addObserver(this, topic); - console.log(`TorConnect: observing topic '${topic}'`); + console.log(`TorConnect: Observing topic '${topic}'`); };
// register the Tor topics we always care about - for (const topicKey in TorTopics) { - const topic = TorTopics[topicKey]; - observeTopic(topic); - } - observeTopic(TorSettingsTopics.Ready); + observeTopic(TorTopics.ProcessExited); + observeTopic(TorTopics.LogHasWarnOrErr); + observeTopic(TorSettingsTopics.Ready); } Services.obs.removeObserver(this, topic); break; @@ -271,45 +474,13 @@ const TorConnect = (() => { case TorSettingsTopics.Ready: { if (this.shouldQuickStart) { // Quickstart - this.beginBootstrap(); + this._changeState(TorConnectState.Bootstrapping); } else { // Configuring - this.beginConfigure(); + this._changeState(TorConnectState.Configuring); } break; } - /* Updates our bootstrap status */ - case TorTopics.BootstrapStatus: { - if (this._state != TorConnectState.Bootstrapping) { - console.log(`TorConnect: observed ${TorTopics.BootstrapStatus} topic while in state TorConnectState.${this._state}`); - break; - } - - const obj = subject?.wrappedJSObject; - if (obj) { - this._bootstrapProgress= obj.PROGRESS; - this._bootstrapStatus = TorLauncherUtil.getLocalizedBootstrapStatus(obj, "TAG"); - - console.log(`TorConnect: Bootstrapping ${this._bootstrapProgress}% complete (${this._bootstrapStatus})`); - Services.obs.notifyObservers({ - progress: this._bootstrapProgress, - status: this._bootstrapStatus, - hasWarnings: this._logHasWarningOrError - }, TorConnectTopics.BootstrapProgress); - - if (this._bootstrapProgress === 100) { - this.bootstrapComplete(); - } - } - break; - } - /* Handle bootstrap error*/ - case TorTopics.BootstrapError: { - const obj = subject?.wrappedJSObject; - await TorProtocolService.torStopBootstrap(); - this.onError(obj.message, obj.details); - break; - } case TorTopics.LogHasWarnOrErr: { this._logHasWarningOrError = true; break; @@ -365,55 +536,27 @@ const TorConnect = (() => { },
/* - These functions tell TorConnect to transition states + These functions allow external consumers to tell TorConnect to transition states */
- legacyOrSystemTor: function() { - console.log("TorConnect: legacyOrSystemTor()"); - this._changeState(TorConnectState.Disabled); - }, - beginBootstrap: function() { console.log("TorConnect: beginBootstrap()"); this._changeState(TorConnectState.Bootstrapping); },
- beginConfigure: function() { - console.log("TorConnect: beginConfigure()"); - this._changeState(TorConnectState.Configuring); - }, - - autoConfigure: function() { - console.log("TorConnect: autoConfigure()"); - // TODO: implement - throw Error("TorConnect: not implemented"); - }, - - cancelAutoConfigure: function() { - console.log("TorConnect: cancelAutoConfigure()"); - // TODO: implement - throw Error("TorConnect: not implemented"); - }, - cancelBootstrap: function() { console.log("TorConnect: cancelBootstrap()"); this._changeState(TorConnectState.Configuring); },
- bootstrapComplete: function() { - console.log("TorConnect: bootstrapComplete()"); - this._changeState(TorConnectState.Bootstrapped); - }, - - onError: function(message, details) { - console.log("TorConnect: onError()"); - this._changeState(TorConnectState.Error, message, details, false); + beginAutoBootstrap: function(countryCode) { + console.log("TorConnect: beginAutoBootstrap()"); + this._changeState(TorConnectState.AutoBootstrapping, countryCode); },
- onFatalError: function() { - console.log("TorConnect: onFatalError()"); - // TODO: implement - throw Error("TorConnect: not implemented"); + cancelAutoBootstrap: function() { + console.log("TorConnect: cancelAutoBootstrap()"); + this._changeState(TorConnectState.Configuring); },
/* @@ -490,7 +633,7 @@ const TorConnect = (() => { }; let redirectUris = uris.map(uriToRedirectUri);
- console.log(`TorConnect: will load after bootstrap => [${uris.map((uri) => {return uri.spec;}).join(", ")}]`); + console.log(`TorConnect: Will load after bootstrap => [${uris.map((uri) => {return uri.spec;}).join(", ")}]`); return redirectUris; }, }; diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm index b8678fbca9aa..ac6d643691f6 100644 --- a/browser/modules/TorProtocolService.jsm +++ b/browser/modules/TorProtocolService.jsm @@ -2,12 +2,18 @@
"use strict";
-var EXPORTED_SYMBOLS = ["TorProtocolService", "TorProcessStatus"]; +var EXPORTED_SYMBOLS = ["TorProtocolService", "TorProcessStatus", "TorTopics", "TorBootstrapRequest"];
const { Services } = ChromeUtils.import( "resource://gre/modules/Services.jsm" );
+const { setTimeout, clearTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm"); + +const { TorLauncherUtil } = ChromeUtils.import( + "resource://torlauncher/modules/tl-util.jsm" +); + // see tl-process.js const TorProcessStatus = Object.freeze({ Unknown: 0, @@ -16,6 +22,14 @@ const TorProcessStatus = Object.freeze({ Exited: 3, });
+/* tor-launcher observer topics */ +const TorTopics = Object.freeze({ + BootstrapStatus: "TorBootstrapStatus", + BootstrapError: "TorBootstrapError", + ProcessExited: "TorProcessExited", + LogHasWarnOrErr: "TorLogHasWarnOrErr", +}); + /* Browser observer topis */ const BrowserTopics = Object.freeze({ ProfileAfterChange: "profile-after-change", @@ -360,4 +374,111 @@ var TorProtocolService = { return TorProcessStatus.Unknown; }, }; -TorProtocolService.init(); \ No newline at end of file +TorProtocolService.init(); + +// modeled after XMLHttpRequest +// nicely encapsulates the observer register/unregister logic +class TorBootstrapRequest { + constructor() { + // number of ms to wait before we abandon the bootstrap attempt + // a value of 0 implies we never wait + this.timeout = 0; + // callbacks for bootstrap process status updates + this.onbootstrapstatus = (progress, status) => {}; + this.onbootstrapcomplete = () => {}; + this.onbootstraperror = (message, details) => {}; + + // internal resolve() method for bootstrap + this._bootstrapPromiseResolve = null; + this._bootstrapPromise = null; + this._timeoutID = null; + } + + async observe(subject, topic, data) { + const obj = subject?.wrappedJSObject; + switch(topic) { + case TorTopics.BootstrapStatus: { + const progress = obj.PROGRESS; + const status = TorLauncherUtil.getLocalizedBootstrapStatus(obj, "TAG"); + if (this.onbootstrapstatus) { + this.onbootstrapstatus(progress, status); + } + if (progress === 100) { + if (this.onbootstrapcomplete) { + this.onbootstrapcomplete(); + } + this._bootstrapPromiseResolve(true); + clearTimeout(this._timeoutID); + } + + break; + } + case TorTopics.BootstrapError: { + // first stop our bootstrap timeout before handling the error + clearTimeout(this._timeoutID); + + await TorProtocolService.torStopBootstrap(); + + const message = obj.message; + const details = obj.details; + if (this.onbootstraperror) { + this.onbootstraperror(message, details); + } + this._bootstrapPromiseResolve(false); + break; + } + } + } + + // resolves 'true' if bootstrap succeeds, false otherwise + async bootstrap() { + if (this._bootstrapPromise) return this._bootstrapPromise; + + this._bootstrapPromise = new Promise(async (resolve, reject) => { + this._bootstrapPromiseResolve = resolve; + + // register ourselves to listen for bootstrap events + Services.obs.addObserver(this, TorTopics.BootstrapStatus); + Services.obs.addObserver(this, TorTopics.BootstrapError); + + // optionally cancel bootstrap after a given timeout + if (this.timeout > 0) { + this._timeoutID = setTimeout(async () => { + await TorProtocolService.torStopBootstrap(); + if (this.onbootstraperror) { + this.onbootstraperror("Tor Bootstrap process timed out", `Bootstrap attempt abandoned after waiting ${this.timeout} ms`); + } + this._bootstrapPromiseResolve(false); + }, this.timeout); + } + + // wait for bootstrapping to begin and maybe handle error + let err = await TorProtocolService.connect(); + if (err) { + clearTimeout(this._timeoutID); + await TorProtocolService.torStopBootstrap(); + + const message = err.message; + const details = err.details; + if (this.onbootstraperror) { + this.onbootstraperror(message, details); + } + this._bootstrapPromiseResolve(false); + } + }).finally(() => { + // and remove ourselves once bootstrap is resolved + Services.obs.removeObserver(this, TorTopics.BootstrapStatus); + Services.obs.removeObserver(this, TorTopics.BootstrapError); + }); + + return this._bootstrapPromise; + } + + async cancel() { + clearTimeout(this._timeoutID); + + await TorProtocolService.torStopBootstrap(); + + this._bootstrapPromiseResolve(false); + } +};