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