Pier Angelo Vendrame pushed to branch tor-browser-115.2.1esr-13.0-1 at The Tor Project / Applications / Tor Browser

Commits:

23 changed files:

Changes:

  • browser/components/BrowserGlue.sys.mjs
    ... ... @@ -67,6 +67,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
    67 67
         "resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
    
    68 68
       TRRRacer: "resource:///modules/TRRPerformance.sys.mjs",
    
    69 69
       TelemetryUtils: "resource://gre/modules/TelemetryUtils.sys.mjs",
    
    70
    +  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
    
    70 71
       UIState: "resource://services-sync/UIState.sys.mjs",
    
    71 72
       UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
    
    72 73
       WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
    
    ... ... @@ -772,10 +773,10 @@ let JSWINDOWACTORS = {
    772 773
     
    
    773 774
       TorConnect: {
    
    774 775
         parent: {
    
    775
    -      moduleURI: "resource:///modules/TorConnectParent.jsm",
    
    776
    +      esModuleURI: "resource:///actors/TorConnectParent.sys.mjs",
    
    776 777
         },
    
    777 778
         child: {
    
    778
    -      moduleURI: "resource:///modules/TorConnectChild.jsm",
    
    779
    +      esModuleURI: "resource:///actors/TorConnectChild.sys.mjs",
    
    779 780
           events: {
    
    780 781
             DOMWindowCreated: {},
    
    781 782
           },
    
    ... ... @@ -1750,6 +1751,8 @@ BrowserGlue.prototype = {
    1750 1751
     
    
    1751 1752
         lazy.DragDropFilter.init();
    
    1752 1753
     
    
    1754
    +    lazy.TorProviderBuilder.firstWindowLoaded();
    
    1755
    +
    
    1753 1756
         this._firstWindowTelemetry(aWindow);
    
    1754 1757
         this._firstWindowLoaded();
    
    1755 1758
     
    

  • browser/components/onionservices/content/authPrompt.js
    ... ... @@ -179,12 +179,12 @@ const OnionAuthPrompt = (function () {
    179 179
         },
    
    180 180
     
    
    181 181
         async _onDone() {
    
    182
    -      let keyElem = this._getKeyElement();
    
    182
    +      const keyElem = this._getKeyElement();
    
    183 183
           if (!keyElem) {
    
    184 184
             return;
    
    185 185
           }
    
    186 186
     
    
    187
    -      let base64key = this._keyToBase64(keyElem.value);
    
    187
    +      const base64key = this._keyToBase64(keyElem.value);
    
    188 188
           if (!base64key) {
    
    189 189
             this._showWarning(TorStrings.onionServices.authPrompt.invalidKey);
    
    190 190
             return;
    
    ... ... @@ -192,8 +192,7 @@ const OnionAuthPrompt = (function () {
    192 192
     
    
    193 193
           this._prompt.remove();
    
    194 194
     
    
    195
    -      // Use Torbutton's controller module to add the private key to Tor.
    
    196
    -      let controllerFailureMsg =
    
    195
    +      const controllerFailureMsg =
    
    197 196
             TorStrings.onionServices.authPrompt.failedToSetKey;
    
    198 197
           try {
    
    199 198
             // ^(subdomain.)*onionserviceid.onion$ (case-insensitive)
    
    ... ... @@ -204,29 +203,19 @@ const OnionAuthPrompt = (function () {
    204 203
               .match(onionServiceIdRegExp)
    
    205 204
               .groups.onionServiceId.toLowerCase();
    
    206 205
     
    
    207
    -        let checkboxElem = this._getCheckboxElement();
    
    208
    -        let isPermanent = checkboxElem && checkboxElem.checked;
    
    209
    -        TorProviderBuilder.build()
    
    210
    -          .onionAuthAdd(onionServiceId, base64key, isPermanent)
    
    211
    -          .then(aResponse => {
    
    212
    -            // Success! Reload the page.
    
    213
    -            this._browser.sendMessageToActor(
    
    214
    -              "Browser:Reload",
    
    215
    -              {},
    
    216
    -              "BrowserTab"
    
    217
    -            );
    
    218
    -          })
    
    219
    -          .catch(aError => {
    
    220
    -            if (aError.torMessage) {
    
    221
    -              this.show(aError.torMessage);
    
    222
    -            } else {
    
    223
    -              console.error(controllerFailureMsg, aError);
    
    224
    -              this.show(controllerFailureMsg);
    
    225
    -            }
    
    226
    -          });
    
    206
    +        const checkboxElem = this._getCheckboxElement();
    
    207
    +        const isPermanent = checkboxElem && checkboxElem.checked;
    
    208
    +        const provider = await TorProviderBuilder.build();
    
    209
    +        await provider.onionAuthAdd(onionServiceId, base64key, isPermanent);
    
    210
    +        // Success! Reload the page.
    
    211
    +        this._browser.sendMessageToActor("Browser:Reload", {}, "BrowserTab");
    
    227 212
           } catch (e) {
    
    228
    -        console.error(controllerFailureMsg, e);
    
    229
    -        this.show(controllerFailureMsg);
    
    213
    +        if (e.torMessage) {
    
    214
    +          this.show(e.torMessage);
    
    215
    +        } else {
    
    216
    +          console.error(controllerFailureMsg, e);
    
    217
    +          this.show(controllerFailureMsg);
    
    218
    +        }
    
    230 219
           }
    
    231 220
         },
    
    232 221
     
    

  • browser/components/onionservices/content/savedKeysDialog.js
    ... ... @@ -26,42 +26,46 @@ var gOnionServicesSavedKeysDialog = {
    26 26
       },
    
    27 27
     
    
    28 28
       _tree: undefined,
    
    29
    -  _isBusy: false, // true when loading data, deleting a key, etc.
    
    29
    +  _busyCount: 0,
    
    30
    +  get _isBusy() {
    
    31
    +    // true when loading data, deleting a key, etc.
    
    32
    +    return this._busyCount > 0;
    
    33
    +  },
    
    30 34
     
    
    31 35
       // Public functions (called from outside this file).
    
    32 36
       async deleteSelectedKeys() {
    
    33
    -    this._setBusyState(true);
    
    34
    -
    
    35
    -    const indexesToDelete = [];
    
    36
    -    const count = this._tree.view.selection.getRangeCount();
    
    37
    -    for (let i = 0; i < count; ++i) {
    
    38
    -      const minObj = {};
    
    39
    -      const maxObj = {};
    
    40
    -      this._tree.view.selection.getRangeAt(i, minObj, maxObj);
    
    41
    -      for (let idx = minObj.value; idx <= maxObj.value; ++idx) {
    
    42
    -        indexesToDelete.push(idx);
    
    37
    +    this._withBusy(async () => {
    
    38
    +      const indexesToDelete = [];
    
    39
    +      const count = this._tree.view.selection.getRangeCount();
    
    40
    +      for (let i = 0; i < count; ++i) {
    
    41
    +        const minObj = {};
    
    42
    +        const maxObj = {};
    
    43
    +        this._tree.view.selection.getRangeAt(i, minObj, maxObj);
    
    44
    +        for (let idx = minObj.value; idx <= maxObj.value; ++idx) {
    
    45
    +          indexesToDelete.push(idx);
    
    46
    +        }
    
    43 47
           }
    
    44
    -    }
    
    45 48
     
    
    46
    -    if (indexesToDelete.length) {
    
    47
    -      const controllerFailureMsg =
    
    48
    -        TorStrings.onionServices.authPreferences.failedToRemoveKey;
    
    49
    -      try {
    
    50
    -        // Remove in reverse index order to avoid issues caused by index changes.
    
    51
    -        for (let i = indexesToDelete.length - 1; i >= 0; --i) {
    
    52
    -          await this._deleteOneKey(indexesToDelete[i]);
    
    53
    -        }
    
    54
    -      } catch (e) {
    
    55
    -        console.error("Removing a saved key failed", e);
    
    56
    -        if (e.torMessage) {
    
    57
    -          this._showError(e.torMessage);
    
    58
    -        } else {
    
    59
    -          this._showError(controllerFailureMsg);
    
    49
    +      if (indexesToDelete.length) {
    
    50
    +        const controllerFailureMsg =
    
    51
    +          TorStrings.onionServices.authPreferences.failedToRemoveKey;
    
    52
    +        const provider = await TorProviderBuilder.build();
    
    53
    +        try {
    
    54
    +          // Remove in reverse index order to avoid issues caused by index
    
    55
    +          // changes.
    
    56
    +          for (let i = indexesToDelete.length - 1; i >= 0; --i) {
    
    57
    +            await this._deleteOneKey(provider, indexesToDelete[i]);
    
    58
    +          }
    
    59
    +        } catch (e) {
    
    60
    +          console.error("Removing a saved key failed", e);
    
    61
    +          if (e.torMessage) {
    
    62
    +            this._showError(e.torMessage);
    
    63
    +          } else {
    
    64
    +            this._showError(controllerFailureMsg);
    
    65
    +          }
    
    60 66
             }
    
    61 67
           }
    
    62
    -    }
    
    63
    -
    
    64
    -    this._setBusyState(false);
    
    68
    +    });
    
    65 69
       },
    
    66 70
     
    
    67 71
       async deleteAllKeys() {
    
    ... ... @@ -84,16 +88,12 @@ var gOnionServicesSavedKeysDialog = {
    84 88
       },
    
    85 89
     
    
    86 90
       async _init() {
    
    87
    -    await this._populateXUL();
    
    88
    -
    
    91
    +    this._populateXUL();
    
    89 92
         window.addEventListener("keypress", this._onWindowKeyPress.bind(this));
    
    90
    -
    
    91
    -    // We don't use await here because we want _loadSavedKeys() to run
    
    92
    -    // in the background and not block loading of this dialog.
    
    93 93
         this._loadSavedKeys();
    
    94 94
       },
    
    95 95
     
    
    96
    -  async _populateXUL() {
    
    96
    +  _populateXUL() {
    
    97 97
         const dialog = document.querySelector(this.selector.dialog);
    
    98 98
         const authPrefStrings = TorStrings.onionServices.authPreferences;
    
    99 99
         dialog.setAttribute("title", authPrefStrings.dialogTitle);
    
    ... ... @@ -119,56 +119,68 @@ var gOnionServicesSavedKeysDialog = {
    119 119
       async _loadSavedKeys() {
    
    120 120
         const controllerFailureMsg =
    
    121 121
           TorStrings.onionServices.authPreferences.failedToGetKeys;
    
    122
    -    this._setBusyState(true);
    
    123
    -
    
    124
    -    try {
    
    125
    -      this._tree.view = this;
    
    126
    -
    
    127
    -      const keyInfoList = await TorProviderBuilder.build().onionAuthViewKeys();
    
    128
    -      if (keyInfoList) {
    
    129
    -        // Filter out temporary keys.
    
    130
    -        this._keyInfoList = keyInfoList.filter(aKeyInfo =>
    
    131
    -          aKeyInfo.flags?.includes("Permanent")
    
    132
    -        );
    
    133
    -        // Sort by the .onion address.
    
    134
    -        this._keyInfoList.sort((aObj1, aObj2) => {
    
    135
    -          const hsAddr1 = aObj1.address.toLowerCase();
    
    136
    -          const hsAddr2 = aObj2.address.toLowerCase();
    
    137
    -          if (hsAddr1 < hsAddr2) {
    
    138
    -            return -1;
    
    139
    -          }
    
    140
    -          return hsAddr1 > hsAddr2 ? 1 : 0;
    
    141
    -        });
    
    142
    -      }
    
    122
    +    this._withBusy(async () => {
    
    123
    +      try {
    
    124
    +        this._tree.view = this;
    
    125
    +
    
    126
    +        const provider = await TorProviderBuilder.build();
    
    127
    +        const keyInfoList = await provider.onionAuthViewKeys();
    
    128
    +        if (keyInfoList) {
    
    129
    +          // Filter out temporary keys.
    
    130
    +          this._keyInfoList = keyInfoList.filter(aKeyInfo =>
    
    131
    +            aKeyInfo.flags?.includes("Permanent")
    
    132
    +          );
    
    133
    +          // Sort by the .onion address.
    
    134
    +          this._keyInfoList.sort((aObj1, aObj2) => {
    
    135
    +            const hsAddr1 = aObj1.address.toLowerCase();
    
    136
    +            const hsAddr2 = aObj2.address.toLowerCase();
    
    137
    +            if (hsAddr1 < hsAddr2) {
    
    138
    +              return -1;
    
    139
    +            }
    
    140
    +            return hsAddr1 > hsAddr2 ? 1 : 0;
    
    141
    +          });
    
    142
    +        }
    
    143 143
     
    
    144
    -      // Render the tree content.
    
    145
    -      this._tree.rowCountChanged(0, this.rowCount);
    
    146
    -    } catch (e) {
    
    147
    -      if (e.torMessage) {
    
    148
    -        this._showError(e.torMessage);
    
    149
    -      } else {
    
    150
    -        this._showError(controllerFailureMsg);
    
    144
    +        // Render the tree content.
    
    145
    +        this._tree.rowCountChanged(0, this.rowCount);
    
    146
    +      } catch (e) {
    
    147
    +        if (e.torMessage) {
    
    148
    +          this._showError(e.torMessage);
    
    149
    +        } else {
    
    150
    +          this._showError(controllerFailureMsg);
    
    151
    +        }
    
    151 152
           }
    
    152
    -    }
    
    153
    -
    
    154
    -    this._setBusyState(false);
    
    153
    +    });
    
    155 154
       },
    
    156 155
     
    
    157 156
       // This method may throw; callers should catch errors.
    
    158
    -  async _deleteOneKey(aIndex) {
    
    157
    +  async _deleteOneKey(provider, aIndex) {
    
    159 158
         const keyInfoObj = this._keyInfoList[aIndex];
    
    160
    -    await TorProviderBuilder.build().onionAuthRemove(keyInfoObj.address);
    
    159
    +    await provider.onionAuthRemove(keyInfoObj.address);
    
    161 160
         this._tree.view.selection.clearRange(aIndex, aIndex);
    
    162 161
         this._keyInfoList.splice(aIndex, 1);
    
    163 162
         this._tree.rowCountChanged(aIndex + 1, -1);
    
    164 163
       },
    
    165 164
     
    
    166
    -  _setBusyState(aIsBusy) {
    
    167
    -    this._isBusy = aIsBusy;
    
    168
    -    this.updateButtonsState();
    
    165
    +  async _withBusy(func) {
    
    166
    +    this._busyCount++;
    
    167
    +    if (this._busyCount === 1) {
    
    168
    +      this.updateButtonsState();
    
    169
    +    }
    
    170
    +    try {
    
    171
    +      await func();
    
    172
    +    } finally {
    
    173
    +      this._busyCount--;
    
    174
    +      if (this._busyCount === 0) {
    
    175
    +        this.updateButtonsState();
    
    176
    +      }
    
    177
    +    }
    
    169 178
       },
    
    170 179
     
    
    171 180
       _onWindowKeyPress(event) {
    
    181
    +    if (this._isBusy) {
    
    182
    +      return;
    
    183
    +    }
    
    172 184
         if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
    
    173 185
           window.close();
    
    174 186
         } else if (event.keyCode === KeyEvent.DOM_VK_DELETE) {
    

  • browser/components/torconnect/TorConnectChild.jsm deleted
    1
    -// Copyright (c) 2021, The Tor Project, Inc.
    
    2
    -
    
    3
    -var EXPORTED_SYMBOLS = ["TorConnectChild"];
    
    4
    -
    
    5
    -const { RemotePageChild } = ChromeUtils.import(
    
    6
    -  "resource://gre/actors/RemotePageChild.jsm"
    
    7
    -);
    
    8
    -
    
    9
    -class TorConnectChild extends RemotePageChild {}

  • browser/components/torconnect/TorConnectChild.sys.mjs
    1
    +// Copyright (c) 2021, The Tor Project, Inc.
    
    2
    +
    
    3
    +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs";
    
    4
    +
    
    5
    +export class TorConnectChild extends RemotePageChild {}

  • browser/components/torconnect/TorConnectParent.jsmbrowser/components/torconnect/TorConnectParent.sys.mjs
    1 1
     // Copyright (c) 2021, The Tor Project, Inc.
    
    2 2
     
    
    3
    -var EXPORTED_SYMBOLS = ["TorConnectParent"];
    
    4
    -
    
    5
    -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
    
    6 3
     const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
    
    7
    -const { InternetStatus, TorConnect, TorConnectTopics, TorConnectState } =
    
    8
    -  ChromeUtils.import("resource:///modules/TorConnect.jsm");
    
    9
    -const { TorSettings, TorSettingsTopics, TorSettingsData } = ChromeUtils.import(
    
    10
    -  "resource:///modules/TorSettings.jsm"
    
    11
    -);
    
    4
    +import {
    
    5
    +  InternetStatus,
    
    6
    +  TorConnect,
    
    7
    +  TorConnectTopics,
    
    8
    +  TorConnectState,
    
    9
    +} from "resource:///modules/TorConnect.sys.mjs";
    
    10
    +import {
    
    11
    +  TorSettings,
    
    12
    +  TorSettingsTopics,
    
    13
    +  TorSettingsData,
    
    14
    +} from "resource:///modules/TorSettings.sys.mjs";
    
    12 15
     
    
    13 16
     const BroadcastTopic = "about-torconnect:broadcast";
    
    14 17
     
    
    ... ... @@ -17,7 +20,7 @@ This object is basically a marshalling interface between the TorConnect module
    17 20
     and a particular about:torconnect page
    
    18 21
     */
    
    19 22
     
    
    20
    -class TorConnectParent extends JSWindowActorParent {
    
    23
    +export class TorConnectParent extends JSWindowActorParent {
    
    21 24
       constructor(...args) {
    
    22 25
         super(...args);
    
    23 26
     
    
    ... ... @@ -35,10 +38,20 @@ class TorConnectParent extends JSWindowActorParent {
    35 38
           DetectedLocation: TorConnect.detectedLocation,
    
    36 39
           ShowViewLog: TorConnect.logHasWarningOrError,
    
    37 40
           HasEverFailed: TorConnect.hasEverFailed,
    
    38
    -      QuickStartEnabled: TorSettings.quickstart.enabled,
    
    39 41
           UIState: TorConnect.uiState,
    
    40 42
         };
    
    41 43
     
    
    44
    +    // Workaround for a race condition, but we should fix it asap.
    
    45
    +    // about:torconnect is loaded before TorSettings is actually initialized.
    
    46
    +    // The getter might throw and the page not loaded correctly as a result.
    
    47
    +    // Silence any warning for now, but we should really fix it.
    
    48
    +    // See also tor-browser#41921.
    
    49
    +    try {
    
    50
    +      this.state.QuickStartEnabled = TorSettings.quickstart.enabled;
    
    51
    +    } catch (e) {
    
    52
    +      this.state.QuickStartEnabled = false;
    
    53
    +    }
    
    54
    +
    
    42 55
         // JSWindowActiveParent derived objects cannot observe directly, so create a member
    
    43 56
         // object to do our observing for us
    
    44 57
         //
    
    ... ... @@ -84,6 +97,16 @@ class TorConnectParent extends JSWindowActorParent {
    84 97
                 self.state.ShowViewLog = true;
    
    85 98
                 break;
    
    86 99
               }
    
    100
    +          case TorSettingsTopics.Ready: {
    
    101
    +            if (
    
    102
    +              self.state.QuickStartEnabled !== TorSettings.quickstart.enabled
    
    103
    +            ) {
    
    104
    +              self.state.QuickStartEnabled = TorSettings.quickstart.enabled;
    
    105
    +            } else {
    
    106
    +              return;
    
    107
    +            }
    
    108
    +            break;
    
    109
    +          }
    
    87 110
               case TorSettingsTopics.SettingChanged: {
    
    88 111
                 if (aData === TorSettingsData.QuickStartEnabled) {
    
    89 112
                   self.state.QuickStartEnabled = obj.value;
    
    ... ... @@ -107,6 +130,7 @@ class TorConnectParent extends JSWindowActorParent {
    107 130
           const topic = TorConnectTopics[key];
    
    108 131
           Services.obs.addObserver(this.torConnectObserver, topic);
    
    109 132
         }
    
    133
    +    Services.obs.addObserver(this.torConnectObserver, TorSettingsTopics.Ready);
    
    110 134
         Services.obs.addObserver(
    
    111 135
           this.torConnectObserver,
    
    112 136
           TorSettingsTopics.SettingChanged
    
    ... ... @@ -130,6 +154,10 @@ class TorConnectParent extends JSWindowActorParent {
    130 154
           const topic = TorConnectTopics[key];
    
    131 155
           Services.obs.removeObserver(this.torConnectObserver, topic);
    
    132 156
         }
    
    157
    +    Services.obs.removeObserver(
    
    158
    +      this.torConnectObserver,
    
    159
    +      TorSettingsTopics.Ready
    
    160
    +    );
    
    133 161
         Services.obs.removeObserver(
    
    134 162
           this.torConnectObserver,
    
    135 163
           TorSettingsTopics.SettingChanged
    

  • browser/components/torconnect/moz.build
    1
    -JAR_MANIFESTS += ['jar.mn']
    
    1
    +JAR_MANIFESTS += ["jar.mn"]
    
    2 2
     
    
    3
    -EXTRA_JS_MODULES += [
    
    4
    -    'TorConnectChild.jsm',
    
    5
    -    'TorConnectParent.jsm',
    
    3
    +FINAL_TARGET_FILES.actors += [
    
    4
    +    "TorConnectChild.sys.mjs",
    
    5
    +    "TorConnectParent.sys.mjs",
    
    6 6
     ]

  • browser/components/torpreferences/content/connectionPane.js
    ... ... @@ -153,8 +153,16 @@ const gConnectionPane = (function () {
    153 153
         // populate xul with strings and cache the relevant elements
    
    154 154
         _populateXUL() {
    
    155 155
           // saves tor settings to disk when navigate away from about:preferences
    
    156
    -      window.addEventListener("blur", val => {
    
    157
    -        TorProviderBuilder.build().flushSettings();
    
    156
    +      window.addEventListener("blur", async () => {
    
    157
    +        try {
    
    158
    +          // Build a new provider each time because this might be called also
    
    159
    +          // when closing the browser (if about:preferences was open), maybe
    
    160
    +          // when the provider was already uninitialized.
    
    161
    +          const provider = await TorProviderBuilder.build();
    
    162
    +          provider.flushSettings();
    
    163
    +        } catch (e) {
    
    164
    +          console.warn("Could not save the tor settings.", e);
    
    165
    +        }
    
    158 166
           });
    
    159 167
     
    
    160 168
           document
    
    ... ... @@ -746,11 +754,17 @@ const gConnectionPane = (function () {
    746 754
             placeholder.replaceWith(...cards);
    
    747 755
             this._checkBridgeCardsHeight();
    
    748 756
           };
    
    749
    -      this._checkConnectedBridge = () => {
    
    757
    +      this._checkConnectedBridge = async () => {
    
    750 758
             // TODO: We could make sure TorSettings is in sync by monitoring also
    
    751 759
             // changes of settings. At that point, we could query it, instead of
    
    752 760
             // doing a query over the control port.
    
    753
    -        const bridge = TorProviderBuilder.build().currentBridge;
    
    761
    +        let bridge = null;
    
    762
    +        try {
    
    763
    +          const provider = await TorProviderBuilder.build();
    
    764
    +          bridge = provider.currentBridge;
    
    765
    +        } catch (e) {
    
    766
    +          console.warn("Could not get current bridge", e);
    
    767
    +       }
    
    754 768
             if (bridge?.fingerprint !== this._currentBridgeId) {
    
    755 769
               this._currentBridgeId = bridge?.fingerprint ?? null;
    
    756 770
               this._updateConnectedBridges();
    

  • browser/components/torpreferences/content/torLogDialog.jsm
    ... ... @@ -26,7 +26,7 @@ class TorLogDialog {
    26 26
         };
    
    27 27
       }
    
    28 28
     
    
    29
    -  _populateXUL(aDialog) {
    
    29
    +  async _populateXUL(aDialog) {
    
    30 30
         this._dialog = aDialog;
    
    31 31
         const dialogWin = this._dialog.parentElement;
    
    32 32
         dialogWin.setAttribute("title", TorStrings.settings.torLogDialogTitle);
    
    ... ... @@ -56,7 +56,12 @@ class TorLogDialog {
    56 56
           }, RESTORE_TIME);
    
    57 57
         });
    
    58 58
     
    
    59
    -    this._logTextarea.value = TorProviderBuilder.build().getLog();
    
    59
    +    // A waiting state should not be needed at this point.
    
    60
    +    // Also, we probably cannot even arrive here if the provider failed to
    
    61
    +    // initialize, otherwise we could use a try/catch, and write the exception
    
    62
    +    // text in the logs, instead.
    
    63
    +    const provider = await TorProviderBuilder.build();
    
    64
    +    this._logTextarea.value = provider.getLog();
    
    60 65
       }
    
    61 66
     
    
    62 67
       init(window, aDialog) {
    

  • browser/modules/Moat.sys.mjs
    ... ... @@ -46,9 +46,8 @@ class MeekTransport {
    46 46
         try {
    
    47 47
           // figure out which pluggable transport to use
    
    48 48
           const supportedTransports = ["meek", "meek_lite"];
    
    49
    -      const proxy = (
    
    50
    -        await lazy.TorProviderBuilder.build().getPluggableTransports()
    
    51
    -      ).find(
    
    49
    +      const provider = await lazy.TorProviderBuilder.build();
    
    50
    +      const proxy = (await provider.getPluggableTransports()).find(
    
    52 51
             pt =>
    
    53 52
               pt.type === "exec" &&
    
    54 53
               supportedTransports.some(t => pt.transports.includes(t))
    

  • browser/modules/TorConnect.sys.mjs
    ... ... @@ -9,7 +9,6 @@ const lazy = {};
    9 9
     ChromeUtils.defineESModuleGetters(lazy, {
    
    10 10
       MoatRPC: "resource:///modules/Moat.sys.mjs",
    
    11 11
       TorBootstrapRequest: "resource://gre/modules/TorBootstrapRequest.sys.mjs",
    
    12
    -  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
    
    13 12
     });
    
    14 13
     
    
    15 14
     // TODO: Should we move this to the about:torconnect actor?
    
    ... ... @@ -19,6 +18,7 @@ ChromeUtils.defineModuleGetter(
    19 18
       "resource:///modules/BrowserWindowTracker.jsm"
    
    20 19
     );
    
    21 20
     
    
    21
    +import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
    
    22 22
     import {
    
    23 23
       TorSettings,
    
    24 24
       TorSettingsTopics,
    
    ... ... @@ -91,15 +91,15 @@ export const TorConnectState = Object.freeze({
    91 91
       │ │       │                                                          │  │
    
    92 92
       └─┼─────▶ │                                                          │  │
    
    93 93
         │       └──────────────────────────────────────────────────────────┘  │
    
    94
    -    │         │                        ▲                        
    
    95
    -    │         │ beginAutoBootstrap()   │ cancelBootstrap()      
    
    96
    -    │         ▼                        │                        
    
    97
    -    │       ┌───────────────────────┐  │                        
    
    98
    -    └────── │   AutoBootstrapping   │ ─┘                        
    
    99
    -            └───────────────────────┘                           
    
    100
    - 
    
    101
    -                                  
    
    102
    - 
    
    94
    +    │         │                        ▲                       
    
    95
    +    │         │ beginAutoBootstrap()   │ cancelBootstrap()     
    
    96
    +    │         ▼                        │                       
    
    97
    +    │       ┌───────────────────────┐  │                       
    
    98
    +    └────── │   AutoBootstrapping   │ ─┘                       
    
    99
    +            └───────────────────────┘                          
    
    100
    +
    
    101
    +┌────────────────────────────────┘
    
    102
    +
    
    103 103
                 ┌───────────────────────┐                                     │
    
    104 104
                 │     Bootstrapped      │ ◀───────────────────────────────────┘
    
    105 105
                 └───────────────────────┘
    
    ... ... @@ -145,8 +145,8 @@ const TorConnectStateTransitions = Object.freeze(
    145 145
           ],
    
    146 146
         ],
    
    147 147
         [TorConnectState.Error, [TorConnectState.Configuring]],
    
    148
    +    [TorConnectState.Bootstrapped, [TorConnectState.Configuring]],
    
    148 149
         // terminal states
    
    149
    -    [TorConnectState.Bootstrapped, []],
    
    150 150
         [TorConnectState.Disabled, []],
    
    151 151
       ])
    
    152 152
     );
    
    ... ... @@ -703,8 +703,13 @@ export const TorConnect = (() => {
    703 703
     
    
    704 704
                       // bootstrapped failed for all potential settings, so reset daemon to use original
    
    705 705
                       TorSettings.setSettings(this.originalSettings);
    
    706
    -                  await TorSettings.applySettings();
    
    706
    +                  // The original settings should be good, so we save them to
    
    707
    +                  // preferences before trying to apply them, as it might fail
    
    708
    +                  // if the actual problem is with the connection to the control
    
    709
    +                  // port.
    
    710
    +                  // FIXME: We should handle this case in a better way.
    
    707 711
                       TorSettings.saveToPrefs();
    
    712
    +                  await TorSettings.applySettings();
    
    708 713
     
    
    709 714
                       // only explicitly change state here if something else has not transitioned us
    
    710 715
                       if (!this.transitioning) {
    
    ... ... @@ -718,6 +723,8 @@ export const TorConnect = (() => {
    718 723
                       // restore original settings in case of error
    
    719 724
                       try {
    
    720 725
                         TorSettings.setSettings(this.originalSettings);
    
    726
    +                    // As above
    
    727
    +                    TorSettings.saveToPrefs();
    
    721 728
                         await TorSettings.applySettings();
    
    722 729
                       } catch (errRestore) {
    
    723 730
                         console.log(
    
    ... ... @@ -733,12 +740,19 @@ export const TorConnect = (() => {
    733 740
                       TorConnect._countryCodes =
    
    734 741
                         await this.mrpc.circumvention_countries();
    
    735 742
                     }
    
    736
    -                TorConnect._changeState(
    
    737
    -                  TorConnectState.Error,
    
    738
    -                  err?.message,
    
    739
    -                  err?.details,
    
    740
    -                  true
    
    741
    -                );
    
    743
    +                if (!this.transitioning) {
    
    744
    +                  TorConnect._changeState(
    
    745
    +                    TorConnectState.Error,
    
    746
    +                    err?.message,
    
    747
    +                    err?.details,
    
    748
    +                    true
    
    749
    +                  );
    
    750
    +                } else {
    
    751
    +                  console.error(
    
    752
    +                    "TorConnect: Received AutoBootstrapping error after transitioning",
    
    753
    +                    err
    
    754
    +                  );
    
    755
    +                }
    
    742 756
                   } finally {
    
    743 757
                     // important to uninit MoatRPC object or else the pt process will live as long as tor-browser
    
    744 758
                     this.mrpc?.uninit();
    
    ... ... @@ -751,7 +765,11 @@ export const TorConnect = (() => {
    751 765
               TorConnectState.Bootstrapped,
    
    752 766
               new StateCallback(TorConnectState.Bootstrapped, async function () {
    
    753 767
                 await new Promise((resolve, reject) => {
    
    754
    -              // on_transition not defined because no way to leave Bootstrapped state
    
    768
    +              // We may need to leave the bootstrapped state if the tor daemon
    
    769
    +              // exits (if it is restarted, we will have to bootstrap again).
    
    770
    +              this.on_transition = nextState => {
    
    771
    +                resolve();
    
    772
    +              };
    
    755 773
                   // notify observers of bootstrap completion
    
    756 774
                   Services.obs.notifyObservers(
    
    757 775
                     null,
    
    ... ... @@ -895,6 +913,25 @@ export const TorConnect = (() => {
    895 913
               this._logHasWarningOrError = true;
    
    896 914
               break;
    
    897 915
             }
    
    916
    +        case TorTopics.ProcessExited: {
    
    917
    +          // Treat a failure as a possibly broken configuration.
    
    918
    +          // So, prevent quickstart at the next start.
    
    919
    +          Services.prefs.setBoolPref(TorLauncherPrefs.prompt_at_startup, true);
    
    920
    +          switch (this._state) {
    
    921
    +            case TorConnectState.Bootstrapping:
    
    922
    +            case TorConnectState.AutoBootstrapping:
    
    923
    +            case TorConnectState.Bootstrapped:
    
    924
    +              // If we are in the bootstrap or auto bootstrap, we could go
    
    925
    +              // through the error phase (and eventually we might do it, if some
    
    926
    +              // transition calls fail). However, this would start the
    
    927
    +              // connection assist, so we go directly to configuring.
    
    928
    +              // FIXME: Find a better way to handle this.
    
    929
    +              this._changeState(TorConnectState.Configuring);
    
    930
    +              break;
    
    931
    +            // Other states naturally resolve in configuration.
    
    932
    +          }
    
    933
    +          break;
    
    934
    +        }
    
    898 935
             default:
    
    899 936
               // ignore
    
    900 937
               break;
    
    ... ... @@ -911,7 +948,10 @@ export const TorConnect = (() => {
    911 948
          * @type {boolean}
    
    912 949
          */
    
    913 950
         get enabled() {
    
    914
    -      return lazy.TorProviderBuilder.build().ownsTorDaemon;
    
    951
    +      // FIXME: This is called before the TorProvider is ready.
    
    952
    +      // As a matter of fact, at the moment it is equivalent to the following
    
    953
    +      // line, but this might become a problem in the future.
    
    954
    +      return TorLauncherUtil.shouldStartAndOwnTor;
    
    915 955
         },
    
    916 956
     
    
    917 957
         get shouldShowTorConnect() {
    

  • browser/modules/TorSettings.sys.mjs
    ... ... @@ -5,6 +5,7 @@
    5 5
     const lazy = {};
    
    6 6
     
    
    7 7
     ChromeUtils.defineESModuleGetters(lazy, {
    
    8
    +  TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
    
    8 9
       TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
    
    9 10
       TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
    
    10 11
     });
    
    ... ... @@ -273,9 +274,10 @@ export const TorSettings = (() => {
    273 274
         },
    
    274 275
     
    
    275 276
         /* load or init our settings, and register observers */
    
    276
    -    init() {
    
    277
    -      const provider = lazy.TorProviderBuilder.build();
    
    278
    -      if (provider.ownsTorDaemon) {
    
    277
    +    async init() {
    
    278
    +      // TODO: We could use a shared promise, and wait for it to be fullfilled
    
    279
    +      // instead of Service.obs.
    
    280
    +      if (lazy.TorLauncherUtil.shouldStartAndOwnTor) {
    
    279 281
             // if the settings branch exists, load settings from prefs
    
    280 282
             if (Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)) {
    
    281 283
               this.loadFromPrefs();
    
    ... ... @@ -285,9 +287,12 @@ export const TorSettings = (() => {
    285 287
             }
    
    286 288
             Services.obs.addObserver(this, lazy.TorProviderTopics.ProcessIsReady);
    
    287 289
     
    
    288
    -        if (provider.isRunning) {
    
    289
    -          this.handleProcessReady();
    
    290
    -        }
    
    290
    +        try {
    
    291
    +          const provider = await lazy.TorProviderBuilder.build();
    
    292
    +          if (provider.isRunning) {
    
    293
    +            this.handleProcessReady();
    
    294
    +          }
    
    295
    +        } catch {}
    
    291 296
           }
    
    292 297
         },
    
    293 298
     
    
    ... ... @@ -559,7 +564,8 @@ export const TorSettings = (() => {
    559 564
           }
    
    560 565
     
    
    561 566
           /* Push to Tor */
    
    562
    -      await lazy.TorProviderBuilder.build().writeSettings(settingsMap);
    
    567
    +      const provider = await lazy.TorProviderBuilder.build();
    
    568
    +      await provider.writeSettings(settingsMap);
    
    563 569
     
    
    564 570
           return this;
    
    565 571
         },
    

  • toolkit/components/tor-launcher/TorBootstrapRequest.sys.mjs
    ... ... @@ -26,11 +26,6 @@ export class TorBootstrapRequest {
    26 26
       #bootstrapPromiseResolve = null;
    
    27 27
       #bootstrapPromise = null;
    
    28 28
       #timeoutID = null;
    
    29
    -  #provider = null;
    
    30
    -
    
    31
    -  constructor() {
    
    32
    -    this.#provider = TorProviderBuilder.build();
    
    33
    -  }
    
    34 29
     
    
    35 30
       observe(subject, topic, data) {
    
    36 31
         const obj = subject?.wrappedJSObject;
    
    ... ... @@ -85,10 +80,14 @@ export class TorBootstrapRequest {
    85 80
             }, this.timeout);
    
    86 81
           }
    
    87 82
     
    
    88
    -      // wait for bootstrapping to begin and maybe handle error
    
    89
    -      this.#provider.connect().catch(err => {
    
    90
    -        this.#stop(err.message, "");
    
    91
    -      });
    
    83
    +      // Wait for bootstrapping to begin and maybe handle error.
    
    84
    +      // Notice that we do not resolve the promise here in case of success, but
    
    85
    +      // we do it from the BootstrapStatus observer.
    
    86
    +      TorProviderBuilder.build()
    
    87
    +        .then(provider => provider.connect())
    
    88
    +        .catch(err => {
    
    89
    +          this.#stop(err.message, err.torMessage);
    
    90
    +        });
    
    92 91
         }).finally(() => {
    
    93 92
           // and remove ourselves once bootstrap is resolved
    
    94 93
           Services.obs.removeObserver(this, TorTopics.BootstrapStatus);
    
    ... ... @@ -111,8 +110,15 @@ export class TorBootstrapRequest {
    111 110
           this.#timeoutID = null;
    
    112 111
         }
    
    113 112
     
    
    113
    +    let provider;
    
    114
    +    try {
    
    115
    +      provider = await TorProviderBuilder.build();
    
    116
    +    } catch {
    
    117
    +      // This was probably the error that lead to stop in the first place.
    
    118
    +      // No need to continue propagating it.
    
    119
    +    }
    
    114 120
         try {
    
    115
    -      await this.#provider.stopBootstrap();
    
    121
    +      await provider?.stopBootstrap();
    
    116 122
         } catch (e) {
    
    117 123
           console.error("Failed to stop the bootstrap.", e);
    
    118 124
           if (!message) {
    

  • toolkit/components/tor-launcher/TorControlPort.sys.mjs
    1
    -import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
    
    2
    -
    
    3
    -/**
    
    4
    - * @callback MessageCallback A callback to receive messages from the control
    
    5
    - * port.
    
    6
    - * @param {string} message The message to handle
    
    7
    - */
    
    8
    -/**
    
    9
    - * @callback RemoveCallback A function used to remove a previously registered
    
    10
    - * callback.
    
    11
    - */
    
    1
    +/* This Source Code Form is subject to the terms of the Mozilla Public
    
    2
    + * License, v. 2.0. If a copy of the MPL was not distributed with this
    
    3
    + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    12 4
     
    
    13
    -class CallbackDispatcher {
    
    14
    -  #callbackPairs = [];
    
    5
    +import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
    
    15 6
     
    
    16
    -  /**
    
    17
    -   * Register a callback to handle a certain type of responses.
    
    18
    -   *
    
    19
    -   * @param {RegExp} regex The regex that tells which messages the callback
    
    20
    -   * wants to handle.
    
    21
    -   * @param {MessageCallback} callback The function to call
    
    22
    -   * @returns {RemoveCallback} A function to remove the just added callback
    
    23
    -   */
    
    24
    -  addCallback(regex, callback) {
    
    25
    -    this.#callbackPairs.push([regex, callback]);
    
    26
    -  }
    
    7
    +import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
    
    27 8
     
    
    28
    -  /**
    
    29
    -   * Push a certain message to all the callbacks whose regex matches it.
    
    30
    -   *
    
    31
    -   * @param {string} message The message to push to the callbacks
    
    32
    -   */
    
    33
    -  pushMessage(message) {
    
    34
    -    for (const [regex, callback] of this.#callbackPairs) {
    
    35
    -      if (message.match(regex)) {
    
    36
    -        callback(message);
    
    37
    -      }
    
    38
    -    }
    
    39
    -  }
    
    40
    -}
    
    9
    +const logger = new ConsoleAPI({
    
    10
    +  maxLogLevel: "warn",
    
    11
    +  maxLogLevelPref: "browser.tor_provider.cp_log_level",
    
    12
    +  prefix: "TorControlPort",
    
    13
    +});
    
    41 14
     
    
    42 15
     /**
    
    43 16
      * A wrapper around XPCOM sockets and buffers to handle streams in a standard
    
    ... ... @@ -273,13 +246,38 @@ class AsyncSocket {
    273 246
      * @property {Function} reject The function to reject the promise associated to
    
    274 247
      * the command
    
    275 248
      */
    
    276
    -
    
    249
    +/**
    
    250
    + * The ID of a circuit.
    
    251
    + * From control-spec.txt:
    
    252
    + *   CircuitID = 1*16 IDChar
    
    253
    + *   IDChar = ALPHA / DIGIT
    
    254
    + *   Currently, Tor only uses digits, but this may change.
    
    255
    + *
    
    256
    + * @typedef {string} CircuitID
    
    257
    + */
    
    258
    +/**
    
    259
    + * The ID of a stream.
    
    260
    + * From control-spec.txt:
    
    261
    + *   CircuitID = 1*16 IDChar
    
    262
    + *   IDChar = ALPHA / DIGIT
    
    263
    + *   Currently, Tor only uses digits, but this may change.
    
    264
    + *
    
    265
    + * @typedef {string} StreamID
    
    266
    + */
    
    267
    +/**
    
    268
    + * The fingerprint of a node.
    
    269
    + * From control-spec.txt:
    
    270
    + *   Fingerprint = "$" 40*HEXDIG
    
    271
    + * However, we do not keep the $ in our structures.
    
    272
    + *
    
    273
    + * @typedef {string} NodeFingerprint
    
    274
    + */
    
    277 275
     /**
    
    278 276
      * @typedef {object} Bridge
    
    279 277
      * @property {string} transport The transport of the bridge, or vanilla if not
    
    280 278
      * specified.
    
    281 279
      * @property {string} addr The IP address and port of the bridge
    
    282
    - * @property {string} id The fingerprint of the bridge
    
    280
    + * @property {NodeFingerprint} id The fingerprint of the bridge
    
    283 281
      * @property {string} args Optional arguments passed to the bridge
    
    284 282
      */
    
    285 283
     /**
    
    ... ... @@ -303,13 +301,9 @@ class AsyncSocket {
    303 301
      * @property {string} Flags Additional flags, such as Permanent
    
    304 302
      */
    
    305 303
     /**
    
    306
    - * @callback EventFilterCallback
    
    307
    - * @param {any} data Either a raw string, or already parsed data
    
    308
    - * @returns {boolean}
    
    309
    - */
    
    310
    -/**
    
    311
    - * @callback EventCallback
    
    312
    - * @param {any} data Either a raw string, or already parsed data
    
    304
    + * @callback EventCallback A callback to receive messages from the control
    
    305
    + * port.
    
    306
    + * @param {string} message The message to handle
    
    313 307
      */
    
    314 308
     
    
    315 309
     class TorError extends Error {
    
    ... ... @@ -324,7 +318,7 @@ class TorError extends Error {
    324 318
       }
    
    325 319
     }
    
    326 320
     
    
    327
    -class ControlSocket {
    
    321
    +export class TorController {
    
    328 322
       /**
    
    329 323
        * The socket to write to the control port.
    
    330 324
        *
    
    ... ... @@ -332,19 +326,6 @@ class ControlSocket {
    332 326
        */
    
    333 327
       #socket;
    
    334 328
     
    
    335
    -  /**
    
    336
    -   * The dispatcher used for the data we receive over the control port.
    
    337
    -   *
    
    338
    -   * @type {CallbackDispatcher}
    
    339
    -   */
    
    340
    -  #mainDispatcher = new CallbackDispatcher();
    
    341
    -  /**
    
    342
    -   * A secondary dispatcher used only to dispatch aynchronous events.
    
    343
    -   *
    
    344
    -   * @type {CallbackDispatcher}
    
    345
    -   */
    
    346
    -  #notificationDispatcher = new CallbackDispatcher();
    
    347
    -
    
    348 329
       /**
    
    349 330
        * Data we received on a read but that was not a complete line (missing a
    
    350 331
        * final CRLF). We will prepend it to the next read.
    
    ... ... @@ -365,24 +346,59 @@ class ControlSocket {
    365 346
        */
    
    366 347
       #commandQueue = [];
    
    367 348
     
    
    368
    -  constructor(asyncSocket) {
    
    369
    -    this.#socket = asyncSocket;
    
    349
    +  /**
    
    350
    +   * The event handler.
    
    351
    +   *
    
    352
    +   * @type {TorEventHandler}
    
    353
    +   */
    
    354
    +  #eventHandler;
    
    370 355
     
    
    371
    -    // #mainDispatcher pushes only async notifications (650) to
    
    372
    -    // #notificationDispatcher
    
    373
    -    this.#mainDispatcher.addCallback(
    
    374
    -      /^650/,
    
    375
    -      this.#handleNotification.bind(this)
    
    376
    -    );
    
    377
    -    // callback for handling responses and errors
    
    378
    -    this.#mainDispatcher.addCallback(
    
    379
    -      /^[245]\d\d/,
    
    380
    -      this.#handleCommandReply.bind(this)
    
    356
    +  /**
    
    357
    +   * Connect to a control port over a Unix socket.
    
    358
    +   * Not available on Windows.
    
    359
    +   *
    
    360
    +   * @param {nsIFile} ipcFile The path to the Unix socket to connect to
    
    361
    +   * @param {TorEventHandler} eventHandler The event handler to use for
    
    362
    +   * asynchronous notifications
    
    363
    +   */
    
    364
    +  static fromIpcFile(ipcFile, eventHandler) {
    
    365
    +    return new TorController(AsyncSocket.fromIpcFile(ipcFile), eventHandler);
    
    366
    +  }
    
    367
    +
    
    368
    +  /**
    
    369
    +   * Connect to a control port over a TCP socket.
    
    370
    +   *
    
    371
    +   * @param {string} host The hostname to connect to
    
    372
    +   * @param {number} port The port to connect the to
    
    373
    +   * @param {TorEventHandler} eventHandler The event handler to use for
    
    374
    +   * asynchronous notifications
    
    375
    +   */
    
    376
    +  static fromSocketAddress(host, port, eventHandler) {
    
    377
    +    return new TorController(
    
    378
    +      AsyncSocket.fromSocketAddress(host, port),
    
    379
    +      eventHandler
    
    381 380
         );
    
    381
    +  }
    
    382 382
     
    
    383
    +  /**
    
    384
    +   * Construct the controller and start the message pump.
    
    385
    +   * The class should not be constructed directly, but through static methods.
    
    386
    +   * However, this is public because JavaScript does not support private
    
    387
    +   * constructors.
    
    388
    +   *
    
    389
    +   * @private
    
    390
    +   * @param {AsyncSocket} socket The socket to use
    
    391
    +   * @param {TorEventHandler} eventHandler The event handler to use for
    
    392
    +   * asynchronous notifications
    
    393
    +   */
    
    394
    +  constructor(socket, eventHandler) {
    
    395
    +    this.#socket = socket;
    
    396
    +    this.#eventHandler = eventHandler;
    
    383 397
         this.#startMessagePump();
    
    384 398
       }
    
    385 399
     
    
    400
    +  // Socket and communication handling
    
    401
    +
    
    386 402
       /**
    
    387 403
        * Return the next line in the queue. If there is not any, block until one is
    
    388 404
        * read (or until a communication error happens, including the underlying
    
    ... ... @@ -463,6 +479,24 @@ class ControlSocket {
    463 479
         return message.join("\r\n");
    
    464 480
       }
    
    465 481
     
    
    482
    +  /**
    
    483
    +   * Handles a message that was received as a reply to a command (i.e., all the
    
    484
    +   * messages that are not async notification messages starting with 650).
    
    485
    +   *
    
    486
    +   * @param {string} message The message to handle
    
    487
    +   */
    
    488
    +  #handleCommandReply(message) {
    
    489
    +    const cmd = this.#commandQueue.shift();
    
    490
    +    // We resolve also for messages that are failures for sure. The commands
    
    491
    +    // should always check the output.
    
    492
    +    cmd.resolve(message);
    
    493
    +
    
    494
    +    // send next command if one is available
    
    495
    +    if (this.#commandQueue.length) {
    
    496
    +      this.#writeNextCommand();
    
    497
    +    }
    
    498
    +  }
    
    499
    +
    
    466 500
       /**
    
    467 501
        * Read messages on the socket and routed them to a dispatcher until the
    
    468 502
        * socket is open or some error happens (including the underlying socket being
    
    ... ... @@ -475,14 +509,25 @@ class ControlSocket {
    475 509
           // condition becoming false.
    
    476 510
           while (this.#socket) {
    
    477 511
             const message = await this.#readMessage();
    
    478
    -        // log("controlPort >> " + message);
    
    479
    -        this.#mainDispatcher.pushMessage(message);
    
    512
    +        try {
    
    513
    +          if (message.startsWith("650")) {
    
    514
    +            this.#handleNotification(message);
    
    515
    +          } else {
    
    516
    +            this.#handleCommandReply(message);
    
    517
    +          }
    
    518
    +        } catch (err) {
    
    519
    +          // E.g., if a notification handler fails. Without this internal
    
    520
    +          // try/catch we risk of closing the connection while not actually
    
    521
    +          // needed.
    
    522
    +          logger.error("Caught an exception while handling a message", err);
    
    523
    +        }
    
    480 524
           }
    
    481 525
         } catch (err) {
    
    526
    +      logger.debug("Caught an exception, closing the control port", err);
    
    482 527
           try {
    
    483 528
             this.#close(err);
    
    484 529
           } catch (ec) {
    
    485
    -        console.error(
    
    530
    +        logger.error(
    
    486 531
               "Caught another error while closing the control socket.",
    
    487 532
               ec
    
    488 533
             );
    
    ... ... @@ -497,7 +542,6 @@ class ControlSocket {
    497 542
        */
    
    498 543
       #writeNextCommand() {
    
    499 544
         const cmd = this.#commandQueue[0];
    
    500
    -    // log("controlPort << " + cmd.commandString);
    
    501 545
         this.#socket.write(`${cmd.commandString}\r\n`).catch(cmd.reject);
    
    502 546
       }
    
    503 547
     
    
    ... ... @@ -508,12 +552,11 @@ class ControlSocket {
    508 552
        * needs to handle multi-line messages.
    
    509 553
        *
    
    510 554
        * @param {string} commandString
    
    511
    -   * @returns {Promise<string>} The message sent by the control port. It will
    
    512
    -   * always start with 2xx. In case of other codes the function will throw,
    
    513
    -   * instead. This means that the return value will never be an empty string
    
    514
    -   * (even though it will not include the final CRLF).
    
    555
    +   * @returns {Promise<string>} The message sent by the control port. The return
    
    556
    +   * value should never be an empty string (even though it will not include the
    
    557
    +   * final CRLF).
    
    515 558
        */
    
    516
    -  async sendCommand(commandString) {
    
    559
    +  async #sendCommand(commandString) {
    
    517 560
         if (!this.#socket) {
    
    518 561
           throw new Error("ControlSocket not open");
    
    519 562
         }
    
    ... ... @@ -534,42 +577,16 @@ class ControlSocket {
    534 577
       }
    
    535 578
     
    
    536 579
       /**
    
    537
    -   * Handles a message starting with 2xx, 4xx, or 5xx.
    
    538
    -   * This function should be used only as a callback for the main dispatcher.
    
    539
    -   *
    
    540
    -   * @param {string} message The message to handle
    
    541
    -   */
    
    542
    -  #handleCommandReply(message) {
    
    543
    -    const cmd = this.#commandQueue.shift();
    
    544
    -    if (message[0] === "2") {
    
    545
    -      cmd.resolve(message);
    
    546
    -    } else if (message.match(/^[45]/)) {
    
    547
    -      cmd.reject(new TorError(cmd.commandString, message));
    
    548
    -    } else {
    
    549
    -      // This should never happen, as the dispatcher should filter the messages
    
    550
    -      // already.
    
    551
    -      cmd.reject(
    
    552
    -        new Error(`Received unexpected message:\n----\n${message}\n----`)
    
    553
    -      );
    
    554
    -    }
    
    555
    -
    
    556
    -    // send next command if one is available
    
    557
    -    if (this.#commandQueue.length) {
    
    558
    -      this.#writeNextCommand();
    
    559
    -    }
    
    560
    -  }
    
    561
    -
    
    562
    -  /**
    
    563
    -   * Re-route an event message to the notification dispatcher.
    
    564
    -   * This function should be used only as a callback for the main dispatcher.
    
    580
    +   * Send a simple command whose response is expected to be simply a "250 OK".
    
    581
    +   * The function will not return a reply, but will throw if an unexpected one
    
    582
    +   * is received.
    
    565 583
        *
    
    566
    -   * @param {string} message The message received on the control port
    
    584
    +   * @param {string} command The command to send
    
    567 585
        */
    
    568
    -  #handleNotification(message) {
    
    569
    -    try {
    
    570
    -      this.#notificationDispatcher.pushMessage(message);
    
    571
    -    } catch (e) {
    
    572
    -      console.error("An event watcher threw", e);
    
    586
    +  async #sendCommandSimple(command) {
    
    587
    +    const reply = await this.#sendCommand(command);
    
    588
    +    if (!/^250 OK\s*$/i.test(reply)) {
    
    589
    +      throw new TorError(command, reply);
    
    573 590
         }
    
    574 591
       }
    
    575 592
     
    
    ... ... @@ -581,6 +598,7 @@ class ControlSocket {
    581 598
        * rejection reason to the commands that are still queued.
    
    582 599
        */
    
    583 600
       #close(reason) {
    
    601
    +    logger.info("Closing the control port", reason);
    
    584 602
         const error = new Error(
    
    585 603
           "The control socket has been closed" +
    
    586 604
             (reason ? `: ${reason.message}` : "")
    
    ... ... @@ -604,98 +622,29 @@ class ControlSocket {
    604 622
         this.#close(null);
    
    605 623
       }
    
    606 624
     
    
    607
    -  /**
    
    608
    -   * Register an event watcher.
    
    609
    -   *
    
    610
    -   * @param {RegExp} regex The regex to filter on messages to receive
    
    611
    -   * @param {MessageCallback} callback The callback for the messages
    
    612
    -   */
    
    613
    -  addNotificationCallback(regex, callback) {
    
    614
    -    this.#notificationDispatcher.addCallback(regex, callback);
    
    615
    -  }
    
    616
    -
    
    617 625
       /**
    
    618 626
        * Tells whether the underlying socket is still open.
    
    619 627
        */
    
    620 628
       get isOpen() {
    
    621 629
         return !!this.#socket;
    
    622 630
       }
    
    623
    -}
    
    624
    -
    
    625
    -class TorController {
    
    626
    -  /**
    
    627
    -   * The control socket
    
    628
    -   *
    
    629
    -   * @type {ControlSocket}
    
    630
    -   */
    
    631
    -  #socket;
    
    632
    -
    
    633
    -  /**
    
    634
    -   * Builds a new TorController.
    
    635
    -   *
    
    636
    -   * @param {AsyncSocket} socket The socket to communicate to the control port
    
    637
    -   */
    
    638
    -  constructor(socket) {
    
    639
    -    this.#socket = new ControlSocket(socket);
    
    640
    -  }
    
    641
    -
    
    642
    -  /**
    
    643
    -   * Tells whether the underlying socket is open.
    
    644
    -   *
    
    645
    -   * @returns {boolean}
    
    646
    -   */
    
    647
    -  get isOpen() {
    
    648
    -    return this.#socket.isOpen;
    
    649
    -  }
    
    650
    -
    
    651
    -  /**
    
    652
    -   * Close the underlying socket.
    
    653
    -   */
    
    654
    -  close() {
    
    655
    -    this.#socket.close();
    
    656
    -  }
    
    657
    -
    
    658
    -  /**
    
    659
    -   * Send a command over the control port.
    
    660
    -   * TODO: Make this function private, and force the operations to go through
    
    661
    -   * specialized methods.
    
    662
    -   *
    
    663
    -   * @param {string} cmd The command to send
    
    664
    -   * @returns {Promise<string>} A 2xx response obtained from the control port.
    
    665
    -   * For other codes, this function will throw. The returned string will never
    
    666
    -   * be empty.
    
    667
    -   */
    
    668
    -  async sendCommand(cmd) {
    
    669
    -    return this.#socket.sendCommand(cmd);
    
    670
    -  }
    
    671
    -
    
    672
    -  /**
    
    673
    -   * Send a simple command whose response is expected to be simply a "250 OK".
    
    674
    -   * The function will not return a reply, but will throw if an unexpected one
    
    675
    -   * is received.
    
    676
    -   *
    
    677
    -   * @param {string} command The command to send
    
    678
    -   */
    
    679
    -  async #sendCommandSimple(command) {
    
    680
    -    const reply = await this.sendCommand(command);
    
    681
    -    if (!/^250 OK\s*$/i.test(reply)) {
    
    682
    -      throw new TorError(command, reply);
    
    683
    -    }
    
    684
    -  }
    
    685 631
     
    
    686 632
       /**
    
    687 633
        * Authenticate to the tor daemon.
    
    688 634
        * Notice that a failure in the authentication makes the connection close.
    
    689 635
        *
    
    690
    -   * @param {string} password The password for the control port.
    
    636
    +   * @param {Uint8Array} password The password for the control port, as an array
    
    637
    +   * of bytes
    
    691 638
        */
    
    692 639
       async authenticate(password) {
    
    693
    -    if (password) {
    
    694
    -      this.#expectString(password, "password");
    
    695
    -    }
    
    696
    -    await this.#sendCommandSimple(`authenticate ${password || ""}`);
    
    640
    +    const passwordString = Array.from(password ?? [], b =>
    
    641
    +      b.toString(16).padStart(2, "0")
    
    642
    +    ).join("");
    
    643
    +    await this.#sendCommandSimple(`authenticate ${passwordString}`);
    
    697 644
       }
    
    698 645
     
    
    646
    +  // Information
    
    647
    +
    
    699 648
       /**
    
    700 649
        * Sends a GETINFO for a single key.
    
    701 650
        * control-spec.txt says "one ReplyLine is sent for each requested value", so,
    
    ... ... @@ -713,10 +662,10 @@ class TorController {
    713 662
       async #getInfo(key) {
    
    714 663
         this.#expectString(key);
    
    715 664
         const cmd = `GETINFO ${key}`;
    
    716
    -    const reply = await this.sendCommand(cmd);
    
    665
    +    const reply = await this.#sendCommand(cmd);
    
    717 666
         const match =
    
    718 667
           reply.match(/^250-([^=]+)=(.*)$/m) ||
    
    719
    -      reply.match(/^250\+([^=]+)=([\s\S]*?)^\.\r?\n^250 OK\s*$/m);
    
    668
    +      reply.match(/^250\+([^=]+)=\r?\n(.*?)\r?\n^\.\r?\n^250 OK\s*$/ms);
    
    720 669
         if (!match || match[1] !== key) {
    
    721 670
           throw new TorError(cmd, reply);
    
    722 671
         }
    
    ... ... @@ -777,6 +726,17 @@ class TorController {
    777 726
         return this.#getInfo(`ip-to-country/${ip}`);
    
    778 727
       }
    
    779 728
     
    
    729
    +  /**
    
    730
    +   * Ask Tor a list of circuits.
    
    731
    +   *
    
    732
    +   * @returns {string[]} An array with a string for each line
    
    733
    +   */
    
    734
    +  async getCircuits() {
    
    735
    +    const circuits = await this.#getInfo("circuit-status");
    
    736
    +    // TODO: Do more parsing once we move the event parsing to this class!
    
    737
    +    return circuits.split(/\r?\n/);
    
    738
    +  }
    
    739
    +
    
    780 740
       // Configuration
    
    781 741
     
    
    782 742
       /**
    
    ... ... @@ -805,7 +765,7 @@ class TorController {
    805 765
           throw new Error("The key can be composed only of letters and numbers.");
    
    806 766
         }
    
    807 767
         const cmd = `GETCONF ${key}`;
    
    808
    -    const reply = await this.sendCommand(cmd);
    
    768
    +    const reply = await this.#sendCommand(cmd);
    
    809 769
         // From control-spec.txt: a 'default' value semantically different from an
    
    810 770
         // empty string will not have an equal sign, just `250 $key`.
    
    811 771
         const defaultRe = new RegExp(`^250[-\\s]${key}$`, "gim");
    
    ... ... @@ -924,7 +884,7 @@ class TorController {
    924 884
        */
    
    925 885
       async onionAuthViewKeys() {
    
    926 886
         const cmd = "onion_client_auth_view";
    
    927
    -    const message = await this.sendCommand(cmd);
    
    887
    +    const message = await this.#sendCommand(cmd);
    
    928 888
         // Either `250-CLIENT`, or `250 OK` if no keys are available.
    
    929 889
         if (!message.startsWith("250")) {
    
    930 890
           throw new TorError(cmd, message);
    
    ... ... @@ -964,7 +924,7 @@ class TorController {
    964 924
         if (isPermanent) {
    
    965 925
           cmd += " Flags=Permanent";
    
    966 926
         }
    
    967
    -    const reply = await this.sendCommand(cmd);
    
    927
    +    const reply = await this.#sendCommand(cmd);
    
    968 928
         const status = reply.substring(0, 3);
    
    969 929
         if (status !== "250" && status !== "251" && status !== "252") {
    
    970 930
           throw new TorError(cmd, reply);
    
    ... ... @@ -980,7 +940,7 @@ class TorController {
    980 940
       async onionAuthRemove(address) {
    
    981 941
         this.#expectString(address, "address");
    
    982 942
         const cmd = `onion_client_auth_remove ${address}`;
    
    983
    -    const reply = await this.sendCommand(cmd);
    
    943
    +    const reply = await this.#sendCommand(cmd);
    
    984 944
         const status = reply.substring(0, 3);
    
    985 945
         if (status !== "250" && status !== "251") {
    
    986 946
           throw new TorError(cmd, reply);
    
    ... ... @@ -1035,19 +995,77 @@ class TorController {
    1035 995
       }
    
    1036 996
     
    
    1037 997
       /**
    
    1038
    -   * Watches for a particular type of asynchronous event.
    
    1039
    -   * Notice: we only observe `"650" SP...` events, currently (no `650+...` or
    
    1040
    -   * `650-...` events).
    
    1041
    -   * Also, you need to enable the events in the control port with SETEVENTS,
    
    1042
    -   * first.
    
    998
    +   * Parse an asynchronous event and pass the data to the relative handler.
    
    999
    +   * Only single-line messages are currently supported.
    
    1043 1000
        *
    
    1044
    -   * @param {string} type The event type to catch
    
    1045
    -   * @param {EventCallback} callback The callback that will handle the event
    
    1001
    +   * @param {string} message The message received on the control port. It should
    
    1002
    +   * starts with `"650" SP`.
    
    1046 1003
        */
    
    1047
    -  watchEvent(type, callback) {
    
    1048
    -    this.#expectString(type, "type");
    
    1049
    -    const start = `650 ${type}`;
    
    1050
    -    this.#socket.addNotificationCallback(new RegExp(`^${start}`), callback);
    
    1004
    +  #handleNotification(message) {
    
    1005
    +    if (!this.#eventHandler) {
    
    1006
    +      return;
    
    1007
    +    }
    
    1008
    +    const data = message.match(/^650\s+(?<type>\S+)\s*(?<data>.*)?/);
    
    1009
    +    if (!data) {
    
    1010
    +      return;
    
    1011
    +    }
    
    1012
    +    switch (data.groups.type) {
    
    1013
    +      case "STATUS_CLIENT":
    
    1014
    +        let status;
    
    1015
    +        try {
    
    1016
    +          status = this.#parseBootstrapStatus(data.groups.data);
    
    1017
    +        } catch (e) {
    
    1018
    +          // Probably, a non bootstrap client status
    
    1019
    +          logger.debug(`Failed to parse STATUS_CLIENT: ${data.groups.data}`, e);
    
    1020
    +          break;
    
    1021
    +        }
    
    1022
    +        this.#eventHandler.onBootstrapStatus(status);
    
    1023
    +        break;
    
    1024
    +      case "CIRC":
    
    1025
    +        const builtEvent =
    
    1026
    +          /^(?<ID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(,?\$([0-9a-fA-F]{40})(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
    
    1027
    +            data.groups.data
    
    1028
    +          );
    
    1029
    +        const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(
    
    1030
    +          data.groups.data
    
    1031
    +        );
    
    1032
    +        if (builtEvent) {
    
    1033
    +          const fp = /\$([0-9a-fA-F]{40})/g;
    
    1034
    +          const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
    
    1035
    +            g[1].toUpperCase()
    
    1036
    +          );
    
    1037
    +          // In some cases, we might already receive SOCKS credentials in the
    
    1038
    +          // line. However, this might be a problem with onion services: we get
    
    1039
    +          // also a 4-hop circuit that we likely do not want to show to the
    
    1040
    +          // user, especially because it is used only temporarily, and it would
    
    1041
    +          // need a technical explaination.
    
    1042
    +          // const credentials = this.#parseCredentials(data.groups.data);
    
    1043
    +          this.#eventHandler.onCircuitBuilt(builtEvent.groups.ID, nodes);
    
    1044
    +        } else if (closedEvent) {
    
    1045
    +          this.#eventHandler.onCircuitClosed(closedEvent.groups.ID);
    
    1046
    +        }
    
    1047
    +        break;
    
    1048
    +      case "STREAM":
    
    1049
    +        const succeeedEvent =
    
    1050
    +          /^(?<StreamID>[a-zA-Z0-9]{1,16})\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec(
    
    1051
    +            data.groups.data
    
    1052
    +          );
    
    1053
    +        if (succeeedEvent) {
    
    1054
    +          const credentials = this.#parseCredentials(data.groups.data);
    
    1055
    +          this.#eventHandler.onStreamSucceeded(
    
    1056
    +            succeeedEvent.groups.StreamID,
    
    1057
    +            succeeedEvent.groups.CircuitID,
    
    1058
    +            credentials?.username ?? null,
    
    1059
    +            credentials?.password ?? null
    
    1060
    +          );
    
    1061
    +        }
    
    1062
    +        break;
    
    1063
    +      case "NOTICE":
    
    1064
    +      case "WARN":
    
    1065
    +      case "ERR":
    
    1066
    +        this.#eventHandler.onLogMessage(data.groups.type, data.groups.data);
    
    1067
    +        break;
    
    1068
    +    }
    
    1051 1069
       }
    
    1052 1070
     
    
    1053 1071
       // Other helpers
    
    ... ... @@ -1092,6 +1110,24 @@ class TorController {
    1092 1110
         }
    
    1093 1111
       }
    
    1094 1112
     
    
    1113
    +  /**
    
    1114
    +   * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and
    
    1115
    +   * SOCKS_PASSWORD.
    
    1116
    +   *
    
    1117
    +   * @param {string} line The circ or stream line to check
    
    1118
    +   * @returns {object?} The credentials, or null if not found
    
    1119
    +   */
    
    1120
    +  #parseCredentials(line) {
    
    1121
    +    const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line);
    
    1122
    +    const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line);
    
    1123
    +    return username && password
    
    1124
    +      ? {
    
    1125
    +          username: TorParsers.unescapeString(username[1]),
    
    1126
    +          password: TorParsers.unescapeString(password[1]),
    
    1127
    +        }
    
    1128
    +      : null;
    
    1129
    +  }
    
    1130
    +
    
    1095 1131
       /**
    
    1096 1132
        * Return an object with all the matches that are in the form `key="value"` or
    
    1097 1133
        * `key=value`. The values will be unescaped, but no additional parsing will
    
    ... ... @@ -1112,67 +1148,54 @@ class TorController {
    1112 1148
       }
    
    1113 1149
     }
    
    1114 1150
     
    
    1115
    -const controlPortInfo = {};
    
    1116
    -
    
    1117 1151
     /**
    
    1118
    - * Sets Tor control port connection parameters to be used in future calls to
    
    1119
    - * the controller() function.
    
    1152
    + * @typedef {object} TorEventHandler
    
    1153
    + * The event handler interface.
    
    1154
    + * The controller owner can implement this methods to receive asynchronous
    
    1155
    + * notifications from the controller.
    
    1120 1156
      *
    
    1121
    - * Example:
    
    1122
    - *   configureControlPortModule(undefined, "127.0.0.1", 9151, "MyPassw0rd");
    
    1157
    + * @property {OnBootstrapStatus} onBootstrapStatus Called when a bootstrap
    
    1158
    + * status is received (i.e., a STATUS_CLIENT event with a BOOTSTRAP action)
    
    1159
    + * @property {OnLogMessage} onLogMessage Called when a log message is received
    
    1160
    + * (i.e., a NOTICE, WARN or ERR notification)
    
    1161
    + * @property {OnCircuitBuilt} onCircuitBuilt Called when a circuit is built
    
    1162
    + * (i.e., a CIRC event with a BUILT status)
    
    1163
    + * @property {OnCircuitClosed} onCircuitClosed Called when a circuit is closed
    
    1164
    + * (i.e., a CIRC event with a CLOSED status)
    
    1165
    + * @property {OnStreamSucceeded} onStreamSucceeded Called when a stream receives
    
    1166
    + * a reply (i.e., a STREAM event with a SUCCEEDED status)
    
    1167
    + */
    
    1168
    +/**
    
    1169
    + * @callback OnBootstrapStatus
    
    1123 1170
      *
    
    1124
    - * @param {nsIFile?} ipcFile An optional file to use to communicate to the
    
    1125
    - * control port on Unix platforms
    
    1126
    - * @param {string?} host The hostname to connect to the control port. Mutually
    
    1127
    - * exclusive with ipcFile
    
    1128
    - * @param {integer?} port The port number of the control port. To be used only
    
    1129
    - * with host. The default is 9151.
    
    1130
    - * @param {string} password The password of the control port in clear text.
    
    1171
    + * @param {object} status An object with the bootstrap information. Its keys
    
    1172
    + * depend on what the arguments sent by the tor daemon
    
    1131 1173
      */
    
    1132
    -export function configureControlPortModule(ipcFile, host, port, password) {
    
    1133
    -  controlPortInfo.ipcFile = ipcFile;
    
    1134
    -  controlPortInfo.host = host;
    
    1135
    -  controlPortInfo.port = port || 9151;
    
    1136
    -  controlPortInfo.password = password;
    
    1137
    -}
    
    1138
    -
    
    1139 1174
     /**
    
    1140
    - * Instantiates and returns a controller object that is connected and
    
    1141
    - * authenticated to a Tor ControlPort using the connection parameters
    
    1142
    - * provided in the most recent call to configureControlPortModule().
    
    1175
    + * @callback OnLogMessage
    
    1143 1176
      *
    
    1144
    - * Example:
    
    1145
    - *     // Get a new controller
    
    1146
    - *     let c = await controller();
    
    1147
    - *     // Send command and receive a `250` reply or an error message:
    
    1148
    - *     let replyPromise = await c.getInfo("ip-to-country/16.16.16.16");
    
    1149
    - *     // Close the controller permanently
    
    1150
    - *     c.close();
    
    1177
    + * @param {string} type The type of message (NOTICE, WARNING, ERR, etc...)
    
    1178
    + * @param {string} message The actual log message
    
    1179
    + */
    
    1180
    +/**
    
    1181
    + * @callback OnCircuitBuilt
    
    1182
    + *
    
    1183
    + * @param {CircuitID} id The id of the circuit that has been built
    
    1184
    + * @param {NodeFingerprint[]} nodes The onion routers composing the circuit
    
    1185
    + */
    
    1186
    +/**
    
    1187
    + * @callback OnCircuitClosed
    
    1188
    + *
    
    1189
    + * @param {CircuitID} id The id of the circuit that has been closed
    
    1190
    + */
    
    1191
    +/**
    
    1192
    + * @callback OnStreamSucceeded
    
    1193
    + *
    
    1194
    + * @param {StreamID} streamId The id of the stream that switched to the succeeded
    
    1195
    + * state
    
    1196
    + * @param {CircuitID} circuitId The id of the circuit the stream is using
    
    1197
    + * @param {string?} username The SOCKS username associated to the stream, or
    
    1198
    + * null if not available
    
    1199
    + * @param {string?} username The SOCKS password associated to the stream, or
    
    1200
    + * null if not available
    
    1151 1201
      */
    1152
    -export async function controller() {
    
    1153
    -  if (!controlPortInfo.ipcFile && !controlPortInfo.host) {
    
    1154
    -    throw new Error("Please call configureControlPortModule first");
    
    1155
    -  }
    
    1156
    -  let socket;
    
    1157
    -  if (controlPortInfo.ipcFile) {
    
    1158
    -    socket = AsyncSocket.fromIpcFile(controlPortInfo.ipcFile);
    
    1159
    -  } else {
    
    1160
    -    socket = AsyncSocket.fromSocketAddress(
    
    1161
    -      controlPortInfo.host,
    
    1162
    -      controlPortInfo.port
    
    1163
    -    );
    
    1164
    -  }
    
    1165
    -  const controller = new TorController(socket);
    
    1166
    -  try {
    
    1167
    -    await controller.authenticate(controlPortInfo.password);
    
    1168
    -  } catch (e) {
    
    1169
    -    try {
    
    1170
    -      controller.close();
    
    1171
    -    } catch (ec) {
    
    1172
    -      // TODO: Use a custom logger?
    
    1173
    -      console.error("Cannot close the socket", ec);
    
    1174
    -    }
    
    1175
    -    throw e;
    
    1176
    -  }
    
    1177
    -  return controller;
    
    1178
    -}

  • toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
    ... ... @@ -136,8 +136,11 @@ class TorDomainIsolatorImpl {
    136 136
       init() {
    
    137 137
         logger.info("Setup circuit isolation by domain and user context");
    
    138 138
     
    
    139
    -    if (Services.prefs.getBoolPref(NON_TOR_PROXY_PREF)) {
    
    139
    +    if (Services.prefs.getBoolPref(NON_TOR_PROXY_PREF, false)) {
    
    140 140
           this.#isolationEnabled = false;
    
    141
    +      logger.info(
    
    142
    +        `The domain isolation will not be enabled because of ${NON_TOR_PROXY_PREF}.`
    
    143
    +      );
    
    141 144
         }
    
    142 145
         this.#setupProxyFilter();
    
    143 146
     
    
    ... ... @@ -257,7 +260,8 @@ class TorDomainIsolatorImpl {
    257 260
           );
    
    258 261
           this.clearIsolation();
    
    259 262
           try {
    
    260
    -        await lazy.TorProviderBuilder.build().newnym();
    
    263
    +        const provider = await lazy.TorProviderBuilder.build();
    
    264
    +        await provider.newnym();
    
    261 265
           } catch (e) {
    
    262 266
             logger.error("Could not send the newnym command", e);
    
    263 267
             // TODO: What UX to use here? See tor-browser#41708
    
    ... ... @@ -305,7 +309,9 @@ class TorDomainIsolatorImpl {
    305 309
             try {
    
    306 310
               const searchParams = new URLSearchParams(loadingPrincipalURI.query);
    
    307 311
               if (searchParams.has("url")) {
    
    308
    -            firstPartyDomain = Services.eTLD.getSchemelessSite(Services.io.newURI(searchParams.get("url")));
    
    312
    +            firstPartyDomain = Services.eTLD.getSchemelessSite(
    
    313
    +              Services.io.newURI(searchParams.get("url"))
    
    314
    +            );
    
    309 315
               }
    
    310 316
             } catch (e) {
    
    311 317
               logger.error("Failed to get first party domain for about:reader", e);
    
    ... ... @@ -562,10 +568,9 @@ class TorDomainIsolatorImpl {
    562 568
           return;
    
    563 569
         }
    
    564 570
     
    
    571
    +    const provider = await lazy.TorProviderBuilder.build();
    
    565 572
         data = await Promise.all(
    
    566
    -      circuit.map(fingerprint =>
    
    567
    -        lazy.TorProviderBuilder.build().getNodeInfo(fingerprint)
    
    568
    -      )
    
    573
    +      circuit.map(fingerprint => provider.getNodeInfo(fingerprint))
    
    569 574
         );
    
    570 575
         this.#knownCircuits.set(id, data);
    
    571 576
         // We know that something changed, but we cannot know if anyone is
    
    ... ... @@ -673,7 +678,9 @@ function getDomainForBrowser(browser) {
    673 678
           try {
    
    674 679
             const searchParams = new URLSearchParams(documentURI.query);
    
    675 680
             if (searchParams.has("url")) {
    
    676
    -          fpd = Services.eTLD.getSchemelessSite(Services.io.newURI(searchParams.get("url")));
    
    681
    +          fpd = Services.eTLD.getSchemelessSite(
    
    682
    +            Services.io.newURI(searchParams.get("url"))
    
    683
    +          );
    
    677 684
             }
    
    678 685
           } catch (e) {
    
    679 686
             logger.error("Failed to get first party domain for about:reader", e);
    

  • toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs
    ... ... @@ -366,6 +366,38 @@ export const TorLauncherUtil = Object.freeze({
    366 366
         return btnIndex === 0;
    
    367 367
       },
    
    368 368
     
    
    369
    +  /**
    
    370
    +   * Ask the user whether they desire to restart tor.
    
    371
    +   *
    
    372
    +   * @param {boolean} initError If we could connect to the control port at
    
    373
    +   * least once and we are showing this prompt because the tor process exited
    
    374
    +   * suddenly, we will display a different message
    
    375
    +   * @returns {boolean} true if the user asked to restart tor
    
    376
    +   */
    
    377
    +  showRestartPrompt(initError) {
    
    378
    +    let s;
    
    379
    +    if (initError) {
    
    380
    +      const key = "tor_exited_during_startup";
    
    381
    +      s = this.getLocalizedString(key);
    
    382
    +    } else {
    
    383
    +      // tor exited suddenly, so configuration should be okay
    
    384
    +      s =
    
    385
    +        this.getLocalizedString("tor_exited") +
    
    386
    +        "\n\n" +
    
    387
    +        this.getLocalizedString("tor_exited2");
    
    388
    +    }
    
    389
    +    const defaultBtnLabel = this.getLocalizedString("restart_tor");
    
    390
    +    let cancelBtnLabel = "OK";
    
    391
    +    try {
    
    392
    +      const kSysBundleURI = "chrome://global/locale/commonDialogs.properties";
    
    393
    +      const sysBundle = Services.strings.createBundle(kSysBundleURI);
    
    394
    +      cancelBtnLabel = sysBundle.GetStringFromName(cancelBtnLabel);
    
    395
    +    } catch (e) {
    
    396
    +      console.warn("Could not localize the cancel button", e);
    
    397
    +    }
    
    398
    +    return this.showConfirm(null, s, defaultBtnLabel, cancelBtnLabel);
    
    399
    +  },
    
    400
    +
    
    369 401
       // Localized Strings
    
    370 402
       // TODO: Switch to fluent also these ones.
    
    371 403
     
    
    ... ... @@ -503,10 +535,6 @@ export const TorLauncherUtil = Object.freeze({
    503 535
        */
    
    504 536
       getPreferredSocksConfiguration() {
    
    505 537
         if (Services.env.exists("TOR_TRANSPROXY")) {
    
    506
    -      Services.prefs.setBoolPref("network.proxy.socks_remote_dns", false);
    
    507
    -      Services.prefs.setIntPref("network.proxy.type", 0);
    
    508
    -      Services.prefs.setIntPref("network.proxy.socks_port", 0);
    
    509
    -      Services.prefs.setCharPref("network.proxy.socks", "");
    
    510 538
           return { transproxy: true };
    
    511 539
         }
    
    512 540
     
    
    ... ... @@ -576,6 +604,10 @@ export const TorLauncherUtil = Object.freeze({
    576 604
     
    
    577 605
       setProxyConfiguration(socksPortInfo) {
    
    578 606
         if (socksPortInfo.transproxy) {
    
    607
    +      Services.prefs.setBoolPref("network.proxy.socks_remote_dns", false);
    
    608
    +      Services.prefs.setIntPref("network.proxy.type", 0);
    
    609
    +      Services.prefs.setIntPref("network.proxy.socks_port", 0);
    
    610
    +      Services.prefs.setCharPref("network.proxy.socks", "");
    
    579 611
           return;
    
    580 612
         }
    
    581 613
     
    

  • toolkit/components/tor-launcher/TorParsers.sys.mjs
    ... ... @@ -6,171 +6,6 @@ export const TorStatuses = Object.freeze({
    6 6
     });
    
    7 7
     
    
    8 8
     export const TorParsers = Object.freeze({
    
    9
    -  commandSucceeded(aReply) {
    
    10
    -    return aReply?.statusCode === TorStatuses.OK;
    
    11
    -  },
    
    12
    -
    
    13
    -  // parseReply() understands simple GETCONF and GETINFO replies.
    
    14
    -  parseReply(aCmd, aKey, aReply) {
    
    15
    -    if (!aCmd || !aKey || !aReply || !aReply.lineArray?.length) {
    
    16
    -      return [];
    
    17
    -    }
    
    18
    -
    
    19
    -    const lcKey = aKey.toLowerCase();
    
    20
    -    const prefix = lcKey + "=";
    
    21
    -    const prefixLen = prefix.length;
    
    22
    -    const tmpArray = [];
    
    23
    -    for (const line of aReply.lineArray) {
    
    24
    -      var lcLine = line.toLowerCase();
    
    25
    -      if (lcLine === lcKey) {
    
    26
    -        tmpArray.push("");
    
    27
    -      } else if (lcLine.indexOf(prefix) !== 0) {
    
    28
    -        console.warn(`Unexpected ${aCmd} response: ${line}`);
    
    29
    -      } else {
    
    30
    -        try {
    
    31
    -          let s = this.unescapeString(line.substring(prefixLen));
    
    32
    -          tmpArray.push(s);
    
    33
    -        } catch (e) {
    
    34
    -          console.warn(
    
    35
    -            `Error while unescaping the response of ${aCmd}: ${line}`,
    
    36
    -            e
    
    37
    -          );
    
    38
    -        }
    
    39
    -      }
    
    40
    -    }
    
    41
    -
    
    42
    -    return tmpArray;
    
    43
    -  },
    
    44
    -
    
    45
    -  // Returns false if more lines are needed.  The first time, callers
    
    46
    -  // should pass an empty aReplyObj.
    
    47
    -  // Parsing errors are indicated by aReplyObj._parseError = true.
    
    48
    -  parseReplyLine(aLine, aReplyObj) {
    
    49
    -    if (!aLine || !aReplyObj) {
    
    50
    -      return false;
    
    51
    -    }
    
    52
    -
    
    53
    -    if (!("_parseError" in aReplyObj)) {
    
    54
    -      aReplyObj.statusCode = 0;
    
    55
    -      aReplyObj.lineArray = [];
    
    56
    -      aReplyObj._parseError = false;
    
    57
    -    }
    
    58
    -
    
    59
    -    if (aLine.length < 4) {
    
    60
    -      console.error("Unexpected response: ", aLine);
    
    61
    -      aReplyObj._parseError = true;
    
    62
    -      return true;
    
    63
    -    }
    
    64
    -
    
    65
    -    // TODO: handle + separators (data)
    
    66
    -    aReplyObj.statusCode = parseInt(aLine.substring(0, 3), 10);
    
    67
    -    const s = aLine.length < 5 ? "" : aLine.substring(4);
    
    68
    -    // Include all lines except simple "250 OK" ones.
    
    69
    -    if (aReplyObj.statusCode !== TorStatuses.OK || s !== "OK") {
    
    70
    -      aReplyObj.lineArray.push(s);
    
    71
    -    }
    
    72
    -
    
    73
    -    return aLine.charAt(3) === " ";
    
    74
    -  },
    
    75
    -
    
    76
    -  // Split aStr at spaces, accounting for quoted values.
    
    77
    -  // Returns an array of strings.
    
    78
    -  splitReplyLine(aStr) {
    
    79
    -    // Notice: the original function did not check for escaped quotes.
    
    80
    -    return aStr
    
    81
    -      .split('"')
    
    82
    -      .flatMap((token, index) => {
    
    83
    -        const inQuotedStr = index % 2 === 1;
    
    84
    -        return inQuotedStr ? `"${token}"` : token.split(" ");
    
    85
    -      })
    
    86
    -      .filter(s => s);
    
    87
    -  },
    
    88
    -
    
    89
    -  // Helper function for converting a raw controller response into a parsed object.
    
    90
    -  parseCommandResponse(reply) {
    
    91
    -    if (!reply) {
    
    92
    -      return {};
    
    93
    -    }
    
    94
    -    const lines = reply.split("\r\n");
    
    95
    -    const rv = {};
    
    96
    -    for (const line of lines) {
    
    97
    -      if (this.parseReplyLine(line, rv) || rv._parseError) {
    
    98
    -        break;
    
    99
    -      }
    
    100
    -    }
    
    101
    -    return rv;
    
    102
    -  },
    
    103
    -
    
    104
    -  // If successful, returns a JS object with these fields:
    
    105
    -  //   status.TYPE            -- "NOTICE" or "WARN"
    
    106
    -  //   status.PROGRESS        -- integer
    
    107
    -  //   status.TAG             -- string
    
    108
    -  //   status.SUMMARY         -- string
    
    109
    -  //   status.WARNING         -- string (optional)
    
    110
    -  //   status.REASON          -- string (optional)
    
    111
    -  //   status.COUNT           -- integer (optional)
    
    112
    -  //   status.RECOMMENDATION  -- string (optional)
    
    113
    -  //   status.HOSTADDR        -- string (optional)
    
    114
    -  // Returns null upon failure.
    
    115
    -  parseBootstrapStatus(aStatusMsg) {
    
    116
    -    if (!aStatusMsg || !aStatusMsg.length) {
    
    117
    -      return null;
    
    118
    -    }
    
    119
    -
    
    120
    -    let sawBootstrap = false;
    
    121
    -    const statusObj = {};
    
    122
    -    statusObj.TYPE = "NOTICE";
    
    123
    -
    
    124
    -    // The following code assumes that this is a one-line response.
    
    125
    -    for (const tokenAndVal of this.splitReplyLine(aStatusMsg)) {
    
    126
    -      let token, val;
    
    127
    -      const idx = tokenAndVal.indexOf("=");
    
    128
    -      if (idx < 0) {
    
    129
    -        token = tokenAndVal;
    
    130
    -      } else {
    
    131
    -        token = tokenAndVal.substring(0, idx);
    
    132
    -        try {
    
    133
    -          val = TorParsers.unescapeString(tokenAndVal.substring(idx + 1));
    
    134
    -        } catch (e) {
    
    135
    -          console.debug("Could not parse the token value", e);
    
    136
    -        }
    
    137
    -        if (!val) {
    
    138
    -          // skip this token/value pair.
    
    139
    -          continue;
    
    140
    -        }
    
    141
    -      }
    
    142
    -
    
    143
    -      switch (token) {
    
    144
    -        case "BOOTSTRAP":
    
    145
    -          sawBootstrap = true;
    
    146
    -          break;
    
    147
    -        case "WARN":
    
    148
    -        case "NOTICE":
    
    149
    -        case "ERR":
    
    150
    -          statusObj.TYPE = token;
    
    151
    -          break;
    
    152
    -        case "COUNT":
    
    153
    -        case "PROGRESS":
    
    154
    -          statusObj[token] = parseInt(val, 10);
    
    155
    -          break;
    
    156
    -        default:
    
    157
    -          statusObj[token] = val;
    
    158
    -          break;
    
    159
    -      }
    
    160
    -    }
    
    161
    -
    
    162
    -    if (!sawBootstrap) {
    
    163
    -      if (statusObj.TYPE === "NOTICE") {
    
    164
    -        console.info(aStatusMsg);
    
    165
    -      } else {
    
    166
    -        console.warn(aStatusMsg);
    
    167
    -      }
    
    168
    -      return null;
    
    169
    -    }
    
    170
    -
    
    171
    -    return statusObj;
    
    172
    -  },
    
    173
    -
    
    174 9
       // Escape non-ASCII characters for use within the Tor Control protocol.
    
    175 10
       // Based on Vidalia's src/common/stringutil.cpp:string_escape().
    
    176 11
       // Returns the new string.
    

  • toolkit/components/tor-launcher/TorProcess.sys.mjs
    ... ... @@ -37,12 +37,11 @@ export class TorProcess {
    37 37
       #didConnectToTorControlPort = false;
    
    38 38
     
    
    39 39
       onExit = exitCode => {};
    
    40
    -  onRestart = () => {};
    
    41 40
     
    
    42 41
       constructor(controlSettings, socksSettings) {
    
    43 42
         if (
    
    44 43
           controlSettings &&
    
    45
    -      !controlSettings.password &&
    
    44
    +      !controlSettings.password?.length &&
    
    46 45
           !controlSettings.cookieFilePath
    
    47 46
         ) {
    
    48 47
           throw new Error("Unauthenticated control port is not supported");
    
    ... ... @@ -204,43 +203,22 @@ export class TorProcess {
    204 203
       #processExitedUnexpectedly(exitCode) {
    
    205 204
         this.#subprocess = null;
    
    206 205
         this.#status = TorProcessStatus.Exited;
    
    207
    -
    
    208
    -    // TODO: Move this logic somewhere else?
    
    209
    -    let s;
    
    206
    +    // FIXME: We can probably drop #didConnectToTorControlPort and use only one
    
    207
    +    // callback. Then we can let the provider actually distinguish between the
    
    208
    +    // cases.
    
    210 209
         if (!this.#didConnectToTorControlPort) {
    
    211
    -      // tor might be misconfigured, becauser we could never connect to it
    
    212
    -      const key = "tor_exited_during_startup";
    
    213
    -      s = lazy.TorLauncherUtil.getLocalizedString(key);
    
    214
    -    } else {
    
    215
    -      // tor exited suddenly, so configuration should be okay
    
    216
    -      s =
    
    217
    -        lazy.TorLauncherUtil.getLocalizedString("tor_exited") +
    
    218
    -        "\n\n" +
    
    219
    -        lazy.TorLauncherUtil.getLocalizedString("tor_exited2");
    
    220
    -    }
    
    221
    -    logger.info(s);
    
    222
    -    const defaultBtnLabel =
    
    223
    -      lazy.TorLauncherUtil.getLocalizedString("restart_tor");
    
    224
    -    let cancelBtnLabel = "OK";
    
    225
    -    try {
    
    226
    -      const kSysBundleURI = "chrome://global/locale/commonDialogs.properties";
    
    227
    -      const sysBundle = Services.strings.createBundle(kSysBundleURI);
    
    228
    -      cancelBtnLabel = sysBundle.GetStringFromName(cancelBtnLabel);
    
    229
    -    } catch (e) {
    
    230
    -      logger.warn("Could not localize the cancel button", e);
    
    231
    -    }
    
    232
    -
    
    233
    -    const restart = lazy.TorLauncherUtil.showConfirm(
    
    234
    -      null,
    
    235
    -      s,
    
    236
    -      defaultBtnLabel,
    
    237
    -      cancelBtnLabel
    
    238
    -    );
    
    239
    -    if (restart) {
    
    240
    -      this.start().then(this.onRestart);
    
    241
    -    } else {
    
    210
    +      logger.warn("Tor exited before we could connect to its control port.");
    
    211
    +      // tor might be misconfigured, because we could never connect to it.
    
    212
    +      // Two instances of Tor Browser trying to use the same port numbers is
    
    213
    +      // also a typical scenario for this.
    
    214
    +      // This might happen very early, before the browser UI is actually
    
    215
    +      // available. So, we will tell the process owner that the process exited,
    
    216
    +      // without trying to restart it.
    
    242 217
           this.onExit(exitCode);
    
    218
    +      return;
    
    243 219
         }
    
    220
    +    logger.warn("Tor exited suddenly.");
    
    221
    +    this.onExit(exitCode);
    
    244 222
       }
    
    245 223
     
    
    246 224
       #makeArgs() {
    
    ... ... @@ -318,7 +296,7 @@ export class TorProcess {
    318 296
           this.#args.push("+__ControlPort", controlPortArg);
    
    319 297
         }
    
    320 298
     
    
    321
    -    if (this.#controlSettings.password) {
    
    299
    +    if (this.#controlSettings.password?.length) {
    
    322 300
           this.#args.push(
    
    323 301
             "HashedControlPassword",
    
    324 302
             this.#hashPassword(this.#controlSettings.password)
    
    ... ... @@ -357,36 +335,43 @@ export class TorProcess {
    357 335
         }
    
    358 336
       }
    
    359 337
     
    
    360
    -  // Based on Vidalia's TorSettings::hashPassword().
    
    361
    -  #hashPassword(aHexPassword) {
    
    362
    -    if (!aHexPassword) {
    
    363
    -      return null;
    
    364
    -    }
    
    338
    +  /**
    
    339
    +   * Hash a password to then pass it to Tor as a command line argument.
    
    340
    +   * Based on Vidalia's TorSettings::hashPassword().
    
    341
    +   *
    
    342
    +   * @param {Uint8Array} password The password, as an array of bytes
    
    343
    +   */
    
    344
    +  #hashPassword(password) {
    
    345
    +    // The password has already been checked by the caller.
    
    365 346
     
    
    366 347
         // Generate a random, 8 byte salt value.
    
    367 348
         const salt = Array.from(crypto.getRandomValues(new Uint8Array(8)));
    
    368 349
     
    
    369
    -    // Convert hex-encoded password to an array of bytes.
    
    370
    -    const password = [];
    
    371
    -    for (let i = 0; i < aHexPassword.length; i += 2) {
    
    372
    -      password.push(parseInt(aHexPassword.substring(i, i + 2), 16));
    
    373
    -    }
    
    374
    -
    
    375 350
         // Run through the S2K algorithm and convert to a string.
    
    376 351
         const toHex = v => v.toString(16).padStart(2, "0");
    
    377 352
         const arrayToHex = aArray => aArray.map(toHex).join("");
    
    378 353
         const kCodedCount = 96;
    
    379
    -    const hashVal = this.#cryptoSecretToKey(password, salt, kCodedCount);
    
    354
    +    const hashVal = this.#cryptoSecretToKey(
    
    355
    +      Array.from(password),
    
    356
    +      salt,
    
    357
    +      kCodedCount
    
    358
    +    );
    
    380 359
         return "16:" + arrayToHex(salt) + toHex(kCodedCount) + arrayToHex(hashVal);
    
    381 360
       }
    
    382 361
     
    
    383
    -  // #cryptoSecretToKey() is similar to Vidalia's crypto_secret_to_key().
    
    384
    -  // It generates and returns a hash of aPassword by following the iterated
    
    385
    -  // and salted S2K algorithm (see RFC 2440 section 3.6.1.3).
    
    386
    -  // See also https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/control-spec.txt#L3824.
    
    387
    -  // Returns an array of bytes.
    
    388
    -  #cryptoSecretToKey(aPassword, aSalt, aCodedCount) {
    
    389
    -    const inputArray = aSalt.concat(aPassword);
    
    362
    +  /**
    
    363
    +   * Generates and return a hash of a password by following the iterated and
    
    364
    +   * salted S2K algorithm (see RFC 2440 section 3.6.1.3).
    
    365
    +   * See also https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/control-spec.txt#L3824.
    
    366
    +   * #cryptoSecretToKey() is similar to Vidalia's crypto_secret_to_key().
    
    367
    +   *
    
    368
    +   * @param {Array} password The password to hash, as an array of bytes
    
    369
    +   * @param {Array} salt The salt to use for the hash, as an array of bytes
    
    370
    +   * @param {number} codedCount The counter, coded as specified in RFC 2440
    
    371
    +   * @returns {Array} The hash of the password, as an array of bytes
    
    372
    +   */
    
    373
    +  #cryptoSecretToKey(password, salt, codedCount) {
    
    374
    +    const inputArray = salt.concat(password);
    
    390 375
     
    
    391 376
         // Subtle crypto only has the final digest, and does not allow incremental
    
    392 377
         // updates.
    
    ... ... @@ -395,7 +380,7 @@ export class TorProcess {
    395 380
         );
    
    396 381
         hasher.init(hasher.SHA1);
    
    397 382
         const kEXPBIAS = 6;
    
    398
    -    let count = (16 + (aCodedCount & 15)) << ((aCodedCount >> 4) + kEXPBIAS);
    
    383
    +    let count = (16 + (codedCount & 15)) << ((codedCount >> 4) + kEXPBIAS);
    
    399 384
         while (count > 0) {
    
    400 385
           if (count > inputArray.length) {
    
    401 386
             hasher.update(inputArray, inputArray.length);
    

  • toolkit/components/tor-launcher/TorProvider.sys.mjs
    ... ... @@ -2,21 +2,17 @@
    2 2
      * License, v. 2.0. If a copy of the MPL was not distributed with this
    
    3 3
      * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    4 4
     
    
    5
    -import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
    
    5
    +import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
    
    6 6
     import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
    
    7 7
     
    
    8 8
     import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
    
    9
    -import {
    
    10
    -  TorParsers,
    
    11
    -  TorStatuses,
    
    12
    -} from "resource://gre/modules/TorParsers.sys.mjs";
    
    9
    +import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
    
    13 10
     import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs";
    
    14 11
     
    
    15 12
     const lazy = {};
    
    16 13
     ChromeUtils.defineESModuleGetters(lazy, {
    
    17
    -  controller: "resource://gre/modules/TorControlPort.sys.mjs",
    
    18
    -  configureControlPortModule: "resource://gre/modules/TorControlPort.sys.mjs",
    
    19 14
       FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
    
    15
    +  TorController: "resource://gre/modules/TorControlPort.sys.mjs",
    
    20 16
       TorProcess: "resource://gre/modules/TorProcess.sys.mjs",
    
    21 17
     });
    
    22 18
     
    
    ... ... @@ -27,20 +23,23 @@ const logger = new ConsoleAPI({
    27 23
     });
    
    28 24
     
    
    29 25
     /**
    
    30
    - * From control-spec.txt:
    
    31
    - *   CircuitID = 1*16 IDChar
    
    32
    - *   IDChar = ALPHA / DIGIT
    
    33
    - *   Currently, Tor only uses digits, but this may change.
    
    34
    - *
    
    35
    - * @typedef {string} CircuitID
    
    26
    + * @typedef {object} ControlPortSettings An object with the settings to use for
    
    27
    + * the control port. All the entries are optional, but an authentication
    
    28
    + * mechanism and a communication method must be specified.
    
    29
    + * @property {Uint8Array=} password The clear text password as an array of
    
    30
    + * bytes. It must always be defined, unless cookieFilePath is
    
    31
    + * @property {string=} cookieFilePath The path to the cookie file to use for
    
    32
    + * authentication
    
    33
    + * @property {nsIFile=} ipcFile The nsIFile object with the path to a Unix
    
    34
    + * socket to use for control socket
    
    35
    + * @property {string=} host The host to connect for a TCP control port
    
    36
    + * @property {number=} port The port number to use for a TCP control port
    
    36 37
      */
    
    37 38
     /**
    
    38
    - * The fingerprint of a node.
    
    39
    - * From control-spec.txt:
    
    40
    - *   Fingerprint = "$" 40*HEXDIG
    
    41
    - * However, we do not keep the $ in our structures.
    
    42
    - *
    
    43
    - * @typedef {string} NodeFingerprint
    
    39
    + * @typedef {object} LogEntry An object with a log message
    
    40
    + * @property {Date} date The date at which we received the message
    
    41
    + * @property {string} type The message level
    
    42
    + * @property {string} msg The message
    
    44 43
      */
    
    45 44
     /**
    
    46 45
      * Stores the data associated with a circuit node.
    
    ... ... @@ -57,15 +56,13 @@ const logger = new ConsoleAPI({
    57 56
      */
    
    58 57
     
    
    59 58
     const Preferences = Object.freeze({
    
    59
    +  ControlUseIpc: "extensions.torlauncher.control_port_use_ipc",
    
    60
    +  ControlHost: "extensions.torlauncher.control_host",
    
    61
    +  ControlPort: "extensions.torlauncher.control_port",
    
    62
    +  MaxLogEntries: "extensions.torlauncher.max_tor_log_entries",
    
    60 63
       PromptAtStartup: "extensions.torlauncher.prompt_at_startup",
    
    61 64
     });
    
    62 65
     
    
    63
    -const ControlConnTimings = Object.freeze({
    
    64
    -  initialDelayMS: 25, // Wait 25ms after the process has started, before trying to connect
    
    65
    -  maxRetryMS: 10000, // Retry at most every 10 seconds
    
    66
    -  timeoutMS: 5 * 60 * 1000, // Wait at most 5 minutes for tor to start
    
    67
    -});
    
    68
    -
    
    69 66
     /**
    
    70 67
      * This is a Tor provider for the C Tor daemon.
    
    71 68
      *
    
    ... ... @@ -73,61 +70,169 @@ const ControlConnTimings = Object.freeze({
    73 70
      * In the former case, it also takes its ownership by default.
    
    74 71
      */
    
    75 72
     export class TorProvider {
    
    76
    -  #inited = false;
    
    73
    +  /**
    
    74
    +   * The control port settings.
    
    75
    +   *
    
    76
    +   * @type {ControlPortSettings?}
    
    77
    +   */
    
    78
    +  #controlPortSettings = null;
    
    79
    +  /**
    
    80
    +   * An instance of the tor controller.
    
    81
    +   * We take for granted that if it is not null, we connected to it and managed
    
    82
    +   * to authenticate.
    
    83
    +   * Public methods can use the #controller getter, which will throw an
    
    84
    +   * exception whenever the control port is not open.
    
    85
    +   *
    
    86
    +   * @type {TorController?}
    
    87
    +   */
    
    88
    +  #controlConnection = null;
    
    89
    +  /**
    
    90
    +   * A helper that can be used to get the control port connection and assert it
    
    91
    +   * is open and it can be used.
    
    92
    +   * If this is not the case, this getter will throw.
    
    93
    +   *
    
    94
    +   * @returns {TorController}
    
    95
    +   */
    
    96
    +  get #controller() {
    
    97
    +    if (!this.#controlConnection?.isOpen) {
    
    98
    +      throw new Error("Control port connection not available.");
    
    99
    +    }
    
    100
    +    return this.#controlConnection;
    
    101
    +  }
    
    102
    +  /**
    
    103
    +   * A function that can be called to cancel the current connection attempt.
    
    104
    +   */
    
    105
    +  #cancelConnection = () => {};
    
    77 106
     
    
    78
    -  // Maintain a map of tor settings set by Tor Browser so that we don't
    
    79
    -  // repeatedly set the same key/values over and over.
    
    80
    -  // This map contains string keys to primitives or array values.
    
    81
    -  #settingsCache = new Map();
    
    107
    +  /**
    
    108
    +   * The tor process we launched.
    
    109
    +   *
    
    110
    +   * @type {TorProcess}
    
    111
    +   */
    
    112
    +  #torProcess = null;
    
    82 113
     
    
    83
    -  #controlPort = null;
    
    84
    -  #controlHost = null;
    
    85
    -  #controlIPCFile = null; // An nsIFile if using IPC for control port.
    
    86
    -  #controlPassword = null; // JS string that contains hex-encoded password.
    
    87
    -  #SOCKSPortInfo = null; // An object that contains ipcFile, host, port.
    
    114
    +  /**
    
    115
    +   * The logs we received over the control port.
    
    116
    +   * We store a finite number of log entries which can be configured with
    
    117
    +   * extensions.torlauncher.max_tor_log_entries.
    
    118
    +   *
    
    119
    +   * @type {LogEntry[]}
    
    120
    +   */
    
    121
    +  #logs = [];
    
    88 122
     
    
    89
    -  #controlConnection = null; // This is cached and reused.
    
    123
    +  #isBootstrapDone = false;
    
    124
    +  /**
    
    125
    +   * Keep the last warning to avoid broadcasting an async warning if it is the
    
    126
    +   * same one as the last broadcast.
    
    127
    +   */
    
    128
    +  #lastWarning = {};
    
    90 129
     
    
    91
    -  // Public methods
    
    130
    +  /**
    
    131
    +   * Stores the nodes of a circuit. Keys are cicuit IDs, and values are the node
    
    132
    +   * fingerprints.
    
    133
    +   *
    
    134
    +   * Theoretically, we could hook this map up to the new identity notification,
    
    135
    +   * but in practice it does not work. Tor pre-builds circuits, and the NEWNYM
    
    136
    +   * signal does not affect them. So, we might end up using a circuit that was
    
    137
    +   * built before the new identity but not yet used. If we cleaned the map, we
    
    138
    +   * risked of not having the data about it.
    
    139
    +   *
    
    140
    +   * @type {Map<CircuitID, NodeFingerprint[]>}
    
    141
    +   */
    
    142
    +  #circuits = new Map();
    
    143
    +  /**
    
    144
    +   * The last used bridge, or null if bridges are not in use or if it was not
    
    145
    +   * possible to detect the bridge. This needs the user to have specified bridge
    
    146
    +   * lines with fingerprints to work.
    
    147
    +   *
    
    148
    +   * @type {NodeFingerprint?}
    
    149
    +   */
    
    150
    +  #currentBridge = null;
    
    92 151
     
    
    152
    +  /**
    
    153
    +   * Maintain a map of tor settings set by Tor Browser so that we don't
    
    154
    +   * repeatedly set the same key/values over and over.
    
    155
    +   * This map contains string keys to primitives or array values.
    
    156
    +   *
    
    157
    +   * @type {Map<string, any>}
    
    158
    +   */
    
    159
    +  #settingsCache = new Map();
    
    160
    +
    
    161
    +  /**
    
    162
    +   * Starts a new tor process and connect to its control port, or connect to the
    
    163
    +   * control port of an existing tor daemon.
    
    164
    +   */
    
    93 165
       async init() {
    
    94
    -    if (this.#inited) {
    
    95
    -      return;
    
    166
    +    logger.debug("Initializing the Tor provider.");
    
    167
    +
    
    168
    +    const socksSettings = TorLauncherUtil.getPreferredSocksConfiguration();
    
    169
    +    logger.debug("Requested SOCKS configuration", socksSettings);
    
    170
    +
    
    171
    +    try {
    
    172
    +      await this.#setControlPortConfiguration();
    
    173
    +    } catch (e) {
    
    174
    +      logger.error("We do not have a control port configuration", e);
    
    175
    +      throw e;
    
    96 176
         }
    
    97
    -    this.#inited = true;
    
    98 177
     
    
    99
    -    Services.obs.addObserver(this, TorProviderTopics.ProcessExited);
    
    100
    -    Services.obs.addObserver(this, TorProviderTopics.ProcessRestarted);
    
    178
    +    if (socksSettings.transproxy) {
    
    179
    +      logger.info("Transparent proxy required, not starting a Tor daemon.");
    
    180
    +    } else if (this.ownsTorDaemon) {
    
    181
    +      try {
    
    182
    +        await this.#startDaemon(socksSettings);
    
    183
    +      } catch (e) {
    
    184
    +        logger.error("Failed to start the tor daemon", e);
    
    185
    +        throw e;
    
    186
    +      }
    
    187
    +    } else {
    
    188
    +      logger.debug(
    
    189
    +        "Not starting a tor daemon because we were requested not to."
    
    190
    +      );
    
    191
    +    }
    
    192
    +
    
    193
    +    try {
    
    194
    +      await this.#firstConnection();
    
    195
    +    } catch (e) {
    
    196
    +      logger.error("Cannot connect to the control port", e);
    
    197
    +      throw e;
    
    198
    +    }
    
    101 199
     
    
    102
    -    await this.#setSockets();
    
    200
    +    // We do not customize SOCKS settings, at least for now.
    
    201
    +    TorLauncherUtil.setProxyConfiguration(socksSettings);
    
    103 202
     
    
    104
    -    this._monitorInit();
    
    203
    +    logger.info("The Tor provider is ready.");
    
    105 204
     
    
    106
    -    logger.debug("TorProvider initialized");
    
    205
    +    logger.debug(`Notifying ${TorProviderTopics.ProcessIsReady}`);
    
    206
    +    Services.obs.notifyObservers(null, TorProviderTopics.ProcessIsReady);
    
    107 207
       }
    
    108 208
     
    
    209
    +  /**
    
    210
    +   * Close the connection to the tor daemon.
    
    211
    +   * When Tor is started by Tor Browser, it is configured to exit when the
    
    212
    +   * control connection is closed. Therefore, as a matter of facts, calling this
    
    213
    +   * function also makes the child Tor instance stop.
    
    214
    +   */
    
    109 215
       uninit() {
    
    110
    -    Services.obs.removeObserver(this, TorProviderTopics.ProcessExited);
    
    111
    -    Services.obs.removeObserver(this, TorProviderTopics.ProcessRestarted);
    
    112
    -    this.#closeConnection();
    
    113
    -    this._monitorUninit();
    
    114
    -  }
    
    216
    +    logger.debug("Uninitializing the Tor provider.");
    
    115 217
     
    
    116
    -  observe(subject, topic, data) {
    
    117
    -    if (topic === TorProviderTopics.ProcessExited) {
    
    118
    -      this.#closeConnection();
    
    119
    -    } else if (topic === TorProviderTopics.ProcessRestarted) {
    
    120
    -      this.#reconnect();
    
    218
    +    if (this.#torProcess) {
    
    219
    +      this.#torProcess.forget();
    
    220
    +      this.#torProcess.onExit = () => {};
    
    221
    +      this.#torProcess = null;
    
    121 222
         }
    
    223
    +
    
    224
    +    this.#closeConnection("Uninitializing the provider.");
    
    122 225
       }
    
    123 226
     
    
    124
    -  // takes a Map containing tor settings
    
    125
    -  // throws on error
    
    126
    -  async writeSettings(aSettingsObj) {
    
    227
    +  // Provider API
    
    228
    +
    
    229
    +  async writeSettings(settingsObj) {
    
    230
    +    // TODO: Move the translation from settings object to settings understood by
    
    231
    +    // tor here.
    
    127 232
         const entries =
    
    128
    -      aSettingsObj instanceof Map
    
    129
    -        ? Array.from(aSettingsObj.entries())
    
    130
    -        : Object.entries(aSettingsObj);
    
    233
    +      settingsObj instanceof Map
    
    234
    +        ? Array.from(settingsObj.entries())
    
    235
    +        : Object.entries(settingsObj);
    
    131 236
         // only write settings that have changed
    
    132 237
         const newSettings = entries.filter(([setting, value]) => {
    
    133 238
           if (!this.#settingsCache.has(setting)) {
    
    ... ... @@ -150,8 +255,7 @@ export class TorProvider {
    150 255
     
    
    151 256
         // only write if new setting to save
    
    152 257
         if (newSettings.length) {
    
    153
    -      const conn = await this.#getConnection();
    
    154
    -      await conn.setConf(Object.fromEntries(newSettings));
    
    258
    +      await this.#controller.setConf(Object.fromEntries(newSettings));
    
    155 259
     
    
    156 260
           // save settings to cache after successfully writing to Tor
    
    157 261
           for (const [setting, value] of newSettings) {
    
    ... ... @@ -160,49 +264,76 @@ export class TorProvider {
    160 264
         }
    
    161 265
       }
    
    162 266
     
    
    163
    -  // writes current tor settings to disk
    
    164 267
       async flushSettings() {
    
    165
    -    const conn = await this.#getConnection();
    
    166
    -    await conn.flushSettings();
    
    268
    +    await this.#controller.flushSettings();
    
    167 269
       }
    
    168 270
     
    
    271
    +  /**
    
    272
    +   * Start the bootstrap process.
    
    273
    +   */
    
    169 274
       async connect() {
    
    170
    -    const conn = await this.#getConnection();
    
    171
    -    await conn.setNetworkEnabled(true);
    
    172
    -    this.clearBootstrapError();
    
    275
    +    await this.#controller.setNetworkEnabled(true);
    
    276
    +    this.#lastWarning = {};
    
    173 277
         this.retrieveBootstrapStatus();
    
    174 278
       }
    
    175 279
     
    
    280
    +  /**
    
    281
    +   * Stop the bootstrap process.
    
    282
    +   */
    
    176 283
       async stopBootstrap() {
    
    177
    -    // Tell tor to disable use of the network; this should stop the bootstrap
    
    178
    -    // process.
    
    179
    -    const conn = await this.#getConnection();
    
    180
    -    await conn.setNetworkEnabled(false);
    
    284
    +    // Tell tor to disable use of the network; this should stop the bootstrap.
    
    285
    +    await this.#controller.setNetworkEnabled(false);
    
    181 286
         // We are not interested in waiting for this, nor in **catching its error**,
    
    182 287
         // so we do not await this. We just want to be notified when the bootstrap
    
    183 288
         // status is actually updated through observers.
    
    184 289
         this.retrieveBootstrapStatus();
    
    185 290
       }
    
    186 291
     
    
    292
    +  /**
    
    293
    +   * Ask Tor to swtich to new circuits and clear the DNS cache.
    
    294
    +   */
    
    187 295
       async newnym() {
    
    188
    -    const conn = await this.#getConnection();
    
    189
    -    await conn.newnym();
    
    296
    +    await this.#controller.newnym();
    
    190 297
       }
    
    191 298
     
    
    299
    +  /**
    
    300
    +   * Get the bridges Tor has been configured with.
    
    301
    +   *
    
    302
    +   * @returns {Bridge[]} The configured bridges
    
    303
    +   */
    
    192 304
       async getBridges() {
    
    193
    -    const conn = await this.#getConnection();
    
    194 305
         // Ideally, we would not need this function, because we should be the one
    
    195 306
         // setting them with TorSettings. However, TorSettings is not notified of
    
    196 307
         // change of settings. So, asking tor directly with the control connection
    
    197 308
         // is the most reliable way of getting the configured bridges, at the
    
    198 309
         // moment. Also, we are using this for the circuit display, which should
    
    199 310
         // work also when we are not configuring the tor daemon, but just using it.
    
    200
    -    return conn.getBridges();
    
    311
    +    return this.#controller.getBridges();
    
    201 312
       }
    
    202 313
     
    
    314
    +  /**
    
    315
    +   * Get the configured pluggable transports.
    
    316
    +   *
    
    317
    +   * @returns {PTInfo[]} An array with the info of all the configured pluggable
    
    318
    +   * transports.
    
    319
    +   */
    
    203 320
       async getPluggableTransports() {
    
    204
    -    const conn = await this.#getConnection();
    
    205
    -    return conn.getPluggableTransports();
    
    321
    +    return this.#controller.getPluggableTransports();
    
    322
    +  }
    
    323
    +
    
    324
    +  /**
    
    325
    +   * Ask Tor its bootstrap phase.
    
    326
    +   * This function will also update the internal state when using an external
    
    327
    +   * tor daemon.
    
    328
    +   *
    
    329
    +   * @returns {object} An object with the bootstrap information received from
    
    330
    +   * Tor. Its keys might vary, depending on the input
    
    331
    +   */
    
    332
    +  async retrieveBootstrapStatus() {
    
    333
    +    this.#processBootstrapStatus(
    
    334
    +      await this.#controller.getBootstrapPhase(),
    
    335
    +      false
    
    336
    +    );
    
    206 337
       }
    
    207 338
     
    
    208 339
       /**
    
    ... ... @@ -212,14 +343,13 @@ export class TorProvider {
    212 343
        * @returns {Promise<NodeData>}
    
    213 344
        */
    
    214 345
       async getNodeInfo(id) {
    
    215
    -    const conn = await this.#getConnection();
    
    216 346
         const node = {
    
    217 347
           fingerprint: id,
    
    218 348
           ipAddrs: [],
    
    219 349
           bridgeType: null,
    
    220 350
           regionCode: null,
    
    221 351
         };
    
    222
    -    const bridge = (await conn.getBridges())?.find(
    
    352
    +    const bridge = (await this.#controller.getBridges())?.find(
    
    223 353
           foundBridge => foundBridge.id?.toUpperCase() === id.toUpperCase()
    
    224 354
         );
    
    225 355
         if (bridge) {
    
    ... ... @@ -230,14 +360,14 @@ export class TorProvider {
    230 360
             node.ipAddrs.push(ip);
    
    231 361
           }
    
    232 362
         } else {
    
    233
    -      node.ipAddrs = await conn.getNodeAddresses(id);
    
    363
    +      node.ipAddrs = await this.#controller.getNodeAddresses(id);
    
    234 364
         }
    
    235 365
         if (node.ipAddrs.length) {
    
    236 366
           // Get the country code for the node's IP address.
    
    237 367
           try {
    
    238 368
             // Expect a 2-letter ISO3166-1 code, which should also be a valid
    
    239 369
             // BCP47 Region subtag.
    
    240
    -        const regionCode = await conn.getIPCountry(node.ipAddrs[0]);
    
    370
    +        const regionCode = await this.#controller.getIPCountry(node.ipAddrs[0]);
    
    241 371
             if (regionCode && regionCode !== "??") {
    
    242 372
               node.regionCode = regionCode.toUpperCase();
    
    243 373
             }
    
    ... ... @@ -248,301 +378,40 @@ export class TorProvider {
    248 378
         return node;
    
    249 379
       }
    
    250 380
     
    
    381
    +  /**
    
    382
    +   * Add a private key to the Tor configuration.
    
    383
    +   *
    
    384
    +   * @param {string} address The address of the onion service
    
    385
    +   * @param {string} b64PrivateKey The private key of the service, in base64
    
    386
    +   * @param {boolean} isPermanent Tell whether the key should be saved forever
    
    387
    +   */
    
    251 388
       async onionAuthAdd(address, b64PrivateKey, isPermanent) {
    
    252
    -    const conn = await this.#getConnection();
    
    253
    -    return conn.onionAuthAdd(address, b64PrivateKey, isPermanent);
    
    254
    -  }
    
    255
    -
    
    256
    -  async onionAuthRemove(address) {
    
    257
    -    const conn = await this.#getConnection();
    
    258
    -    return conn.onionAuthRemove(address);
    
    389
    +    return this.#controller.onionAuthAdd(address, b64PrivateKey, isPermanent);
    
    259 390
       }
    
    260 391
     
    
    261
    -  async onionAuthViewKeys() {
    
    262
    -    const conn = await this.#getConnection();
    
    263
    -    return conn.onionAuthViewKeys();
    
    264
    -  }
    
    265
    -
    
    266
    -  // TODO: transform the following 4 functions in getters.
    
    267
    -
    
    268
    -  // Returns Tor password string or null if an error occurs.
    
    269
    -  torGetPassword() {
    
    270
    -    return this.#controlPassword;
    
    271
    -  }
    
    272
    -
    
    273
    -  torGetControlIPCFile() {
    
    274
    -    return this.#controlIPCFile?.clone();
    
    275
    -  }
    
    276
    -
    
    277
    -  torGetControlPort() {
    
    278
    -    return this.#controlPort;
    
    279
    -  }
    
    280
    -
    
    281
    -  torGetSOCKSPortInfo() {
    
    282
    -    return this.#SOCKSPortInfo;
    
    283
    -  }
    
    284
    -
    
    285
    -  get torControlPortInfo() {
    
    286
    -    const info = {
    
    287
    -      password: this.#controlPassword,
    
    288
    -    };
    
    289
    -    if (this.#controlIPCFile) {
    
    290
    -      info.ipcFile = this.#controlIPCFile?.clone();
    
    291
    -    }
    
    292
    -    if (this.#controlPort) {
    
    293
    -      info.host = this.#controlHost;
    
    294
    -      info.port = this.#controlPort;
    
    295
    -    }
    
    296
    -    return info;
    
    297
    -  }
    
    298
    -
    
    299
    -  get torSOCKSPortInfo() {
    
    300
    -    return this.#SOCKSPortInfo;
    
    301
    -  }
    
    302
    -
    
    303
    -  async #setSockets() {
    
    304
    -    try {
    
    305
    -      const isWindows = TorLauncherUtil.isWindows;
    
    306
    -      // Determine how Tor Launcher will connect to the Tor control port.
    
    307
    -      // Environment variables get top priority followed by preferences.
    
    308
    -      if (!isWindows && Services.env.exists("TOR_CONTROL_IPC_PATH")) {
    
    309
    -        const ipcPath = Services.env.get("TOR_CONTROL_IPC_PATH");
    
    310
    -        this.#controlIPCFile = new lazy.FileUtils.File(ipcPath);
    
    311
    -      } else {
    
    312
    -        // Check for TCP host and port environment variables.
    
    313
    -        if (Services.env.exists("TOR_CONTROL_HOST")) {
    
    314
    -          this.#controlHost = Services.env.get("TOR_CONTROL_HOST");
    
    315
    -        }
    
    316
    -        if (Services.env.exists("TOR_CONTROL_PORT")) {
    
    317
    -          this.#controlPort = parseInt(
    
    318
    -            Services.env.get("TOR_CONTROL_PORT"),
    
    319
    -            10
    
    320
    -          );
    
    321
    -        }
    
    322
    -
    
    323
    -        const useIPC =
    
    324
    -          !isWindows &&
    
    325
    -          Services.prefs.getBoolPref(
    
    326
    -            "extensions.torlauncher.control_port_use_ipc",
    
    327
    -            false
    
    328
    -          );
    
    329
    -        if (!this.#controlHost && !this.#controlPort && useIPC) {
    
    330
    -          this.#controlIPCFile = TorLauncherUtil.getTorFile(
    
    331
    -            "control_ipc",
    
    332
    -            false
    
    333
    -          );
    
    334
    -        } else {
    
    335
    -          if (!this.#controlHost) {
    
    336
    -            this.#controlHost = Services.prefs.getCharPref(
    
    337
    -              "extensions.torlauncher.control_host",
    
    338
    -              "127.0.0.1"
    
    339
    -            );
    
    340
    -          }
    
    341
    -          if (!this.#controlPort) {
    
    342
    -            this.#controlPort = Services.prefs.getIntPref(
    
    343
    -              "extensions.torlauncher.control_port",
    
    344
    -              9151
    
    345
    -            );
    
    346
    -          }
    
    347
    -        }
    
    348
    -      }
    
    349
    -
    
    350
    -      // Populate _controlPassword so it is available when starting tor.
    
    351
    -      if (Services.env.exists("TOR_CONTROL_PASSWD")) {
    
    352
    -        this.#controlPassword = Services.env.get("TOR_CONTROL_PASSWD");
    
    353
    -      } else if (Services.env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
    
    354
    -        // TODO: test this code path (TOR_CONTROL_COOKIE_AUTH_FILE).
    
    355
    -        const cookiePath = Services.env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
    
    356
    -        if (cookiePath) {
    
    357
    -          this.#controlPassword = await this.#readAuthenticationCookie(
    
    358
    -            cookiePath
    
    359
    -          );
    
    360
    -        }
    
    361
    -      }
    
    362
    -      if (!this.#controlPassword) {
    
    363
    -        this.#controlPassword = this.#generateRandomPassword();
    
    364
    -      }
    
    365
    -
    
    366
    -      this.#SOCKSPortInfo = TorLauncherUtil.getPreferredSocksConfiguration();
    
    367
    -      TorLauncherUtil.setProxyConfiguration(this.#SOCKSPortInfo);
    
    368
    -
    
    369
    -      // Set the global control port info parameters.
    
    370
    -      lazy.configureControlPortModule(
    
    371
    -        this.#controlIPCFile,
    
    372
    -        this.#controlHost,
    
    373
    -        this.#controlPort,
    
    374
    -        this.#controlPassword
    
    375
    -      );
    
    376
    -    } catch (e) {
    
    377
    -      logger.error("Failed to get environment variables", e);
    
    378
    -    }
    
    379
    -  }
    
    380
    -
    
    381
    -  async #getConnection() {
    
    382
    -    if (!this.#controlConnection?.isOpen) {
    
    383
    -      this.#controlConnection = await lazy.controller();
    
    384
    -    }
    
    385
    -    return this.#controlConnection;
    
    386
    -  }
    
    387
    -
    
    388
    -  #closeConnection() {
    
    389
    -    if (this.#controlConnection) {
    
    390
    -      logger.info("Closing the control connection");
    
    391
    -      this.#controlConnection.close();
    
    392
    -      this.#controlConnection = null;
    
    393
    -    }
    
    394
    -  }
    
    395
    -
    
    396
    -  async #reconnect() {
    
    397
    -    this.#closeConnection();
    
    398
    -    await this.#getConnection();
    
    399
    -  }
    
    400
    -
    
    401
    -  async #readAuthenticationCookie(aPath) {
    
    402
    -    const bytes = await IOUtils.read(aPath);
    
    403
    -    return Array.from(bytes, b => this.#toHex(b, 2)).join("");
    
    404
    -  }
    
    405
    -
    
    406
    -  // Returns a random 16 character password, hex-encoded.
    
    407
    -  #generateRandomPassword() {
    
    408
    -    // Similar to Vidalia's crypto_rand_string().
    
    409
    -    const kPasswordLen = 16;
    
    410
    -    const kMinCharCode = "!".charCodeAt(0);
    
    411
    -    const kMaxCharCode = "~".charCodeAt(0);
    
    412
    -    let pwd = "";
    
    413
    -    for (let i = 0; i < kPasswordLen; ++i) {
    
    414
    -      const val = this.#cryptoRandInt(kMaxCharCode - kMinCharCode + 1);
    
    415
    -      if (val < 0) {
    
    416
    -        logger.error("_cryptoRandInt() failed");
    
    417
    -        return null;
    
    418
    -      }
    
    419
    -      pwd += this.#toHex(kMinCharCode + val, 2);
    
    420
    -    }
    
    421
    -
    
    422
    -    return pwd;
    
    423
    -  }
    
    424
    -
    
    425
    -  // Returns -1 upon failure.
    
    426
    -  #cryptoRandInt(aMax) {
    
    427
    -    // Based on tor's crypto_rand_int().
    
    428
    -    const maxUInt = 0xffffffff;
    
    429
    -    if (aMax <= 0 || aMax > maxUInt) {
    
    430
    -      return -1;
    
    431
    -    }
    
    432
    -
    
    433
    -    const cutoff = maxUInt - (maxUInt % aMax);
    
    434
    -    let val = cutoff;
    
    435
    -    while (val >= cutoff) {
    
    436
    -      const uint32 = new Uint32Array(1);
    
    437
    -      crypto.getRandomValues(uint32);
    
    438
    -      val = uint32[0];
    
    439
    -    }
    
    440
    -    return val % aMax;
    
    441
    -  }
    
    442
    -
    
    443
    -  #toHex(aValue, aMinLen) {
    
    444
    -    return aValue.toString(16).padStart(aMinLen, "0");
    
    445
    -  }
    
    446
    -
    
    447
    -  // Former TorMonitorService implementation.
    
    448
    -  // FIXME: Refactor and integrate more with the rest of the class.
    
    449
    -
    
    450
    -  _connection = null;
    
    451
    -  _eventHandlers = {};
    
    452
    -  _torLog = []; // Array of objects with date, type, and msg properties
    
    453
    -  _startTimeout = null;
    
    454
    -
    
    455
    -  _isBootstrapDone = false;
    
    456
    -  _lastWarningPhase = null;
    
    457
    -  _lastWarningReason = null;
    
    458
    -
    
    459
    -  _torProcess = null;
    
    460
    -
    
    461
    -  _inited = false;
    
    462
    -
    
    463 392
       /**
    
    464
    -   * Stores the nodes of a circuit. Keys are cicuit IDs, and values are the node
    
    465
    -   * fingerprints.
    
    393
    +   * Remove a private key from the Tor configuration.
    
    466 394
        *
    
    467
    -   * Theoretically, we could hook this map up to the new identity notification,
    
    468
    -   * but in practice it does not work. Tor pre-builds circuits, and the NEWNYM
    
    469
    -   * signal does not affect them. So, we might end up using a circuit that was
    
    470
    -   * built before the new identity but not yet used. If we cleaned the map, we
    
    471
    -   * risked of not having the data about it.
    
    472
    -   *
    
    473
    -   * @type {Map<CircuitID, NodeFingerprint[]>}
    
    395
    +   * @param {string} address The address of the onion service
    
    474 396
        */
    
    475
    -  _circuits = new Map();
    
    397
    +  async onionAuthRemove(address) {
    
    398
    +    return this.#controller.onionAuthRemove(address);
    
    399
    +  }
    
    400
    +
    
    476 401
       /**
    
    477
    -   * The last used bridge, or null if bridges are not in use or if it was not
    
    478
    -   * possible to detect the bridge. This needs the user to have specified bridge
    
    479
    -   * lines with fingerprints to work.
    
    402
    +   * Retrieve the list of private keys.
    
    480 403
        *
    
    481
    -   * @type {NodeFingerprint?}
    
    404
    +   * @returns {OnionAuthKeyInfo[]}
    
    482 405
        */
    
    483
    -  _currentBridge = null;
    
    484
    -
    
    485
    -  // Public methods
    
    486
    -
    
    487
    -  // Starts Tor, if needed, and starts monitoring for events
    
    488
    -  _monitorInit() {
    
    489
    -    if (this._inited) {
    
    490
    -      return;
    
    491
    -    }
    
    492
    -    this._inited = true;
    
    493
    -
    
    494
    -    // We always liten to these events, because they are needed for the circuit
    
    495
    -    // display.
    
    496
    -    this._eventHandlers = new Map([
    
    497
    -      ["CIRC", this._processCircEvent.bind(this)],
    
    498
    -      ["STREAM", this._processStreamEvent.bind(this)],
    
    499
    -    ]);
    
    500
    -
    
    501
    -    if (this.ownsTorDaemon) {
    
    502
    -      // When we own the tor daemon, we listen to more events, that are used
    
    503
    -      // for about:torconnect or for showing the logs in the settings page.
    
    504
    -      this._eventHandlers.set(
    
    505
    -        "STATUS_CLIENT",
    
    506
    -        this._processStatusClient.bind(this)
    
    507
    -      );
    
    508
    -      this._eventHandlers.set("NOTICE", this._processLog.bind(this));
    
    509
    -      this._eventHandlers.set("WARN", this._processLog.bind(this));
    
    510
    -      this._eventHandlers.set("ERR", this._processLog.bind(this));
    
    511
    -      this._controlTor();
    
    512
    -    } else {
    
    513
    -      this._startEventMonitor();
    
    514
    -    }
    
    515
    -    logger.info("TorMonitorService initialized");
    
    516
    -  }
    
    517
    -
    
    518
    -  // Closes the connection that monitors for events.
    
    519
    -  // When Tor is started by Tor Browser, it is configured to exit when the
    
    520
    -  // control connection is closed. Therefore, as a matter of facts, calling this
    
    521
    -  // function also makes the child Tor instance stop.
    
    522
    -  _monitorUninit() {
    
    523
    -    if (this._torProcess) {
    
    524
    -      this._torProcess.forget();
    
    525
    -      this._torProcess.onExit = null;
    
    526
    -      this._torProcess.onRestart = null;
    
    527
    -      this._torProcess = null;
    
    528
    -    }
    
    529
    -    this._shutDownEventMonitor();
    
    530
    -  }
    
    531
    -
    
    532
    -  async retrieveBootstrapStatus() {
    
    533
    -    if (!this._connection) {
    
    534
    -      throw new Error("Event monitor connection not available");
    
    535
    -    }
    
    536
    -
    
    537
    -    this._processBootstrapStatus(
    
    538
    -      await this._connection.getBootstrapPhase(),
    
    539
    -      true
    
    540
    -    );
    
    406
    +  async onionAuthViewKeys() {
    
    407
    +    return this.#controller.onionAuthViewKeys();
    
    541 408
       }
    
    542 409
     
    
    543
    -  // Returns captured log message as a text string (one message per line).
    
    410
    +  /**
    
    411
    +   * Returns captured log message as a text string (one message per line).
    
    412
    +   */
    
    544 413
       getLog() {
    
    545
    -    return this._torLog
    
    414
    +    return this.#logs
    
    546 415
           .map(logObj => {
    
    547 416
             const timeStr = logObj.date
    
    548 417
               .toISOString()
    
    ... ... @@ -553,22 +422,27 @@ export class TorProvider {
    553 422
           .join(TorLauncherUtil.isWindows ? "\r\n" : "\n");
    
    554 423
       }
    
    555 424
     
    
    556
    -  // true if we launched and control tor, false if using system tor
    
    425
    +  /**
    
    426
    +   * @returns {boolean} true if we launched and control tor, false if we are
    
    427
    +   * using system tor.
    
    428
    +   */
    
    557 429
       get ownsTorDaemon() {
    
    558 430
         return TorLauncherUtil.shouldStartAndOwnTor;
    
    559 431
       }
    
    560 432
     
    
    561 433
       get isBootstrapDone() {
    
    562
    -    return this._isBootstrapDone;
    
    563
    -  }
    
    564
    -
    
    565
    -  clearBootstrapError() {
    
    566
    -    this._lastWarningPhase = null;
    
    567
    -    this._lastWarningReason = null;
    
    434
    +    return this.#isBootstrapDone;
    
    568 435
       }
    
    569 436
     
    
    437
    +  /**
    
    438
    +   * TODO: Rename to isReady once we remove finish the migration.
    
    439
    +   *
    
    440
    +   * @returns {boolean} true if we currently have a connection to the control
    
    441
    +   * port. We take for granted that if we have one, we authenticated to it, and
    
    442
    +   * so we have already verified we can send and receive data.
    
    443
    +   */
    
    570 444
       get isRunning() {
    
    571
    -    return !!this._connection;
    
    445
    +    return this.#controlConnection?.isOpen ?? false;
    
    572 446
       }
    
    573 447
     
    
    574 448
       /**
    
    ... ... @@ -580,258 +454,380 @@ export class TorProvider {
    580 454
        * is not a bridge, or no circuit has been opened, yet.
    
    581 455
        */
    
    582 456
       get currentBridge() {
    
    583
    -    return this._currentBridge;
    
    457
    +    return this.#currentBridge;
    
    584 458
       }
    
    585 459
     
    
    586
    -  // Private methods
    
    460
    +  // Process management
    
    587 461
     
    
    588
    -  async _startProcess() {
    
    462
    +  async #startDaemon(socksSettings) {
    
    589 463
         // TorProcess should be instanced once, then always reused and restarted
    
    590 464
         // only through the prompt it exposes when the controlled process dies.
    
    591
    -    if (!this._torProcess) {
    
    592
    -      this._torProcess = new lazy.TorProcess(
    
    593
    -        this.torControlPortInfo,
    
    594
    -        this.torSOCKSPortInfo
    
    465
    +    if (this.#torProcess) {
    
    466
    +      logger.warn(
    
    467
    +        "Ignoring a request to start a tor daemon because one is already running."
    
    595 468
           );
    
    596
    -      this._torProcess.onExit = () => {
    
    597
    -        this._shutDownEventMonitor();
    
    598
    -        Services.obs.notifyObservers(null, TorProviderTopics.ProcessExited);
    
    599
    -      };
    
    600
    -      this._torProcess.onRestart = async () => {
    
    601
    -        this._shutDownEventMonitor();
    
    602
    -        await this._controlTor();
    
    603
    -        Services.obs.notifyObservers(null, TorProviderTopics.ProcessRestarted);
    
    604
    -      };
    
    469
    +      return;
    
    605 470
         }
    
    606 471
     
    
    607
    -    // Already running, but we did not start it
    
    608
    -    if (this._torProcess.isRunning) {
    
    609
    -      return false;
    
    610
    -    }
    
    472
    +    this.#torProcess = new lazy.TorProcess(
    
    473
    +      this.#controlPortSettings,
    
    474
    +      socksSettings
    
    475
    +    );
    
    476
    +    // Use a closure instead of bind because we reassign #cancelConnection.
    
    477
    +    // Also, we now assign an exit handler that cancels the first connection,
    
    478
    +    // so that a sudden exit before the first connection is completed might
    
    479
    +    // still be handled as an initialization failure.
    
    480
    +    // But after the first connection is created successfully, we will change
    
    481
    +    // the exit handler to broadcast a notification instead.
    
    482
    +    this.#torProcess.onExit = () => {
    
    483
    +      this.#cancelConnection(
    
    484
    +        "The tor process exited before the first connection"
    
    485
    +      );
    
    486
    +    };
    
    611 487
     
    
    612
    -    try {
    
    613
    -      await this._torProcess.start();
    
    614
    -      if (this._torProcess.isRunning) {
    
    615
    -        logger.info("tor started");
    
    616
    -        this._torProcessStartTime = Date.now();
    
    617
    -      }
    
    618
    -    } catch (e) {
    
    619
    -      // TorProcess already logs the error.
    
    620
    -      this._lastWarningPhase = "startup";
    
    621
    -      this._lastWarningReason = e.toString();
    
    622
    -    }
    
    623
    -    return this._torProcess.isRunning;
    
    488
    +    logger.debug("Trying to start the tor process.");
    
    489
    +    await this.#torProcess.start();
    
    490
    +    logger.info("Started a tor process");
    
    624 491
       }
    
    625 492
     
    
    626
    -  async _controlTor() {
    
    627
    -    if (!this._torProcess?.isRunning && !(await this._startProcess())) {
    
    628
    -      logger.error("Tor not running, not starting to monitor it.");
    
    629
    -      return;
    
    630
    -    }
    
    631
    -
    
    632
    -    let delayMS = ControlConnTimings.initialDelayMS;
    
    633
    -    const callback = async () => {
    
    634
    -      if (await this._startEventMonitor()) {
    
    635
    -        this.retrieveBootstrapStatus().catch(e => {
    
    636
    -          logger.warn("Could not get the initial bootstrap status", e);
    
    637
    -        });
    
    638
    -
    
    639
    -        // FIXME: TorProcess is misleading here. We should use a topic related
    
    640
    -        // to having a control port connection, instead.
    
    641
    -        logger.info(`Notifying ${TorProviderTopics.ProcessIsReady}`);
    
    642
    -        Services.obs.notifyObservers(null, TorProviderTopics.ProcessIsReady);
    
    493
    +  // Control port setup and connection
    
    643 494
     
    
    644
    -        // We reset this here hoping that _shutDownEventMonitor can interrupt
    
    645
    -        // the current monitor, either by calling clearTimeout and preventing it
    
    646
    -        // from starting, or by closing the control port connection.
    
    647
    -        if (this._startTimeout === null) {
    
    648
    -          logger.warn("Someone else reset _startTimeout!");
    
    649
    -        }
    
    650
    -        this._startTimeout = null;
    
    651
    -      } else if (
    
    652
    -        Date.now() - this._torProcessStartTime >
    
    653
    -        ControlConnTimings.timeoutMS
    
    654
    -      ) {
    
    655
    -        let s = TorLauncherUtil.getLocalizedString("tor_controlconn_failed");
    
    656
    -        this._lastWarningPhase = "startup";
    
    657
    -        this._lastWarningReason = s;
    
    658
    -        logger.info(s);
    
    659
    -        if (this._startTimeout === null) {
    
    660
    -          logger.warn("Someone else reset _startTimeout!");
    
    661
    -        }
    
    662
    -        this._startTimeout = null;
    
    663
    -      } else {
    
    664
    -        delayMS *= 2;
    
    665
    -        if (delayMS > ControlConnTimings.maxRetryMS) {
    
    666
    -          delayMS = ControlConnTimings.maxRetryMS;
    
    495
    +  /**
    
    496
    +   * Read the control port settings from environment variables and from
    
    497
    +   * preferences.
    
    498
    +   */
    
    499
    +  async #setControlPortConfiguration() {
    
    500
    +    logger.debug("Reading the control port configuration");
    
    501
    +    const settings = {};
    
    502
    +
    
    503
    +    const isWindows = Services.appinfo.OS === "WINNT";
    
    504
    +    // Determine how Tor Launcher will connect to the Tor control port.
    
    505
    +    // Environment variables get top priority followed by preferences.
    
    506
    +    if (!isWindows && Services.env.exists("TOR_CONTROL_IPC_PATH")) {
    
    507
    +      const ipcPath = Services.env.get("TOR_CONTROL_IPC_PATH");
    
    508
    +      settings.ipcFile = new lazy.FileUtils.File(ipcPath);
    
    509
    +    } else {
    
    510
    +      // Check for TCP host and port environment variables.
    
    511
    +      if (Services.env.exists("TOR_CONTROL_HOST")) {
    
    512
    +        settings.host = Services.env.get("TOR_CONTROL_HOST");
    
    513
    +      }
    
    514
    +      if (Services.env.exists("TOR_CONTROL_PORT")) {
    
    515
    +        const port = parseInt(Services.env.get("TOR_CONTROL_PORT"), 10);
    
    516
    +        if (Number.isInteger(port) && port > 0 && port <= 65535) {
    
    517
    +          settings.port = port;
    
    667 518
             }
    
    668
    -        this._startTimeout = setTimeout(() => {
    
    669
    -          logger.debug(`Control port not ready, waiting ${delayMS / 1000}s.`);
    
    670
    -          callback();
    
    671
    -        }, delayMS);
    
    672 519
           }
    
    673
    -    };
    
    674
    -    // Check again, in the unfortunate case in which the execution was alrady
    
    675
    -    // queued, but was waiting network code.
    
    676
    -    if (this._startTimeout === null) {
    
    677
    -      this._startTimeout = setTimeout(callback, delayMS);
    
    678
    -    } else {
    
    679
    -      logger.error("Possible race? Refusing to start the timeout again");
    
    680 520
         }
    
    681
    -  }
    
    682 521
     
    
    683
    -  async _startEventMonitor() {
    
    684
    -    if (this._connection) {
    
    685
    -      return true;
    
    522
    +    const useIPC =
    
    523
    +      !isWindows &&
    
    524
    +      Services.prefs.getBoolPref(Preferences.ControlUseIpc, false);
    
    525
    +    if (!settings.host && !settings.port && useIPC) {
    
    526
    +      settings.ipcFile = TorLauncherUtil.getTorFile("control_ipc", false);
    
    527
    +    } else {
    
    528
    +      if (!settings.host) {
    
    529
    +        settings.host = Services.prefs.getCharPref(
    
    530
    +          Preferences.ControlHost,
    
    531
    +          "127.0.0.1"
    
    532
    +        );
    
    533
    +      }
    
    534
    +      if (!settings.port) {
    
    535
    +        settings.port = Services.prefs.getIntPref(
    
    536
    +          Preferences.ControlPort,
    
    537
    +          9151
    
    538
    +        );
    
    539
    +      }
    
    686 540
         }
    
    687 541
     
    
    688
    -    let conn;
    
    689
    -    try {
    
    690
    -      conn = await lazy.controller();
    
    691
    -    } catch (e) {
    
    692
    -      logger.error("Cannot open a control port connection", e);
    
    693
    -      if (conn) {
    
    694
    -        try {
    
    695
    -          conn.close();
    
    696
    -        } catch (e) {
    
    697
    -          logger.error(
    
    698
    -            "Also, the connection is not null but cannot be closed",
    
    699
    -            e
    
    700
    -          );
    
    542
    +    if (Services.env.exists("TOR_CONTROL_PASSWD")) {
    
    543
    +      const password = Services.env.get("TOR_CONTROL_PASSWD");
    
    544
    +      // As per 3.5 of control-spec.txt, AUTHENTICATE can use either a quoted
    
    545
    +      // string, or a sequence of hex characters.
    
    546
    +      // However, the password is hashed byte by byte, so we need to convert the
    
    547
    +      // string to its character codes, or the hex digits to actual bytes.
    
    548
    +      // Notice that Tor requires at least one hex character, without an upper
    
    549
    +      // limit, but it does not explicitly tell how to pad an odd number of hex
    
    550
    +      // characters, so we require the user to hand an even number of hex
    
    551
    +      // digits.
    
    552
    +      // We also want to enforce the authentication if we start the daemon.
    
    553
    +      // So, if a password is not valid (not a hex sequence and not a quoted
    
    554
    +      // string), or if it is empty (including the quoted empty string), we
    
    555
    +      // force a random password.
    
    556
    +      if (
    
    557
    +        password.length >= 2 &&
    
    558
    +        password[0] === '"' &&
    
    559
    +        password[password.length - 1] === '"'
    
    560
    +      ) {
    
    561
    +        const encoder = new TextEncoder();
    
    562
    +        settings.password = encoder.encode(TorParsers.unescapeString(password));
    
    563
    +      } else if (/^([0-9a-fA-F]{2})+$/.test(password)) {
    
    564
    +        settings.password = new Uint8Array(password.length / 2);
    
    565
    +        for (let i = 0, j = 0; i < settings.password.length; i++, j += 2) {
    
    566
    +          settings.password[i] = parseInt(password.substring(j, j + 2), 16);
    
    701 567
             }
    
    702 568
           }
    
    703
    -      return false;
    
    704
    -    }
    
    705
    -
    
    706
    -    // TODO: optionally monitor INFO and DEBUG log messages.
    
    707
    -    try {
    
    708
    -      await conn.setEvents(Array.from(this._eventHandlers.keys()));
    
    709
    -    } catch (e) {
    
    710
    -      logger.error("SETEVENTS failed", e);
    
    711
    -      conn.close();
    
    712
    -      return false;
    
    713
    -    }
    
    714
    -
    
    715
    -    if (this._torProcess) {
    
    716
    -      this._torProcess.connectionWorked();
    
    717
    -    }
    
    718
    -    if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) {
    
    719
    -      try {
    
    720
    -        await this._takeTorOwnership(conn);
    
    721
    -      } catch (e) {
    
    722
    -        logger.warn("Could not take ownership of the Tor daemon", e);
    
    569
    +      if (password && !settings.password?.length) {
    
    570
    +        logger.warn(
    
    571
    +          "Invalid password specified at TOR_CONTROL_PASSWD. " +
    
    572
    +            "You should put it in double quotes, or it should be a hex-encoded sequence. " +
    
    573
    +            "The password cannot be empty. " +
    
    574
    +            "A random password will be used, instead."
    
    575
    +        );
    
    576
    +      }
    
    577
    +    } else if (Services.env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
    
    578
    +      const cookiePath = Services.env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
    
    579
    +      if (cookiePath) {
    
    580
    +        settings.cookieFilePath = cookiePath;
    
    723 581
           }
    
    724 582
         }
    
    725
    -
    
    726
    -    this._connection = conn;
    
    727
    -
    
    728
    -    for (const [type, callback] of this._eventHandlers.entries()) {
    
    729
    -      this._monitorEvent(type, callback);
    
    583
    +    if (
    
    584
    +      this.ownsTorDaemon &&
    
    585
    +      !settings.password?.length &&
    
    586
    +      !settings.cookieFilePath
    
    587
    +    ) {
    
    588
    +      settings.password = this.#generateRandomPassword();
    
    730 589
         }
    
    590
    +    this.#controlPortSettings = settings;
    
    591
    +    logger.debug("Control port configuration read");
    
    592
    +  }
    
    731 593
     
    
    732
    -    // Populate the circuit map already, in case we are connecting to an
    
    733
    -    // external tor daemon.
    
    734
    -    try {
    
    735
    -      const reply = await this._connection.sendCommand(
    
    736
    -        "GETINFO circuit-status"
    
    737
    -      );
    
    738
    -      const lines = reply.split(/\r?\n/);
    
    739
    -      if (lines.shift() === "250+circuit-status=") {
    
    740
    -        for (const line of lines) {
    
    741
    -          if (line === ".") {
    
    742
    -            break;
    
    743
    -          }
    
    744
    -          // _processCircEvent processes only one line at a time
    
    745
    -          this._processCircEvent("CIRC", [line]);
    
    594
    +  /**
    
    595
    +   * Start the first connection to the Tor daemon.
    
    596
    +   * This function should be called only once during the initialization.
    
    597
    +   */
    
    598
    +  async #firstConnection() {
    
    599
    +    let canceled = false;
    
    600
    +    let timeout = 0;
    
    601
    +    const maxDelay = 10_000;
    
    602
    +    let delay = 5;
    
    603
    +    logger.debug("Connecting to the control port for the first time.");
    
    604
    +    this.#controlConnection = await new Promise((resolve, reject) => {
    
    605
    +      this.#cancelConnection = reason => {
    
    606
    +        canceled = true;
    
    607
    +        clearTimeout(timeout);
    
    608
    +        reject(new Error(reason));
    
    609
    +      };
    
    610
    +      const tryConnect = () => {
    
    611
    +        if (this.ownsTorDaemon && !this.#torProcess?.isRunning) {
    
    612
    +          reject(new Error("The controlled tor daemon is not running."));
    
    613
    +          return;
    
    746 614
             }
    
    615
    +        this.#openControlPort()
    
    616
    +          .then(controller => {
    
    617
    +            this.#torProcess?.connectionWorked();
    
    618
    +            this.#cancelConnection = () => {};
    
    619
    +            // The cancel function should have already called reject.
    
    620
    +            if (!canceled) {
    
    621
    +              logger.info("Connected to the control port.");
    
    622
    +              resolve(controller);
    
    623
    +            }
    
    624
    +          })
    
    625
    +          .catch(e => {
    
    626
    +            if (delay < maxDelay && !canceled) {
    
    627
    +              logger.info(
    
    628
    +                `Failed to connect to the control port. Trying again in ${delay}ms.`,
    
    629
    +                e
    
    630
    +              );
    
    631
    +              timeout = setTimeout(tryConnect, delay);
    
    632
    +              delay *= 2;
    
    633
    +            } else {
    
    634
    +              reject(e);
    
    635
    +            }
    
    636
    +          });
    
    637
    +      };
    
    638
    +      tryConnect();
    
    639
    +    });
    
    640
    +
    
    641
    +    // The following code will never throw, but we still want to wait for it
    
    642
    +    // before marking the provider as initialized.
    
    643
    +
    
    644
    +    if (this.ownsTorDaemon) {
    
    645
    +      // The first connection cannot be canceled anymore, and the rest of the
    
    646
    +      // code is supposed not to fail. If the tor process exits, from now on we
    
    647
    +      // can only close the connection and broadcast a notification.
    
    648
    +      this.#torProcess.onExit = exitCode => {
    
    649
    +        logger.info(`The tor process exited with code ${exitCode}`);
    
    650
    +        this.#closeConnection("The tor process exited suddenly");
    
    651
    +        Services.obs.notifyObservers(null, TorProviderTopics.ProcessExited);
    
    652
    +      };
    
    653
    +      if (!TorLauncherUtil.shouldOnlyConfigureTor) {
    
    654
    +        await this.#takeOwnership();
    
    747 655
           }
    
    748
    -    } catch (e) {
    
    749
    -      logger.warn("Could not populate the initial circuit map", e);
    
    750 656
         }
    
    751
    -
    
    752
    -    return true;
    
    657
    +    await this.#setupEvents();
    
    753 658
       }
    
    754 659
     
    
    755
    -  // Try to become the primary controller (TAKEOWNERSHIP).
    
    756
    -  async _takeTorOwnership(conn) {
    
    660
    +  /**
    
    661
    +   * Try to become the primary controller. This will make tor exit when our
    
    662
    +   * connection is closed.
    
    663
    +   * This function cannot fail or throw (any exception will be treated as a
    
    664
    +   * warning and just logged).
    
    665
    +   */
    
    666
    +  async #takeOwnership() {
    
    667
    +    logger.debug("Taking the ownership of the tor process.");
    
    757 668
         try {
    
    758
    -      conn.takeOwnership();
    
    669
    +      await this.#controlConnection.takeOwnership();
    
    759 670
         } catch (e) {
    
    760 671
           logger.warn("Take ownership failed", e);
    
    761 672
           return;
    
    762 673
         }
    
    763 674
         try {
    
    764
    -      conn.resetOwningControllerProcess();
    
    675
    +      await this.#controlConnection.resetOwningControllerProcess();
    
    765 676
         } catch (e) {
    
    766 677
           logger.warn("Clear owning controller process failed", e);
    
    767 678
         }
    
    768 679
       }
    
    769 680
     
    
    770
    -  _monitorEvent(type, callback) {
    
    771
    -    logger.info(`Watching events of type ${type}.`);
    
    772
    -    let replyObj = {};
    
    773
    -    this._connection.watchEvent(type, line => {
    
    774
    -      if (!line) {
    
    775
    -        return;
    
    776
    -      }
    
    777
    -      logger.debug("Event response: ", line);
    
    778
    -      const isComplete = TorParsers.parseReplyLine(line, replyObj);
    
    779
    -      if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) {
    
    780
    -        return;
    
    781
    -      }
    
    782
    -      const reply = replyObj;
    
    783
    -      replyObj = {};
    
    784
    -      if (reply.statusCode !== TorStatuses.EventNotification) {
    
    785
    -        logger.error("Unexpected event status code:", reply.statusCode);
    
    786
    -        return;
    
    681
    +  /**
    
    682
    +   * Tells the Tor daemon which events we want to receive.
    
    683
    +   * This function will never throw. Any failure will be treated as a warning of
    
    684
    +   * a possibly degraded experience, not as an error.
    
    685
    +   */
    
    686
    +  async #setupEvents() {
    
    687
    +    // We always listen to these events, because they are needed for the circuit
    
    688
    +    // display.
    
    689
    +    const events = ["CIRC", "STREAM"];
    
    690
    +    if (this.ownsTorDaemon) {
    
    691
    +      events.push("STATUS_CLIENT", "NOTICE", "WARN", "ERR");
    
    692
    +      // Do not await on the first bootstrap status retrieval, and do not
    
    693
    +      // propagate its errors.
    
    694
    +      this.#controlConnection
    
    695
    +        .getBootstrapPhase()
    
    696
    +        .then(status => this.#processBootstrapStatus(status, false))
    
    697
    +        .catch(e =>
    
    698
    +          logger.error("Failed to get the first bootstrap status", e)
    
    699
    +        );
    
    700
    +    }
    
    701
    +    try {
    
    702
    +      logger.debug(`Setting events: ${events.join(" ")}`);
    
    703
    +      await this.#controlConnection.setEvents(events);
    
    704
    +    } catch (e) {
    
    705
    +      logger.error(
    
    706
    +        "We could not enable all the events we need. Tor Browser's functionalities might be reduced.",
    
    707
    +        e
    
    708
    +      );
    
    709
    +    }
    
    710
    +  }
    
    711
    +
    
    712
    +  /**
    
    713
    +   * Open a connection to the control port and authenticate to it.
    
    714
    +   * #setControlPortConfiguration must have been called before, as this function
    
    715
    +   * will follow the configuration set by it.
    
    716
    +   *
    
    717
    +   * @returns {Promise<TorController>} An authenticated TorController
    
    718
    +   */
    
    719
    +  async #openControlPort() {
    
    720
    +    let controlPort;
    
    721
    +    if (this.#controlPortSettings.ipcFile) {
    
    722
    +      controlPort = lazy.TorController.fromIpcFile(
    
    723
    +        this.#controlPortSettings.ipcFile,
    
    724
    +        this
    
    725
    +      );
    
    726
    +    } else {
    
    727
    +      controlPort = lazy.TorController.fromSocketAddress(
    
    728
    +        this.#controlPortSettings.host,
    
    729
    +        this.#controlPortSettings.port,
    
    730
    +        this
    
    731
    +      );
    
    732
    +    }
    
    733
    +    try {
    
    734
    +      let password = this.#controlPortSettings.password;
    
    735
    +      if (password === undefined && this.#controlPortSettings.cookieFilePath) {
    
    736
    +        password = await this.#readAuthenticationCookie(
    
    737
    +          this.#controlPortSettings.cookieFilePath
    
    738
    +        );
    
    787 739
           }
    
    788
    -      if (!reply.lineArray[0].startsWith(`${type} `)) {
    
    789
    -        logger.error("Wrong format for the first line:", reply.lineArray[0]);
    
    790
    -        return;
    
    740
    +      await controlPort.authenticate(password);
    
    741
    +    } catch (e) {
    
    742
    +      try {
    
    743
    +        controlPort.close();
    
    744
    +      } catch (ec) {
    
    745
    +        // Tor already closes the control port when the authentication fails.
    
    746
    +        logger.debug(
    
    747
    +          "Expected exception when closing the control port for a failed authentication",
    
    748
    +          ec
    
    749
    +        );
    
    791 750
           }
    
    792
    -      reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1);
    
    751
    +      throw e;
    
    752
    +    }
    
    753
    +    return controlPort;
    
    754
    +  }
    
    755
    +
    
    756
    +  /**
    
    757
    +   * Close the connection to the control port.
    
    758
    +   *
    
    759
    +   * @param {string} reason The reason for which we are closing the connection
    
    760
    +   * (used for logging and in case this ends up canceling the current connection
    
    761
    +   * attempt)
    
    762
    +   */
    
    763
    +  #closeConnection(reason) {
    
    764
    +    this.#cancelConnection(reason);
    
    765
    +    if (this.#controlConnection) {
    
    766
    +      logger.info("Closing the control connection", reason);
    
    793 767
           try {
    
    794
    -        callback(type, reply.lineArray);
    
    768
    +        this.#controlConnection.close();
    
    795 769
           } catch (e) {
    
    796
    -        logger.error("Exception while handling an event", reply, e);
    
    770
    +        logger.error("Failed to close the control port connection", e);
    
    797 771
           }
    
    798
    -    });
    
    772
    +      this.#controlConnection = null;
    
    773
    +    } else {
    
    774
    +      logger.trace(
    
    775
    +        "Requested to close an already closed control port connection"
    
    776
    +      );
    
    777
    +    }
    
    778
    +    this.#isBootstrapDone = false;
    
    779
    +    this.#lastWarning = {};
    
    799 780
       }
    
    800 781
     
    
    801
    -  _processLog(type, lines) {
    
    802
    -    if (type === "WARN" || type === "ERR") {
    
    803
    -      // Notify so that Copy Log can be enabled.
    
    804
    -      Services.obs.notifyObservers(null, TorProviderTopics.HasWarnOrErr);
    
    805
    -    }
    
    782
    +  // Authentication
    
    806 783
     
    
    807
    -    const date = new Date();
    
    808
    -    const maxEntries = Services.prefs.getIntPref(
    
    809
    -      "extensions.torlauncher.max_tor_log_entries",
    
    810
    -      1000
    
    811
    -    );
    
    812
    -    if (maxEntries > 0 && this._torLog.length >= maxEntries) {
    
    813
    -      this._torLog.splice(0, 1);
    
    814
    -    }
    
    784
    +  /**
    
    785
    +   * Read a cookie file to perform cookie-based authentication.
    
    786
    +   *
    
    787
    +   * @param {string} path The path to the cookie file
    
    788
    +   * @returns {Uint8Array} The content of the file in bytes
    
    789
    +   */
    
    790
    +  async #readAuthenticationCookie(path) {
    
    791
    +    return IOUtils.read(path);
    
    792
    +  }
    
    815 793
     
    
    816
    -    const msg = lines.join("\n");
    
    817
    -    this._torLog.push({ date, type, msg });
    
    818
    -    const logString = `Tor ${type}: ${msg}`;
    
    819
    -    logger.info(logString);
    
    794
    +  /**
    
    795
    +   * @returns {Uint8Array} A random 16-byte password.
    
    796
    +   */
    
    797
    +  #generateRandomPassword() {
    
    798
    +    const kPasswordLen = 16;
    
    799
    +    return crypto.getRandomValues(new Uint8Array(kPasswordLen));
    
    800
    +  }
    
    801
    +
    
    802
    +  // Notification handlers
    
    803
    +
    
    804
    +  /**
    
    805
    +   * Receive and process a notification with the bootstrap status.
    
    806
    +   *
    
    807
    +   * @param {object} status The status object
    
    808
    +   */
    
    809
    +  onBootstrapStatus(status) {
    
    810
    +    this.#processBootstrapStatus(status, true);
    
    820 811
       }
    
    821 812
     
    
    822
    -  // Process a bootstrap status to update the current state, and broadcast it
    
    823
    -  // to TorBootstrapStatus observers.
    
    824
    -  // If aSuppressErrors is true, errors are ignored. This is used when we
    
    825
    -  // are handling the response to a "GETINFO status/bootstrap-phase" command.
    
    826
    -  _processBootstrapStatus(statusObj, suppressErrors) {
    
    813
    +  /**
    
    814
    +   * Process a bootstrap status to update the current state, and broadcast it
    
    815
    +   * to TorBootstrapStatus observers.
    
    816
    +   *
    
    817
    +   * @param {object} statusObj The status object that the controller returned.
    
    818
    +   * Its entries depend on what Tor sent to us.
    
    819
    +   * @param {boolean} isNotification We broadcast warnings only when we receive
    
    820
    +   * them through an asynchronous notification.
    
    821
    +   */
    
    822
    +  #processBootstrapStatus(statusObj, isNotification) {
    
    827 823
         // Notify observers
    
    828 824
         Services.obs.notifyObservers(
    
    829 825
           { wrappedJSObject: statusObj },
    
    830
    -      "TorBootstrapStatus"
    
    826
    +      TorProviderTopics.BootstrapStatus
    
    831 827
         );
    
    832 828
     
    
    833 829
         if (statusObj.PROGRESS === 100) {
    
    834
    -      this._isBootstrapDone = true;
    
    830
    +      this.#isBootstrapDone = true;
    
    835 831
           try {
    
    836 832
             Services.prefs.setBoolPref(Preferences.PromptAtStartup, false);
    
    837 833
           } catch (e) {
    
    ... ... @@ -840,23 +836,29 @@ export class TorProvider {
    840 836
           return;
    
    841 837
         }
    
    842 838
     
    
    843
    -    this._isBootstrapDone = false;
    
    839
    +    this.#isBootstrapDone = false;
    
    844 840
     
    
    845 841
         if (
    
    842
    +      isNotification &&
    
    846 843
           statusObj.TYPE === "WARN" &&
    
    847
    -      statusObj.RECOMMENDATION !== "ignore" &&
    
    848
    -      !suppressErrors
    
    844
    +      statusObj.RECOMMENDATION !== "ignore"
    
    849 845
         ) {
    
    850
    -      this._notifyBootstrapError(statusObj);
    
    846
    +      this.#notifyBootstrapError(statusObj);
    
    851 847
         }
    
    852 848
       }
    
    853 849
     
    
    854
    -  _notifyBootstrapError(statusObj) {
    
    850
    +  /**
    
    851
    +   * Broadcast a bootstrap warning or error.
    
    852
    +   *
    
    853
    +   * @param {object} statusObj The bootstrap status object with the error
    
    854
    +   */
    
    855
    +  #notifyBootstrapError(statusObj) {
    
    855 856
         try {
    
    856 857
           Services.prefs.setBoolPref(Preferences.PromptAtStartup, true);
    
    857 858
         } catch (e) {
    
    858 859
           logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
    
    859 860
         }
    
    861
    +    // TODO: Move l10n to the above layers?
    
    860 862
         const phase = TorLauncherUtil.getLocalizedBootstrapStatus(statusObj, "TAG");
    
    861 863
         const reason = TorLauncherUtil.getLocalizedBootstrapStatus(
    
    862 864
           statusObj,
    
    ... ... @@ -872,11 +874,11 @@ export class TorProvider {
    872 874
         );
    
    873 875
     
    
    874 876
         if (
    
    875
    -      statusObj.TAG !== this._lastWarningPhase ||
    
    876
    -      statusObj.REASON !== this._lastWarningReason
    
    877
    +      statusObj.TAG !== this.#lastWarning.phase ||
    
    878
    +      statusObj.REASON !== this.#lastWarning.reason
    
    877 879
         ) {
    
    878
    -      this._lastWarningPhase = statusObj.TAG;
    
    879
    -      this._lastWarningReason = statusObj.REASON;
    
    880
    +      this.#lastWarning.phase = statusObj.TAG;
    
    881
    +      this.#lastWarning.reason = statusObj.REASON;
    
    880 882
     
    
    881 883
           const message = TorLauncherUtil.getLocalizedString(
    
    882 884
             "tor_bootstrap_failed"
    
    ... ... @@ -888,123 +890,122 @@ export class TorProvider {
    888 890
         }
    
    889 891
       }
    
    890 892
     
    
    891
    -  _processStatusClient(_type, lines) {
    
    892
    -    const statusObj = TorParsers.parseBootstrapStatus(lines[0]);
    
    893
    -    if (!statusObj) {
    
    894
    -      // No `BOOTSTRAP` in the line
    
    895
    -      return;
    
    893
    +  /**
    
    894
    +   * Handle a log message from the tor daemon. It will be added to the internal
    
    895
    +   * logs. If it is a warning or an error, a notification will be broadcast.
    
    896
    +   *
    
    897
    +   * @param {string} type The message type
    
    898
    +   * @param {string} msg The message
    
    899
    +   */
    
    900
    +  onLogMessage(type, msg) {
    
    901
    +    if (type === "WARN" || type === "ERR") {
    
    902
    +      // Notify so that Copy Log can be enabled.
    
    903
    +      Services.obs.notifyObservers(null, TorProviderTopics.HasWarnOrErr);
    
    896 904
         }
    
    897
    -    this._processBootstrapStatus(statusObj, false);
    
    898
    -  }
    
    899 905
     
    
    900
    -  async _processCircEvent(_type, lines) {
    
    901
    -    const builtEvent =
    
    902
    -      /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
    
    903
    -        lines[0]
    
    904
    -      );
    
    905
    -    const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]);
    
    906
    -    if (builtEvent) {
    
    907
    -      const fp = /\$([0-9a-fA-F]{40})/g;
    
    908
    -      const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
    
    909
    -        g[1].toUpperCase()
    
    910
    -      );
    
    911
    -      this._circuits.set(builtEvent.groups.CircuitID, nodes);
    
    912
    -      // Ignore circuits of length 1, that are used, for example, to probe
    
    913
    -      // bridges. So, only store them, since we might see streams that use them,
    
    914
    -      // but then early-return.
    
    915
    -      if (nodes.length === 1) {
    
    916
    -        return;
    
    917
    -      }
    
    918
    -      // In some cases, we might already receive SOCKS credentials in the line.
    
    919
    -      // However, this might be a problem with onion services: we get also a
    
    920
    -      // 4-hop circuit that we likely do not want to show to the user,
    
    921
    -      // especially because it is used only temporarily, and it would need a
    
    922
    -      // technical explaination.
    
    923
    -      // this._checkCredentials(lines[0], nodes);
    
    924
    -      if (this._currentBridge?.fingerprint !== nodes[0]) {
    
    925
    -        const nodeInfo = await this.getNodeInfo(nodes[0]);
    
    926
    -        let notify = false;
    
    927
    -        if (nodeInfo?.bridgeType) {
    
    928
    -          logger.info(`Bridge changed to ${nodes[0]}`);
    
    929
    -          this._currentBridge = nodeInfo;
    
    930
    -          notify = true;
    
    931
    -        } else if (this._currentBridge) {
    
    932
    -          logger.info("Bridges disabled");
    
    933
    -          this._currentBridge = null;
    
    934
    -          notify = true;
    
    935
    -        }
    
    936
    -        if (notify) {
    
    937
    -          Services.obs.notifyObservers(
    
    938
    -            null,
    
    939
    -            TorProviderTopics.BridgeChanged,
    
    940
    -            this._currentBridge
    
    941
    -          );
    
    942
    -        }
    
    943
    -      }
    
    944
    -    } else if (closedEvent) {
    
    945
    -      this._circuits.delete(closedEvent.groups.ID);
    
    906
    +    const date = new Date();
    
    907
    +    const maxEntries = Services.prefs.getIntPref(
    
    908
    +      Preferences.MaxLogEntries,
    
    909
    +      1000
    
    910
    +    );
    
    911
    +    if (maxEntries > 0 && this.#logs.length >= maxEntries) {
    
    912
    +      this.#logs.splice(0, 1);
    
    913
    +    }
    
    914
    +
    
    915
    +    this.#logs.push({ date, type, msg });
    
    916
    +    switch (type) {
    
    917
    +      case "ERR":
    
    918
    +        logger.error(`[Tor error] ${msg}`);
    
    919
    +        break;
    
    920
    +      case "WARN":
    
    921
    +        logger.warn(`[Tor warning] ${msg}`);
    
    922
    +        break;
    
    923
    +      default:
    
    924
    +        logger.info(`[Tor ${type.toLowerCase()}] ${msg}`);
    
    946 925
         }
    
    947 926
       }
    
    948 927
     
    
    949
    -  _processStreamEvent(_type, lines) {
    
    950
    -    // The first block is the stream ID, which we do not need at the moment.
    
    951
    -    const succeeedEvent =
    
    952
    -      /^[a-zA-Z0-9]{1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec(
    
    953
    -        lines[0]
    
    954
    -      );
    
    955
    -    if (!succeeedEvent) {
    
    928
    +  /**
    
    929
    +   * Handle a notification that a new circuit has been built.
    
    930
    +   * If a change of bridge is detected (including a change from bridge to a
    
    931
    +   * normal guard), a notification is broadcast.
    
    932
    +   *
    
    933
    +   * @param {CircuitID} id The circuit ID
    
    934
    +   * @param {NodeFingerprint[]} nodes The nodes that compose the circuit
    
    935
    +   */
    
    936
    +  async onCircuitBuilt(id, nodes) {
    
    937
    +    this.#circuits.set(id, nodes);
    
    938
    +    // Ignore circuits of length 1, that are used, for example, to probe
    
    939
    +    // bridges. So, only store them, since we might see streams that use them,
    
    940
    +    // but then early-return.
    
    941
    +    if (nodes.length === 1) {
    
    956 942
           return;
    
    957 943
         }
    
    958
    -    const circuit = this._circuits.get(succeeedEvent.groups.CircuitID);
    
    959
    -    if (!circuit) {
    
    960
    -      logger.error(
    
    961
    -        "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers.",
    
    962
    -        lines[0]
    
    963
    -      );
    
    964
    -      return;
    
    944
    +
    
    945
    +    if (this.#currentBridge?.fingerprint !== nodes[0]) {
    
    946
    +      const nodeInfo = await this.getNodeInfo(nodes[0]);
    
    947
    +      let notify = false;
    
    948
    +      if (nodeInfo?.bridgeType) {
    
    949
    +        logger.info(`Bridge changed to ${nodes[0]}`);
    
    950
    +        this.#currentBridge = nodeInfo;
    
    951
    +        notify = true;
    
    952
    +      } else if (this.#currentBridge) {
    
    953
    +        logger.info("Bridges disabled");
    
    954
    +        this.#currentBridge = null;
    
    955
    +        notify = true;
    
    956
    +      }
    
    957
    +      if (notify) {
    
    958
    +        Services.obs.notifyObservers(
    
    959
    +          null,
    
    960
    +          TorProviderTopics.BridgeChanged,
    
    961
    +          this.#currentBridge
    
    962
    +        );
    
    963
    +      }
    
    965 964
         }
    
    966
    -    this._checkCredentials(lines[0], circuit);
    
    967 965
       }
    
    968 966
     
    
    969 967
       /**
    
    970
    -   * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and
    
    971
    -   * SOCKS_PASSWORD. In case, notify observers that we could associate a certain
    
    972
    -   * circuit to these credentials.
    
    968
    +   * Handle a notification of a circuit being closed. We use it to clean the
    
    969
    +   * internal data.
    
    970
    +   *
    
    971
    +   * @param {CircuitID} id The circuit id
    
    972
    +   */
    
    973
    +  onCircuitClosed(id) {
    
    974
    +    logger.debug("Circuit closed event", id);
    
    975
    +    this.#circuits.delete(id);
    
    976
    +  }
    
    977
    +
    
    978
    +  /**
    
    979
    +   * Handle a notification about a stream switching to the succeeded state.
    
    973 980
        *
    
    974
    -   * @param {string} line The circ or stream line to check
    
    975
    -   * @param {NodeFingerprint[]} circuit The fingerprints of the nodes in the
    
    976
    -   * circuit.
    
    981
    +   * @param {StreamID} streamId The ID of the stream that switched to the
    
    982
    +   * succeeded state.
    
    983
    +   * @param {CircuitID} circuitId The ID of the circuit used by the stream
    
    984
    +   * @param {string} username The SOCKS username
    
    985
    +   * @param {string} password The SOCKS password
    
    986
    +   * @returns
    
    977 987
        */
    
    978
    -  _checkCredentials(line, circuit) {
    
    979
    -    const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line);
    
    980
    -    const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line);
    
    988
    +  onStreamSucceeded(streamId, circuitId, username, password) {
    
    981 989
         if (!username || !password) {
    
    982 990
           return;
    
    983 991
         }
    
    992
    +    logger.debug("Stream succeeded event", username, password, circuitId);
    
    993
    +    const circuit = this.#circuits.get(circuitId);
    
    994
    +    if (!circuit) {
    
    995
    +      logger.error(
    
    996
    +        "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers."
    
    997
    +      );
    
    998
    +      return;
    
    999
    +    }
    
    984 1000
         Services.obs.notifyObservers(
    
    985 1001
           {
    
    986 1002
             wrappedJSObject: {
    
    987
    -          username: TorParsers.unescapeString(username[1]),
    
    988
    -          password: TorParsers.unescapeString(password[1]),
    
    1003
    +          username,
    
    1004
    +          password,
    
    989 1005
               circuit,
    
    990 1006
             },
    
    991 1007
           },
    
    992 1008
           TorProviderTopics.StreamSucceeded
    
    993 1009
         );
    
    994 1010
       }
    
    995
    -
    
    996
    -  _shutDownEventMonitor() {
    
    997
    -    try {
    
    998
    -      this._connection?.close();
    
    999
    -    } catch (e) {
    
    1000
    -      logger.error("Could not close the connection to the control port", e);
    
    1001
    -    }
    
    1002
    -    this._connection = null;
    
    1003
    -    if (this._startTimeout !== null) {
    
    1004
    -      clearTimeout(this._startTimeout);
    
    1005
    -      this._startTimeout = null;
    
    1006
    -    }
    
    1007
    -    this._isBootstrapDone = false;
    
    1008
    -    this.clearBootstrapError();
    
    1009
    -  }
    
    1010 1011
     }

  • toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
    ... ... @@ -4,13 +4,13 @@
    4 4
     
    
    5 5
     const lazy = {};
    
    6 6
     ChromeUtils.defineESModuleGetters(lazy, {
    
    7
    +  TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
    
    7 8
       TorProvider: "resource://gre/modules/TorProvider.sys.mjs",
    
    8 9
     });
    
    9 10
     
    
    10 11
     export const TorProviderTopics = Object.freeze({
    
    11 12
       ProcessIsReady: "TorProcessIsReady",
    
    12 13
       ProcessExited: "TorProcessExited",
    
    13
    -  ProcessRestarted: "TorProcessRestarted",
    
    14 14
       BootstrapStatus: "TorBootstrapStatus",
    
    15 15
       BootstrapError: "TorBootstrapError",
    
    16 16
       HasWarnOrErr: "TorLogHasWarnOrErr",
    
    ... ... @@ -18,26 +18,141 @@ export const TorProviderTopics = Object.freeze({
    18 18
       StreamSucceeded: "TorStreamSucceeded",
    
    19 19
     });
    
    20 20
     
    
    21
    +/**
    
    22
    + * The factory to get a Tor provider.
    
    23
    + * Currently we support only TorProvider, i.e., the one that interacts with
    
    24
    + * C-tor through the control port protocol.
    
    25
    + */
    
    21 26
     export class TorProviderBuilder {
    
    27
    +  /**
    
    28
    +   * A promise with the instance of the provider that we are using.
    
    29
    +   *
    
    30
    +   * @type {Promise<TorProvider>?}
    
    31
    +   */
    
    22 32
       static #provider = null;
    
    23 33
     
    
    34
    +  /**
    
    35
    +   * The observer that checks when the tor process exits, and reinitializes the
    
    36
    +   * provider.
    
    37
    +   *
    
    38
    +   * @type {nsIObserver?}
    
    39
    +   */
    
    40
    +  static #observer = null;
    
    41
    +
    
    42
    +  /**
    
    43
    +   * Tell whether the browser UI is ready.
    
    44
    +   * We ignore any errors until it is because we cannot show them.
    
    45
    +   *
    
    46
    +   * @type {boolean}
    
    47
    +   */
    
    48
    +  static #uiReady = false;
    
    49
    +
    
    50
    +  /**
    
    51
    +   * Initialize the provider of choice.
    
    52
    +   * Even though initialization is asynchronous, we do not expect the caller to
    
    53
    +   * await this method. The reason is that any call to build() will wait the
    
    54
    +   * initialization anyway (and re-throw any initialization error).
    
    55
    +   */
    
    24 56
       static async init() {
    
    25
    -    const provider = new lazy.TorProvider();
    
    26
    -    await provider.init();
    
    27
    -    // Assign it only when initialization succeeds.
    
    28
    -    TorProviderBuilder.#provider = provider;
    
    57
    +    this.#observer = {
    
    58
    +      observe(subject, topic, data) {
    
    59
    +        if (topic !== TorProviderTopics.ProcessExited) {
    
    60
    +          return;
    
    61
    +        }
    
    62
    +        if (!TorProviderBuilder.#uiReady) {
    
    63
    +          console.warn(
    
    64
    +            `Seen ${TorProviderTopics.ProcessExited}, but not doing anything because the UI is not ready yet.`
    
    65
    +          );
    
    66
    +          return;
    
    67
    +        }
    
    68
    +        TorProviderBuilder.#torExited();
    
    69
    +      },
    
    70
    +    };
    
    71
    +    Services.obs.addObserver(this.#observer, TorProviderTopics.ProcessExited);
    
    72
    +    await this.#initProvider();
    
    73
    +  }
    
    74
    +
    
    75
    +  static async #initProvider() {
    
    76
    +    try {
    
    77
    +      const old = await this.#provider;
    
    78
    +      old?.uninit();
    
    79
    +    } catch {}
    
    80
    +    this.#provider = new Promise((resolve, reject) => {
    
    81
    +      const provider = new lazy.TorProvider();
    
    82
    +      provider
    
    83
    +        .init()
    
    84
    +        .then(() => resolve(provider))
    
    85
    +        .catch(reject);
    
    86
    +    });
    
    87
    +    await this.#provider;
    
    29 88
       }
    
    30 89
     
    
    31 90
       static uninit() {
    
    32
    -    TorProviderBuilder.#provider.uninit();
    
    33
    -    TorProviderBuilder.#provider = null;
    
    91
    +    this.#provider?.then(provider => {
    
    92
    +      provider.uninit();
    
    93
    +      this.#provider = null;
    
    94
    +    });
    
    95
    +    if (this.#observer) {
    
    96
    +      Services.obs.removeObserver(
    
    97
    +        this.#observer,
    
    98
    +        TorProviderTopics.ProcessExited
    
    99
    +      );
    
    100
    +      this.#observer = null;
    
    101
    +    }
    
    102
    +  }
    
    103
    +
    
    104
    +  /**
    
    105
    +   * Build a provider.
    
    106
    +   * This method will wait for the system to be initialized, and allows you to
    
    107
    +   * catch also any initialization errors.
    
    108
    +   */
    
    109
    +  static async build() {
    
    110
    +    if (!this.#provider) {
    
    111
    +      throw new Error(
    
    112
    +        "The provider has not been initialized or already uninitialized."
    
    113
    +      );
    
    114
    +    }
    
    115
    +    return this.#provider;
    
    116
    +  }
    
    117
    +
    
    118
    +  /**
    
    119
    +   * Check if the provider has been succesfully initialized when the first
    
    120
    +   * browser window is shown.
    
    121
    +   * This is a workaround we need because ideally we would like the tor process
    
    122
    +   * to start as soon as possible, to avoid delays in the about:torconnect page,
    
    123
    +   * but we should modify TorConnect and about:torconnect to handle this case
    
    124
    +   * there with a better UX.
    
    125
    +   */
    
    126
    +  static async firstWindowLoaded() {
    
    127
    +    // FIXME: Just integrate this with the about:torconnect or about:tor UI.
    
    128
    +    let running = false;
    
    129
    +    try {
    
    130
    +      const provider = await this.#provider;
    
    131
    +      // The initialization might have succeeded, but so far we have ignored any
    
    132
    +      // error notification. So, check that the process has not exited after the
    
    133
    +      // provider has been initialized successfully, but the UI was not ready
    
    134
    +      // yet.
    
    135
    +      running = provider.isRunning;
    
    136
    +    } catch {
    
    137
    +      // Not even initialized, running is already false.
    
    138
    +    }
    
    139
    +    while (!running && lazy.TorLauncherUtil.showRestartPrompt(true)) {
    
    140
    +      try {
    
    141
    +        await this.#initProvider();
    
    142
    +        running = true;
    
    143
    +      } catch {}
    
    144
    +    }
    
    145
    +    // The user might have canceled the restart, but at this point the UI is
    
    146
    +    // ready in any case.
    
    147
    +    this.#uiReady = true;
    
    34 148
       }
    
    35 149
     
    
    36
    -  // TODO: Switch to an async build?
    
    37
    -  static build() {
    
    38
    -    if (!TorProviderBuilder.#provider) {
    
    39
    -      throw new Error("TorProviderBuilder has not been initialized yet.");
    
    150
    +  static async #torExited() {
    
    151
    +    while (lazy.TorLauncherUtil.showRestartPrompt(false)) {
    
    152
    +      try {
    
    153
    +        await this.#initProvider();
    
    154
    +        break;
    
    155
    +      } catch {}
    
    40 156
         }
    
    41
    -    return TorProviderBuilder.#provider;
    
    42 157
       }
    
    43 158
     }

  • toolkit/components/tor-launcher/TorStartupService.sys.mjs
    ... ... @@ -34,9 +34,12 @@ export class TorStartupService {
    34 34
       async #init() {
    
    35 35
         Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
    
    36 36
     
    
    37
    -    await lazy.TorProviderBuilder.init();
    
    37
    +    // Do not await on this init. build() is expected to await the
    
    38
    +    // initialization, so anything that should need the Tor Provider should
    
    39
    +    // block there, instead.
    
    40
    +    lazy.TorProviderBuilder.init();
    
    38 41
     
    
    39
    -    lazy.TorSettings.init();
    
    42
    +    await lazy.TorSettings.init();
    
    40 43
         lazy.TorConnect.init();
    
    41 44
     
    
    42 45
         lazy.TorDomainIsolator.init();
    

  • toolkit/mozapps/update/UpdateService.sys.mjs
    ... ... @@ -388,13 +388,13 @@ XPCOMUtils.defineLazyGetter(
    388 388
       }
    
    389 389
     );
    
    390 390
     
    
    391
    -function _shouldRegisterBootstrapObserver(errorCode) {
    
    392
    -  const provider = lazy.TorProviderBuilder.build();
    
    393
    -  return (
    
    394
    -    errorCode == PROXY_SERVER_CONNECTION_REFUSED &&
    
    395
    -    !provider.isBootstrapDone &&
    
    396
    -    provider.ownsTorDaemon
    
    397
    -  );
    
    391
    +async function _shouldRegisterBootstrapObserver(errorCode) {
    
    392
    +  try {
    
    393
    +    const provider = await lazy.TorProviderBuilder.build();
    
    394
    +    return !provider.isBootstrapDone && provider.ownsTorDaemon;
    
    395
    +  } catch {
    
    396
    +    return false;
    
    397
    +  }
    
    398 398
     }
    
    399 399
     
    
    400 400
     /**
    
    ... ... @@ -3338,7 +3338,10 @@ UpdateService.prototype = {
    3338 3338
             AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE);
    
    3339 3339
           }
    
    3340 3340
           return;
    
    3341
    -    } else if (_shouldRegisterBootstrapObserver(update.errorCode)) {
    
    3341
    +    } else if (
    
    3342
    +      update.errorCode === PROXY_SERVER_CONNECTION_REFUSED &&
    
    3343
    +      (await _shouldRegisterBootstrapObserver())
    
    3344
    +    ) {
    
    3342 3345
           // Register boostrap observer to try again, but only when we own the
    
    3343 3346
           // tor process.
    
    3344 3347
           this._registerBootstrapObserver();
    
    ... ... @@ -6735,7 +6738,10 @@ Downloader.prototype = {
    6735 6738
           );
    
    6736 6739
           shouldRegisterOnlineObserver = true;
    
    6737 6740
           deleteActiveUpdate = false;
    
    6738
    -    } else if (_shouldRegisterBootstrapObserver(status)) {
    
    6741
    +    } else if (
    
    6742
    +      status === PROXY_SERVER_CONNECTION_REFUSED &&
    
    6743
    +      (await _shouldRegisterBootstrapObserver())
    
    6744
    +    ) {
    
    6739 6745
           // Register a bootstrap observer to try again.
    
    6740 6746
           // The bootstrap observer will continue the incremental download by
    
    6741 6747
           // calling downloadUpdate on the active update which continues
    

  • toolkit/torbutton/chrome/locale/en-US/torlauncher.properties
    ... ... @@ -9,7 +9,6 @@ torlauncher.tor_exited_during_startup=Tor exited during startup. This might be d
    9 9
     torlauncher.tor_exited=Tor unexpectedly exited. This might be due to a bug in Tor itself, another program on your system, or faulty hardware. Until you restart Tor, Tor Browser will not be able to reach any websites. If the problem persists, please send a copy of your Tor Log to the support team.
    
    10 10
     torlauncher.tor_exited2=Restarting Tor will not close your browser tabs.
    
    11 11
     torlauncher.restart_tor=Restart Tor
    
    12
    -torlauncher.tor_controlconn_failed=Could not connect to Tor control port.
    
    13 12
     torlauncher.tor_bootstrap_failed=Tor failed to establish a Tor network connection.
    
    14 13
     torlauncher.tor_bootstrap_failed_details=%1$S failed (%2$S).
    
    15 14
     
    
    ... ... @@ -60,3 +59,7 @@ torlauncher.bootstrapWarning.pt_missing=missing pluggable transport
    60 59
     torlauncher.nsresult.NS_ERROR_NET_RESET=The connection to the server was lost.
    
    61 60
     torlauncher.nsresult.NS_ERROR_CONNECTION_REFUSED=Could not connect to the server.
    
    62 61
     torlauncher.nsresult.NS_ERROR_PROXY_CONNECTION_REFUSED=Could not connect to the proxy.
    
    62
    +
    
    63
    +## 12.5-only strings that can be removed once it goes EOL.
    
    64
    +
    
    65
    +torlauncher.tor_controlconn_failed=Could not connect to Tor control port.