[tor-commits] [tor-browser] 51/85: fixup! Bug 40597: Implement TorSettings module

gitolite role git at cupani.torproject.org
Fri Jun 10 17:01:42 UTC 2022


This is an automated email from the git hooks/post-receive script.

pierov pushed a commit to branch tor-browser-91.9.0esr-11.5-2
in repository tor-browser.

commit 91ad24bac8bf814874739e655819e80b5d1ae531
Author: Pier Angelo Vendrame <pierov at torproject.org>
AuthorDate: Fri Apr 8 15:24:50 2022 +0200

    fixup! Bug 40597: Implement TorSettings module
    
    Changes introduced by !275
---
 browser/modules/Moat.jsm       |  37 +++++++-
 browser/modules/TorConnect.jsm | 210 +++++++++++++++++++++++++++++++++--------
 2 files changed, 206 insertions(+), 41 deletions(-)

diff --git a/browser/modules/Moat.jsm b/browser/modules/Moat.jsm
index 2995a9148f0a3..35931172f3ee4 100644
--- a/browser/modules/Moat.jsm
+++ b/browser/modules/Moat.jsm
@@ -393,7 +393,7 @@ class InternetTestResponseListener {
   }
 
   // callers wait on this for final response
-  status() {
+  get status() {
     return this._promise;
   }
 
@@ -406,8 +406,18 @@ class InternetTestResponseListener {
       statuses = {
         components: status,
         successful: Components.isSuccessCode(status),
-        http: request.responseStatus,
       };
+      try {
+        if (statuses.successful) {
+          statuses.http = request.responseStatus;
+          statuses.date = request.getResponseHeader("Date");
+        }
+      } catch (err) {
+        console.warn(
+          "Successful request, but could not get the HTTP status or date",
+          err
+        );
+      }
     } catch (err) {
       this._reject(err);
     }
@@ -549,7 +559,7 @@ class MoatRPC {
 
     const listener = new InternetTestResponseListener();
     await ch.asyncOpen(listener, ch);
-    return listener.status();
+    return listener.status;
   }
 
   //
@@ -777,4 +787,25 @@ class MoatRPC {
 
     return map;
   }
+
+  // Request a copy of the defaul/fallback bridge settings, takes the following parameters:
+  // - transports: optional, an array of transports available to the client; if empty (or not
+  //   given) returns settings using all working transports known to the server
+  //
+  // returns an array of settings objects in roughly the same format as the _settings
+  // object on the TorSettings module
+  async circumvention_defaults(transports) {
+    const args = {
+      transports: transports ? transports : [],
+    };
+    const response = await this._makeRequest("circumvention/defaults", args);
+    if ("errors" in response) {
+      const code = response.errors[0].code;
+      const detail = response.errors[0].detail;
+      throw new Error(`MoatRPC: ${detail} (${code})`);
+    } else if ("settings" in response) {
+      return this._fixupSettingsList(response.settings);
+    }
+    return [];
+  }
 }
diff --git a/browser/modules/TorConnect.jsm b/browser/modules/TorConnect.jsm
index 13c1f54d2ee97..711134326a14d 100644
--- a/browser/modules/TorConnect.jsm
+++ b/browser/modules/TorConnect.jsm
@@ -1,12 +1,12 @@
 "use strict";
 
-var EXPORTED_SYMBOLS = ["TorConnect", "TorConnectTopics", "TorConnectState", "TorCensorshipLevel"];
+var EXPORTED_SYMBOLS = ["InternetStatus", "TorConnect", "TorConnectTopics", "TorConnectState"];
 
 const { Services } = ChromeUtils.import(
     "resource://gre/modules/Services.jsm"
 );
 
-const { setTimeout } = ChromeUtils.import(
+const { setTimeout, clearTimeout } = ChromeUtils.import(
     "resource://gre/modules/Timer.jsm"
 );
 
@@ -61,17 +61,6 @@ const TorConnectState = Object.freeze({
     Disabled: "Disabled",
 });
 
-const TorCensorshipLevel = Object.freeze({
-    /* No censorship detected */
-    None: 0,
-    /* Moderate censorship detected, autobootstrap may evade it */
-    Moderate: 1,
-    /* Severe censorship detected, but connection may still succeed */
-    Severe: 2,
-    /* Extreme censorship detected, connection will always end in an error */
-    Extreme: 3,
-});
-
 /*
                              TorConnect State Transitions
 
@@ -225,13 +214,98 @@ const debug_sleep = async (ms) => {
     });
 }
 
+const InternetStatus = Object.freeze({
+    Unknown: -1,
+    Offline: 0,
+    Online: 1,
+});
+
+class InternetTest {
+    constructor() {
+        this._status = InternetStatus.Unknown;
+        this._error = null;
+        this._pending = false;
+        this._timeout = setTimeout(() => {
+            this._timeout = null;
+            this.test();
+        }, this.timeoutRand());
+        this.onResult = (online, date) => {}
+        this.onError = (err) => {};
+    }
+
+    test() {
+        if (this._pending) {
+            return;
+        }
+        this.cancel();
+        this._pending = true;
+
+        this._testAsync()
+            .then((status) => {
+                this._pending = false;
+                this._status = status.successful ? InternetStatus.Online : InternetStatus.Offline;
+                this.onResult(this.status, status.date);
+            })
+            .catch(error => {
+                this._error = error;
+                this._pending = false;
+                this.onError(error);
+            });
+    }
+
+    cancel() {
+        if (this._timeout !== null) {
+            clearTimeout(this._timeout);
+            this._timeout = null;
+        }
+    }
+
+    async _testAsync() {
+        // Callbacks for the Internet test are desirable, because we will be
+        // waiting both for the bootstrap, and for the Internet test.
+        // However, managing Moat with async/await is much easier as it avoids a
+        // callback hell, and it makes extra explicit that we are uniniting it.
+        const mrpc = new MoatRPC();
+        let status = null;
+        let error = null;
+        try {
+            await mrpc.init();
+            status = await mrpc.testInternetConnection();
+        } catch (err) {
+            console.error("Error while checking the Internet connection", err);
+            error = err;
+        } finally {
+            mrpc.uninit();
+        }
+        if (error !== null) {
+            throw error;
+        }
+        return status;
+    }
+
+    get status() {
+        return this._status;
+    }
+
+    get error() {
+        return this._error;
+    }
+
+    // We randomize the Internet test timeout to make fingerprinting it harder, at least a little bit...
+    timeoutRand() {
+        const offset = 30000;
+        const randRange = 5000;
+        return offset + randRange * (Math.random() * 2 - 1);
+    }
+}
+
 const TorConnect = (() => {
     let retval = {
 
         _state: TorConnectState.Initial,
         _bootstrapProgress: 0,
         _bootstrapStatus: null,
-        _detectedCensorshiplevel: TorCensorshipLevel.None,
+        _internetStatus: InternetStatus.Unknown,
         // list of country codes Moat has settings for
         _countryCodes: [],
         _countryNames: Object.freeze((() => {
@@ -246,8 +320,13 @@ const TorConnect = (() => {
         _errorMessage: null,
         _errorDetails: null,
         _logHasWarningOrError: false,
+        _hasBootstrapEverFailed: false,
         _transitionPromise: null,
 
+        // This is used as a helper to make the state of about:torconnect persistent
+        // during a session, but TorConnect does not use this data at all.
+        _uiState: {},
+
         /* 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 */
@@ -294,26 +373,49 @@ const TorConnect = (() => {
             [TorConnectState.Bootstrapping, new StateCallback(TorConnectState.Bootstrapping, async function() {
                 // wait until bootstrap completes or we get an error
                 await new Promise(async (resolve, reject) => {
-
-                    // we reset the bootstrap failure count so users can manually try settings without the
-                    // censorship circumvention state machine getting in the way
-                    TorConnect._detectedCensorshipLevel = TorCensorshipLevel.None;
-
                     // debug hook to simulate censorship preventing bootstrapping
                     if (Services.prefs.getIntPref(TorConnectPrefs.censorship_level, 0) > 0) {
                         this.on_transition = (nextState) => {
                             resolve();
                         };
                         await debug_sleep(1500);
+                        TorConnect._hasBootstrapEverFailed = true;
                         TorConnect._changeState(TorConnectState.Error, "Bootstrap failed (for debugging purposes)", "Error: Censorship simulation", true);
                         TorProtocolService._torBootstrapDebugSetError();
                         return;
                     }
 
                     const tbr = new TorBootstrapRequest();
+                    const internetTest = new InternetTest();
+
+                    let bootstrapError = "";
+                    let bootstrapErrorDetails = "";
+                    const maybeTransitionToError = () => {
+                        if (internetTest.status === InternetStatus.Unknown && internetTest.error === null) {
+                            // We have been called by a failed bootstrap, but the internet test has not run yet - force
+                            // it to run immediately!
+                            internetTest.test();
+                            // Return from this call, because the Internet test's callback will call us again
+                            return;
+                        }
+                        // Do not transition to the offline error until we are sure that also the bootstrap failed, in
+                        // case Moat is down but the bootstrap can proceed anyway.
+                        if (bootstrapError === "") {
+                            return;
+                        }
+                        if (internetTest.status === InternetStatus.Offline) {
+                            TorConnect._changeState(TorConnectState.Error, TorStrings.torConnect.offline, "", true);
+                        } else {
+                            // Give priority to the bootstrap error, in case the Internet test fails
+                            TorConnect._hasBootstrapEverFailed = true;
+                            TorConnect._changeState(TorConnectState.Error, bootstrapError, bootstrapErrorDetails, true);
+                        }
+                    }
+
                     this.on_transition = async (nextState) => {
                         if (nextState === TorConnectState.Configuring) {
                             // stop bootstrap process if user cancelled
+                            internetTest.cancel();
                             await tbr.cancel();
                         }
                         resolve();
@@ -323,11 +425,24 @@ const TorConnect = (() => {
                         TorConnect._updateBootstrapStatus(progress, status);
                     };
                     tbr.onbootstrapcomplete = () => {
+                        internetTest.cancel();
                         TorConnect._changeState(TorConnectState.Bootstrapped);
                     };
                     tbr.onbootstraperror = (message, details) => {
-                        TorConnect._changeState(TorConnectState.Error, message, details, true);
+                        // We have to wait for the Internet test to finish before sending the bootstrap error
+                        bootstrapError = message;
+                        bootstrapErrorDetails = details;
+                        maybeTransitionToError();
+                    };
+
+                    internetTest.onResult = (status, date) => {
+                        // TODO: Use the date to save the clock skew?
+                        TorConnect._internetStatus = status;
+                        maybeTransitionToError();
                     };
+                    internetTest.onError = () => {
+                        maybeTransitionToError();
+                    }
 
                     tbr.bootstrap();
                 });
@@ -349,7 +464,7 @@ const TorConnect = (() => {
                             // always fail even after manually selecting location specific settings
                             if (censorshipLevel == 3) {
                                 await debug_sleep(2500);
-                                TorConnect._changeState(TorConnectState.Error, "Error: Extreme Censorship simulation", "", true);
+                                TorConnect._changeState(TorConnectState.Error, "Error: censorship simulation", "", true);
                                 return;
                             // only fail after auto selecting, manually selecting succeeds
                             } else if (censorshipLevel == 2 && !countryCode) {
@@ -378,12 +493,24 @@ const TorConnect = (() => {
 
                         if (this.transitioning) return;
 
-                        if (this.settings === null) {
-                            // unable to determine country
-                            throw_error(TorStrings.torConnect.autoBootstrappingFailed, TorStrings.torConnect.cannotDetermineCountry);
-                        } else if (this.settings.length === 0) {
-                            // no settings available for country
-                            throw_error(TorStrings.torConnect.autoBootstrappingFailed, TorStrings.torConnect.noSettingsForCountry);
+                        const noCountry = this.settings !== null;
+                        const noLocalizedSettings = this.settings && this.settings.length === 0;
+                        if (noCountry || noLocalizedSettings) {
+                            try {
+                                this.settings = await this.mrpc.circumvention_defaults([...TorBuiltinBridgeTypes, "vanilla"]);
+                            } catch (err) {
+                                console.error("We could not get localized settings, but defaults settings failed as well", err);
+                            }
+                        }
+                        if (this.settings === null || this.settings.length === 0) {
+                            // The fallback has failed as well, so throw the original error
+                            if (noCountry) {
+                                // unable to determine country
+                                throw_error(TorStrings.torConnect.autoBootstrappingFailed, TorStrings.torConnect.cannotDetermineCountry);
+                            } else {
+                                // no settings available for country
+                                throw_error(TorStrings.torConnect.autoBootstrappingFailed, TorStrings.torConnect.noSettingsForCountry);
+                            }
                         }
 
                         // apply each of our settings and try to bootstrap with each
@@ -477,11 +604,7 @@ const TorConnect = (() => {
                     TorConnect._errorMessage = errorMessage;
                     TorConnect._errorDetails = errorDetails;
 
-                    if (bootstrappingFailure && TorConnect._detectedCensorshipLevel < TorCensorshipLevel.Extreme) {
-                        TorConnect._detectedCensorshipLevel += 1;
-                    }
-
-                    Services.obs.notifyObservers({message: errorMessage, details: errorDetails, censorshipLevel: TorConnect.detectedCensorshipLevel}, TorConnectTopics.BootstrapError);
+                    Services.obs.notifyObservers({message: errorMessage, details: errorDetails}, TorConnectTopics.BootstrapError);
 
                     TorConnect._changeState(TorConnectState.Configuring);
                 });
@@ -615,8 +738,16 @@ const TorConnect = (() => {
             return this._bootstrapStatus;
         },
 
-        get detectedCensorshipLevel() {
-            return this._detectedCensorshipLevel;
+        get internetStatus() {
+            return this._internetStatus;
+        },
+
+        get countryCodes() {
+            return this._countryCodes;
+        },
+
+        get countryNames() {
+            return this._countryNames;
         },
 
         get errorMessage() {
@@ -631,12 +762,15 @@ const TorConnect = (() => {
             return this._logHasWarningOrError;
         },
 
-        get countryCodes() {
-            return this._countryCodes;
+        get hasBootstrapEverFailed() {
+            return this._hasBootstrapEverFailed;
         },
 
-        get countryNames() {
-            return this._countryNames;
+        get uiState() {
+            return this._uiState;
+        },
+        set uiState(newState) {
+            this._uiState = newState;
         },
 
         /*

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the tor-commits mailing list