[tor-commits] [tor-browser/tor-browser-91.5.0esr-11.5-1] fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser

gk at torproject.org gk at torproject.org
Mon Jan 17 15:48:21 UTC 2022


commit 7f8276faefc398180879e755a2cf1c87eb28b715
Author: Richard Pospesel <richard at 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);
+  }
+};



More information about the tor-commits mailing list