[tor-commits] [Git][tpo/applications/tor-browser][tor-browser-115.5.0esr-13.5-1] 4 commits: fixup! Bug 40597: Implement TorSettings module

Pier Angelo Vendrame (@pierov) git at gitlab.torproject.org
Wed Dec 13 14:47:16 UTC 2023



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


Commits:
4dd1eadc by Henry Wilkes at 2023-12-13T12:26:50+00:00
fixup! Bug 40597: Implement TorSettings module

Bug 42036: Refactor TorSettings.

We make sure we send out a notification every time a TorSettings setting
property changes.

We also place all string parsing in one place (the "transform" method).

We also use empty strings "" instead of null. The previous null values
would have been saved to preferences as an empty string and read back as
an empty string, so this keeps it consistent.

Enforce the "enum" types to be one of the existing values.

Use structuredClone for copying the _settings object.

Stop using console logging.

Stop exposing the proxy port setting as an array of one number. This
only worked before because a JavaScript array with one member converts
to the same string as the member itself.

Stop resetting the proxy settings when proxy.enabled is set to true
since all places that call this should set the other settings afterwards
anyway.

Stop setting firewall.allowed_ports to 0 rather than [].

Fix setting username and password for HTTP proxy.uri. Before it would
only do so if _proxyUsername was truthy, but this property was never
set.

- - - - -
2f71ea9b by Henry Wilkes at 2023-12-13T12:26:51+00:00
fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection

Bug 42036: Adjust for changes in TorSettings.

New API for SettingChanged notification.

Unset the username and password for SOCKS4 proxies explicitly in the
dialog settings.

- - - - -
e3a0346c by Henry Wilkes at 2023-12-13T12:26:52+00:00
fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser

Bug 42036: Adjust for changes in API for TorSettings SettingChanged
notification.

- - - - -
910bb906 by Henry Wilkes at 2023-12-13T12:26:52+00:00
fixup! Bug 40597: Implement TorSettings module

Bug 42036: Remove reference to "self" in TorSettings, and re-indent.

- - - - -


4 changed files:

- browser/components/torpreferences/content/connectionPane.js
- browser/components/torpreferences/content/connectionSettingsDialog.mjs
- toolkit/components/torconnect/TorConnectParent.sys.mjs
- toolkit/modules/TorSettings.sys.mjs


Changes:

=====================================
browser/components/torpreferences/content/connectionPane.js
=====================================
@@ -11,7 +11,7 @@ const { setTimeout, clearTimeout } = ChromeUtils.import(
   "resource://gre/modules/Timer.jsm"
 );
 
-const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource } =
+const { TorSettings, TorSettingsTopics, TorBridgeSource } =
   ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
 
 const { TorParsers } = ChromeUtils.importESModule(
@@ -285,7 +285,7 @@ const gConnectionPane = (function () {
         TorSettings.saveToPrefs().applySettings();
       });
       this._enableQuickstartCheckbox.checked = TorSettings.quickstart.enabled;
-      Services.obs.addObserver(this, TorSettingsTopics.SettingChanged);
+      Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
 
       // Bridge setup
       prefpane.querySelector(selectors.bridges.header).innerText =
@@ -885,7 +885,7 @@ const gConnectionPane = (function () {
 
     uninit() {
       // unregister our observer topics
-      Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged);
+      Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
       Services.obs.removeObserver(this, TorConnectTopics.StateChange);
       Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged);
       Services.obs.removeObserver(this, "intl:app-locales-changed");
@@ -903,13 +903,10 @@ const gConnectionPane = (function () {
     observe(subject, topic, data) {
       switch (topic) {
         // triggered when a TorSettings param has changed
-        case TorSettingsTopics.SettingChanged: {
-          const obj = subject?.wrappedJSObject;
-          switch (data) {
-            case TorSettingsData.QuickStartEnabled: {
-              this._enableQuickstartCheckbox.checked = obj.value;
-              break;
-            }
+        case TorSettingsTopics.SettingsChanged: {
+          if (subject.wrappedJSObject.changes.includes("quickstart.enabled")) {
+            this._enableQuickstartCheckbox.checked =
+              TorSettings.quickstart.enabled;
           }
           break;
         }


=====================================
browser/components/torpreferences/content/connectionSettingsDialog.mjs
=====================================
@@ -339,6 +339,8 @@ export class ConnectionSettingsDialog {
         TorSettings.proxy.type = type;
         TorSettings.proxy.address = address;
         TorSettings.proxy.port = port;
+        TorSettings.proxy.username = "";
+        TorSettings.proxy.password = "";
         break;
       case TorProxyType.Socks5:
         TorSettings.proxy.enabled = true;


=====================================
toolkit/components/torconnect/TorConnectParent.sys.mjs
=====================================
@@ -12,7 +12,6 @@ import {
 import {
   TorSettings,
   TorSettingsTopics,
-  TorSettingsData,
 } from "resource://gre/modules/TorSettings.sys.mjs";
 
 const BroadcastTopic = "about-torconnect:broadcast";
@@ -115,9 +114,11 @@ export class TorConnectParent extends JSWindowActorParent {
             }
             break;
           }
-          case TorSettingsTopics.SettingChanged: {
-            if (aData === TorSettingsData.QuickStartEnabled) {
-              self.state.QuickStartEnabled = obj.value;
+          case TorSettingsTopics.SettingsChanged: {
+            if (
+              aSubject.wrappedJSObject.changes.includes("quickstart.enabled")
+            ) {
+              self.state.QuickStartEnabled = TorSettings.quickstart.enabled;
             } else {
               // this isn't a setting torconnect cares about
               return;
@@ -141,7 +142,7 @@ export class TorConnectParent extends JSWindowActorParent {
     Services.obs.addObserver(this.torConnectObserver, TorSettingsTopics.Ready);
     Services.obs.addObserver(
       this.torConnectObserver,
-      TorSettingsTopics.SettingChanged
+      TorSettingsTopics.SettingsChanged
     );
 
     this.userActionObserver = {
@@ -168,7 +169,7 @@ export class TorConnectParent extends JSWindowActorParent {
     );
     Services.obs.removeObserver(
       this.torConnectObserver,
-      TorSettingsTopics.SettingChanged
+      TorSettingsTopics.SettingsChanged
     );
     Services.obs.removeObserver(this.userActionObserver, BroadcastTopic);
   }


=====================================
toolkit/modules/TorSettings.sys.mjs
=====================================
@@ -10,15 +10,21 @@ ChromeUtils.defineESModuleGetters(lazy, {
   TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
 });
 
+ChromeUtils.defineLazyGetter(lazy, "logger", () => {
+  let { ConsoleAPI } = ChromeUtils.importESModule(
+    "resource://gre/modules/Console.sys.mjs"
+  );
+  return new ConsoleAPI({
+    maxLogLevel: "warn",
+    maxLogLevelPref: "browser.torsettings.log_level",
+    prefix: "TorSettings",
+  });
+});
+
 /* TorSettings observer topics */
 export const TorSettingsTopics = Object.freeze({
   Ready: "torsettings:ready",
-  SettingChanged: "torsettings:setting-changed",
-});
-
-/* TorSettings observer data (for SettingChanged topic) */
-export const TorSettingsData = Object.freeze({
-  QuickStartEnabled: "torsettings:quickstart_enabled",
+  SettingsChanged: "torsettings:settings-changed",
 });
 
 /* Prefs used to store settings in TorBrowser prefs */
@@ -32,7 +38,7 @@ const TorSettingsPrefs = Object.freeze({
   bridges: {
     /* bool:  does tor use bridges */
     enabled: "torbrowser.settings.bridges.enabled",
-    /* int: -1=invalid|0=builtin|1=bridge_db|2=user_provided */
+    /* int: See TorBridgeSource */
     source: "torbrowser.settings.bridges.source",
     /* string: obfs4|meek_azure|snowflake|etc */
     builtin_type: "torbrowser.settings.bridges.builtin_type",
@@ -42,7 +48,7 @@ const TorSettingsPrefs = Object.freeze({
   proxy: {
     /* bool: does tor use a proxy */
     enabled: "torbrowser.settings.proxy.enabled",
-    /* -1=invalid|0=socks4,1=socks5,2=https */
+    /* See TorProxyType */
     type: "torbrowser.settings.proxy.type",
     /* string: proxy server address */
     address: "torbrowser.settings.proxy.address",
@@ -140,23 +146,6 @@ export const TorBuiltinBridgeTypes = Object.freeze(
 
 /* Parsing Methods */
 
-// expects a string representation of an integer from 1 to 65535
-const parsePort = function (aPort) {
-  // ensure port string is a valid positive integer
-  const validIntRegex = /^[0-9]+$/;
-  if (!validIntRegex.test(aPort)) {
-    return 0;
-  }
-
-  // ensure port value is on valid range
-  const port = Number.parseInt(aPort);
-  if (port < 1 || port > 65535) {
-    return 0;
-  }
-
-  return port;
-};
-
 // expects a '\n' or '\r\n' delimited bridge string, which we split and trim
 // each bridge string can also optionally have 'bridge' at the beginning ie:
 // bridge $(type) $(address):$(port) $(certificate)
@@ -176,17 +165,6 @@ const parseBridgeStrings = function (aBridgeStrings) {
     .filter(bridgeString => bridgeString != "");
 };
 
-// expecting a ',' delimited list of ints with possible white space between
-// returns an array of ints
-const parsePortList = function (aPortListString) {
-  const splitStrings = aPortListString.split(",");
-  // parse and remove duplicates
-  const portSet = new Set(splitStrings.map(val => parsePort(val.trim())));
-  // parsePort returns 0 for failed parses, so remove 0 from list
-  portSet.delete(0);
-  return Array.from(portSet);
-};
-
 const getBuiltinBridgeStrings = function (builtinType) {
   if (!builtinType) {
     return [];
@@ -235,558 +213,679 @@ const arrayShuffle = function (array) {
   }
 };
 
-const arrayCopy = function (array) {
-  return [].concat(array);
-};
-
 /* TorSettings module */
 
-export const TorSettings = (() => {
-  const self = {
-    _settings: null,
+export const TorSettings = {
+  /**
+   * The underlying settings values.
+   *
+   * @type {object}
+   */
+  _settings: {
+    quickstart: {
+      enabled: false,
+    },
+    bridges: {
+      enabled: false,
+      source: TorBridgeSource.Invalid,
+      builtin_type: "",
+      bridge_strings: [],
+    },
+    proxy: {
+      enabled: false,
+      type: TorProxyType.Invalid,
+      address: "",
+      port: 0,
+      username: "",
+      password: "",
+    },
+    firewall: {
+      enabled: false,
+      allowed_ports: [],
+    },
+  },
+
+  /**
+   * The current number of freezes applied to the notifications.
+   *
+   * @type {integer}
+   */
+  _freezeNotificationsCount: 0,
+  /**
+   * The queue for settings that have changed. To be broadcast in the
+   * notification when not frozen.
+   *
+   * @type {Set<string>}
+   */
+  _notificationQueue: new Set(),
+  /**
+   * Send a notification if we have any queued and we are not frozen.
+   */
+  _tryNotification() {
+    if (this._freezeNotificationsCount || !this._notificationQueue.size) {
+      return;
+    }
+    Services.obs.notifyObservers(
+      { changes: [...this._notificationQueue] },
+      TorSettingsTopics.SettingsChanged
+    );
+    this._notificationQueue.clear();
+  },
+  /**
+   * Pause notifications for changes in setting values. This is useful if you
+   * need to make batch changes to settings.
+   *
+   * This should always be paired with a call to thawNotifications once
+   * notifications should be released. Usually you should wrap whatever
+   * changes you make with a `try` block and call thawNotifications in the
+   * `finally` block.
+   */
+  freezeNotifications() {
+    this._freezeNotificationsCount++;
+  },
+  /**
+   * Release the hold on notifications so they may be sent out.
+   *
+   * Note, if some other method has also frozen the notifications, this will
+   * only release them once it has also called this method.
+   */
+  thawNotifications() {
+    this._freezeNotificationsCount--;
+    this._tryNotification();
+  },
+  /**
+   * @typedef {object} TorSettingProperty
+   *
+   * @property {function} [getter] - A getter for the property. If this is
+   *   given, the property cannot be set.
+   * @property {function} [transform] - Called in the setter for the property,
+   *   with the new value given. Should transform the given value into the
+   *   right type.
+   * @property {function} [equal] - Test whether two values for the property
+   *   are considered equal. Otherwise uses `===`.
+   * @property {function} [callback] - Called whenever the property value
+   *   changes, with the new value given. Should be used to trigger any other
+   *   required changes for the new value.
+   * @property {function} [copy] - Called whenever the property is read, with
+   *   the stored value given. Should return a copy of the value. Otherwise
+   *   returns the stored value.
+   */
+  /**
+   * Add properties to the TorSettings instance, to be read or set.
+   *
+   * @param {string} groupname - The name of the setting group. The given
+   *   settings will be accessible from the TorSettings property of the same
+   *   name.
+   * @param {object<string, TorSettingProperty>} propParams - An object that
+   *   defines the settings to add to this group. The object property names
+   *   will be mapped to properties of TorSettings under the given groupname
+   *   property. Details about the setting should be described in the
+   *   TorSettingProperty property value.
+   */
+  _addProperties(groupname, propParams) {
+    // Create a new object to hold all these settings.
+    const group = {};
+    for (const name in propParams) {
+      const { getter, transform, callback, copy, equal } = propParams[name];
+      Object.defineProperty(group, name, {
+        get: getter
+          ? getter
+          : () => {
+              let val = this._settings[groupname][name];
+              if (copy) {
+                val = copy(val);
+              }
+              // Assume string or number value.
+              return val;
+            },
+        set: getter
+          ? undefined
+          : val => {
+              const prevVal = this._settings[groupname][name];
+              this.freezeNotifications();
+              try {
+                if (transform) {
+                  val = transform(val);
+                }
+                const isEqual = equal ? equal(val, prevVal) : val === prevVal;
+                if (!isEqual) {
+                  if (callback) {
+                    callback(val);
+                  }
+                  this._settings[groupname][name] = val;
+                  this._notificationQueue.add(`${groupname}.${name}`);
+                }
+              } finally {
+                this.thawNotifications();
+              }
+            },
+      });
+    }
+    // The group object itself should not be writable.
+    Object.preventExtensions(group);
+    Object.defineProperty(this, groupname, {
+      writable: false,
+      value: group,
+    });
+  },
 
-    // tor daemon related settings
-    defaultSettings() {
-      const settings = {
-        quickstart: {
-          enabled: false,
+  /**
+   * Regular expression for a decimal non-negative integer.
+   *
+   * @type {RegExp}
+   */
+  _portRegex: /^[0-9]+$/,
+  /**
+   * Parse a string as a port number.
+   *
+   * @param {string|integer} val - The value to parse.
+   * @param {boolean} trim - Whether a string value can be stripped of
+   *   whitespace before parsing.
+   *
+   * @return {integer?} - The port number, or null if the given value was not
+   *   valid.
+   */
+  _parsePort(val, trim) {
+    if (typeof val === "string") {
+      if (trim) {
+        val = val.trim();
+      }
+      // ensure port string is a valid positive integer
+      if (this._portRegex.test(val)) {
+        val = Number.parseInt(val, 10);
+      } else {
+        lazy.logger.error(`Invalid port string "${val}"`);
+        return null;
+      }
+    }
+    if (!Number.isInteger(val) || val < 1 || val > 65535) {
+      lazy.logger.error(`Port out of range: ${val}`);
+      return null;
+    }
+    return val;
+  },
+  /**
+   * Test whether two arrays have equal members and order.
+   *
+   * @param {Array} val1 - The first array to test.
+   * @param {Array} val2 - The second array to compare against.
+   *
+   * @return {boolean} - Whether the two arrays are equal.
+   */
+  _arrayEqual(val1, val2) {
+    if (val1.length !== val2.length) {
+      return false;
+    }
+    return val1.every((v, i) => v === val2[i]);
+  },
+
+  /* load or init our settings, and register observers */
+  async init() {
+    this._addProperties("quickstart", {
+      enabled: {},
+    });
+    this._addProperties("bridges", {
+      enabled: {},
+      source: {
+        transform: val => {
+          if (Object.values(TorBridgeSource).includes(val)) {
+            return val;
+          }
+          lazy.logger.error(`Not a valid bridge source: "${val}"`);
+          return TorBridgeSource.Invalid;
+        },
+      },
+      bridge_strings: {
+        transform: val => {
+          if (Array.isArray(val)) {
+            return [...val];
+          }
+          return parseBridgeStrings(val);
+        },
+        copy: val => [...val],
+        equal: (val1, val2) => this._arrayEqual(val1, val2),
+      },
+      builtin_type: {
+        callback: val => {
+          if (!val) {
+            // Make sure that the source is not BuiltIn
+            if (this.bridges.source === TorBridgeSource.BuiltIn) {
+              this.bridges.source = TorBridgeSource.Invalid;
+            }
+            return;
+          }
+          const bridgeStrings = getBuiltinBridgeStrings(val);
+          if (bridgeStrings.length) {
+            this.bridges.bridge_strings = bridgeStrings;
+            return;
+          }
+          lazy.logger.error(`No built-in ${val} bridges found`);
+          // Change to be empty, this will trigger this callback again,
+          // but with val as "".
+          this.bridges.builtin_type == "";
         },
-        bridges: {
-          enabled: false,
-          source: TorBridgeSource.Invalid,
-          builtin_type: null,
-          bridge_strings: [],
+      },
+    });
+    this._addProperties("proxy", {
+      enabled: {
+        callback: val => {
+          if (val) {
+            return;
+          }
+          // Reset proxy settings.
+          this.proxy.type = TorProxyType.Invalid;
+          this.proxy.address = "";
+          this.proxy.port = 0;
+          this.proxy.username = "";
+          this.proxy.password = "";
         },
-        proxy: {
-          enabled: false,
-          type: TorProxyType.Invalid,
-          address: null,
-          port: 0,
-          username: null,
-          password: null,
+      },
+      type: {
+        transform: val => {
+          if (Object.values(TorProxyType).includes(val)) {
+            return val;
+          }
+          lazy.logger.error(`Not a valid proxy type: "${val}"`);
+          return TorProxyType.Invalid;
         },
-        firewall: {
-          enabled: false,
-          allowed_ports: [],
+      },
+      address: {},
+      port: {
+        transform: val => {
+          if (val === 0) {
+            // This is a valid value that "unsets" the port.
+            // Keep this value without giving a warning.
+            // NOTE: In contrast, "0" is not valid.
+            return 0;
+          }
+          // Unset to 0 if invalid null is returned.
+          return this._parsePort(val, false) ?? 0;
         },
-      };
-      return settings;
-    },
-
-    /* load or init our settings, and register observers */
-    async init() {
-      // TODO: We could use a shared promise, and wait for it to be fullfilled
-      // instead of Service.obs.
-      if (lazy.TorLauncherUtil.shouldStartAndOwnTor) {
-        // if the settings branch exists, load settings from prefs
-        if (Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)) {
+      },
+      username: {},
+      password: {},
+      uri: {
+        getter: () => {
+          const { type, address, port, username, password } = this.proxy;
+          switch (type) {
+            case TorProxyType.Socks4:
+              return `socks4a://${address}:${port}`;
+            case TorProxyType.Socks5:
+              if (username) {
+                return `socks5://${username}:${password}@${address}:${port}`;
+              }
+              return `socks5://${address}:${port}`;
+            case TorProxyType.HTTPS:
+              if (username) {
+                return `http://${username}:${password}@${address}:${port}`;
+              }
+              return `http://${address}:${port}`;
+          }
+          return null;
+        },
+      },
+    });
+    this._addProperties("firewall", {
+      enabled: {
+        callback: val => {
+          if (!val) {
+            this.firewall.allowed_ports = "";
+          }
+        },
+      },
+      allowed_ports: {
+        transform: val => {
+          if (!Array.isArray(val)) {
+            val = val === "" ? [] : val.split(",");
+          }
+          // parse and remove duplicates
+          const portSet = new Set(val.map(p => this._parsePort(p, true)));
+          // parsePort returns null for failed parses, so remove it.
+          portSet.delete(null);
+          return [...portSet];
+        },
+        copy: val => [...val],
+        equal: (val1, val2) => this._arrayEqual(val1, val2),
+      },
+    });
+
+    // TODO: We could use a shared promise, and wait for it to be fullfilled
+    // instead of Service.obs.
+    if (lazy.TorLauncherUtil.shouldStartAndOwnTor) {
+      // if the settings branch exists, load settings from prefs
+      if (Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)) {
+        // Do not want notifications for initially loaded prefs.
+        this.freezeNotifications();
+        try {
           this.loadFromPrefs();
-        } else {
-          // otherwise load defaults
-          this._settings = this.defaultSettings();
+        } finally {
+          this._notificationQueue.clear();
+          this.thawNotifications();
         }
-        Services.obs.addObserver(this, lazy.TorProviderTopics.ProcessIsReady);
-
-        try {
-          const provider = await lazy.TorProviderBuilder.build();
-          if (provider.isRunning) {
-            this.handleProcessReady();
-          }
-        } catch {}
       }
-    },
+      try {
+        const provider = await lazy.TorProviderBuilder.build();
+        if (provider.isRunning) {
+          this.handleProcessReady();
+          // No need to add an observer to call this again.
+          return;
+        }
+      } catch {}
 
-    /* wait for relevant life-cycle events to apply saved settings */
-    async observe(subject, topic, data) {
-      console.log(`TorSettings: Observed ${topic}`);
+      Services.obs.addObserver(this, lazy.TorProviderTopics.ProcessIsReady);
+    }
+  },
 
-      switch (topic) {
-        case lazy.TorProviderTopics.ProcessIsReady:
-          Services.obs.removeObserver(
-            this,
-            lazy.TorProviderTopics.ProcessIsReady
-          );
-          await this.handleProcessReady();
-          break;
-      }
-    },
+  /* wait for relevant life-cycle events to apply saved settings */
+  async observe(subject, topic, data) {
+    lazy.logger.debug(`Observed ${topic}`);
 
-    // once the tor daemon is ready, we need to apply our settings
-    async handleProcessReady() {
-      // push down settings to tor
-      await this.applySettings();
-      console.log("TorSettings: Ready");
-      Services.obs.notifyObservers(null, TorSettingsTopics.Ready);
-    },
+    switch (topic) {
+      case lazy.TorProviderTopics.ProcessIsReady:
+        Services.obs.removeObserver(
+          this,
+          lazy.TorProviderTopics.ProcessIsReady
+        );
+        await this.handleProcessReady();
+        break;
+    }
+  },
 
-    // load our settings from prefs
-    loadFromPrefs() {
-      console.log("TorSettings: loadFromPrefs()");
+  // once the tor daemon is ready, we need to apply our settings
+  async handleProcessReady() {
+    // push down settings to tor
+    await this.applySettings();
+    lazy.logger.info("Ready");
+    Services.obs.notifyObservers(null, TorSettingsTopics.Ready);
+  },
 
-      const settings = this.defaultSettings();
+  // load our settings from prefs
+  loadFromPrefs() {
+    lazy.logger.debug("loadFromPrefs()");
 
-      /* Quickstart */
-      settings.quickstart.enabled = Services.prefs.getBoolPref(
-        TorSettingsPrefs.quickstart.enabled,
-        false
+    /* Quickstart */
+    this.quickstart.enabled = Services.prefs.getBoolPref(
+      TorSettingsPrefs.quickstart.enabled,
+      false
+    );
+    /* Bridges */
+    this.bridges.enabled = Services.prefs.getBoolPref(
+      TorSettingsPrefs.bridges.enabled,
+      false
+    );
+    this.bridges.source = Services.prefs.getIntPref(
+      TorSettingsPrefs.bridges.source,
+      TorBridgeSource.Invalid
+    );
+    if (this.bridges.source == TorBridgeSource.BuiltIn) {
+      this.bridges.builtin_type = Services.prefs.getStringPref(
+        TorSettingsPrefs.bridges.builtin_type,
+        ""
       );
-      /* Bridges */
-      settings.bridges.enabled = Services.prefs.getBoolPref(
-        TorSettingsPrefs.bridges.enabled,
-        false
+    } else {
+      const bridgeBranchPrefs = Services.prefs
+        .getBranch(TorSettingsPrefs.bridges.bridge_strings)
+        .getChildList("");
+      this.bridges.bridge_strings = Array.from(bridgeBranchPrefs, pref =>
+        Services.prefs.getStringPref(
+          `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
+        )
       );
-      settings.bridges.source = Services.prefs.getIntPref(
-        TorSettingsPrefs.bridges.source,
-        TorBridgeSource.Invalid
+    }
+    /* Proxy */
+    this.proxy.enabled = Services.prefs.getBoolPref(
+      TorSettingsPrefs.proxy.enabled,
+      false
+    );
+    if (this.proxy.enabled) {
+      this.proxy.type = Services.prefs.getIntPref(
+        TorSettingsPrefs.proxy.type,
+        TorProxyType.Invalid
       );
-      if (settings.bridges.source == TorBridgeSource.BuiltIn) {
-        const builtinType = Services.prefs.getStringPref(
-          TorSettingsPrefs.bridges.builtin_type,
-          ""
-        );
-        settings.bridges.builtin_type = builtinType;
-        settings.bridges.bridge_strings = getBuiltinBridgeStrings(builtinType);
-        if (!settings.bridges.bridge_strings.length) {
-          // in this case the user is using a builtin bridge that is no longer supported,
-          // reset to settings to default values
-          console.warn(
-            `[TorSettings] Cannot find any bridge line for the configured bridge type ${builtinType}`
-          );
-          settings.bridges.source = TorBridgeSource.Invalid;
-          settings.bridges.builtin_type = null;
-        }
-      } else {
-        settings.bridges.bridge_strings = [];
-        const bridgeBranchPrefs = Services.prefs
-          .getBranch(TorSettingsPrefs.bridges.bridge_strings)
-          .getChildList("");
-        bridgeBranchPrefs.forEach(pref => {
-          const bridgeString = Services.prefs.getStringPref(
-            `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
-          );
-          settings.bridges.bridge_strings.push(bridgeString);
-        });
-      }
-      /* Proxy */
-      settings.proxy.enabled = Services.prefs.getBoolPref(
-        TorSettingsPrefs.proxy.enabled,
-        false
+      this.proxy.address = Services.prefs.getStringPref(
+        TorSettingsPrefs.proxy.address,
+        ""
       );
-      if (settings.proxy.enabled) {
-        settings.proxy.type = Services.prefs.getIntPref(
-          TorSettingsPrefs.proxy.type,
-          TorProxyType.Invalid
-        );
-        settings.proxy.address = Services.prefs.getStringPref(
-          TorSettingsPrefs.proxy.address,
-          ""
-        );
-        settings.proxy.port = Services.prefs.getIntPref(
-          TorSettingsPrefs.proxy.port,
-          0
-        );
-        settings.proxy.username = Services.prefs.getStringPref(
-          TorSettingsPrefs.proxy.username,
-          ""
-        );
-        settings.proxy.password = Services.prefs.getStringPref(
-          TorSettingsPrefs.proxy.password,
-          ""
-        );
-      } else {
-        settings.proxy.type = TorProxyType.Invalid;
-        settings.proxy.address = null;
-        settings.proxy.port = 0;
-        settings.proxy.username = null;
-        settings.proxy.password = null;
-      }
-
-      /* Firewall */
-      settings.firewall.enabled = Services.prefs.getBoolPref(
-        TorSettingsPrefs.firewall.enabled,
-        false
+      this.proxy.port = Services.prefs.getIntPref(
+        TorSettingsPrefs.proxy.port,
+        0
       );
-      if (settings.firewall.enabled) {
-        const portList = Services.prefs.getStringPref(
-          TorSettingsPrefs.firewall.allowed_ports,
-          ""
-        );
-        settings.firewall.allowed_ports = parsePortList(portList);
-      } else {
-        settings.firewall.allowed_ports = 0;
-      }
-
-      this._settings = settings;
-
-      return this;
-    },
+      this.proxy.username = Services.prefs.getStringPref(
+        TorSettingsPrefs.proxy.username,
+        ""
+      );
+      this.proxy.password = Services.prefs.getStringPref(
+        TorSettingsPrefs.proxy.password,
+        ""
+      );
+    }
 
-    // save our settings to prefs
-    saveToPrefs() {
-      console.log("TorSettings: saveToPrefs()");
+    /* Firewall */
+    this.firewall.enabled = Services.prefs.getBoolPref(
+      TorSettingsPrefs.firewall.enabled,
+      false
+    );
+    if (this.firewall.enabled) {
+      this.firewall.allowed_ports = Services.prefs.getStringPref(
+        TorSettingsPrefs.firewall.allowed_ports,
+        ""
+      );
+    }
+  },
 
-      const settings = this._settings;
+  // save our settings to prefs
+  saveToPrefs() {
+    lazy.logger.debug("saveToPrefs()");
 
-      /* Quickstart */
-      Services.prefs.setBoolPref(
-        TorSettingsPrefs.quickstart.enabled,
-        settings.quickstart.enabled
-      );
-      /* Bridges */
-      Services.prefs.setBoolPref(
-        TorSettingsPrefs.bridges.enabled,
-        settings.bridges.enabled
+    /* Quickstart */
+    Services.prefs.setBoolPref(
+      TorSettingsPrefs.quickstart.enabled,
+      this.quickstart.enabled
+    );
+    /* Bridges */
+    Services.prefs.setBoolPref(
+      TorSettingsPrefs.bridges.enabled,
+      this.bridges.enabled
+    );
+    Services.prefs.setIntPref(
+      TorSettingsPrefs.bridges.source,
+      this.bridges.source
+    );
+    Services.prefs.setStringPref(
+      TorSettingsPrefs.bridges.builtin_type,
+      this.bridges.builtin_type
+    );
+    // erase existing bridge strings
+    const bridgeBranchPrefs = Services.prefs
+      .getBranch(TorSettingsPrefs.bridges.bridge_strings)
+      .getChildList("");
+    bridgeBranchPrefs.forEach(pref => {
+      Services.prefs.clearUserPref(
+        `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
       );
-      Services.prefs.setIntPref(
-        TorSettingsPrefs.bridges.source,
-        settings.bridges.source
+    });
+    // write new ones
+    if (this.bridges.source !== TorBridgeSource.BuiltIn) {
+      this.bridges.bridge_strings.forEach((string, index) => {
+        Services.prefs.setStringPref(
+          `${TorSettingsPrefs.bridges.bridge_strings}.${index}`,
+          string
+        );
+      });
+    }
+    /* Proxy */
+    Services.prefs.setBoolPref(
+      TorSettingsPrefs.proxy.enabled,
+      this.proxy.enabled
+    );
+    if (this.proxy.enabled) {
+      Services.prefs.setIntPref(TorSettingsPrefs.proxy.type, this.proxy.type);
+      Services.prefs.setStringPref(
+        TorSettingsPrefs.proxy.address,
+        this.proxy.address
       );
+      Services.prefs.setIntPref(TorSettingsPrefs.proxy.port, this.proxy.port);
       Services.prefs.setStringPref(
-        TorSettingsPrefs.bridges.builtin_type,
-        settings.bridges.builtin_type
+        TorSettingsPrefs.proxy.username,
+        this.proxy.username
       );
-      // erase existing bridge strings
-      const bridgeBranchPrefs = Services.prefs
-        .getBranch(TorSettingsPrefs.bridges.bridge_strings)
-        .getChildList("");
-      bridgeBranchPrefs.forEach(pref => {
-        Services.prefs.clearUserPref(
-          `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
-        );
-      });
-      // write new ones
-      if (settings.bridges.source !== TorBridgeSource.BuiltIn) {
-        settings.bridges.bridge_strings.forEach((string, index) => {
-          Services.prefs.setStringPref(
-            `${TorSettingsPrefs.bridges.bridge_strings}.${index}`,
-            string
-          );
-        });
-      }
-      /* Proxy */
-      Services.prefs.setBoolPref(
-        TorSettingsPrefs.proxy.enabled,
-        settings.proxy.enabled
+      Services.prefs.setStringPref(
+        TorSettingsPrefs.proxy.password,
+        this.proxy.password
       );
-      if (settings.proxy.enabled) {
-        Services.prefs.setIntPref(
-          TorSettingsPrefs.proxy.type,
-          settings.proxy.type
-        );
-        Services.prefs.setStringPref(
-          TorSettingsPrefs.proxy.address,
-          settings.proxy.address
-        );
-        Services.prefs.setIntPref(
-          TorSettingsPrefs.proxy.port,
-          settings.proxy.port
-        );
-        Services.prefs.setStringPref(
-          TorSettingsPrefs.proxy.username,
-          settings.proxy.username
-        );
-        Services.prefs.setStringPref(
-          TorSettingsPrefs.proxy.password,
-          settings.proxy.password
-        );
-      } else {
-        Services.prefs.clearUserPref(TorSettingsPrefs.proxy.type);
-        Services.prefs.clearUserPref(TorSettingsPrefs.proxy.address);
-        Services.prefs.clearUserPref(TorSettingsPrefs.proxy.port);
-        Services.prefs.clearUserPref(TorSettingsPrefs.proxy.username);
-        Services.prefs.clearUserPref(TorSettingsPrefs.proxy.password);
-      }
-      /* Firewall */
-      Services.prefs.setBoolPref(
-        TorSettingsPrefs.firewall.enabled,
-        settings.firewall.enabled
+    } else {
+      Services.prefs.clearUserPref(TorSettingsPrefs.proxy.type);
+      Services.prefs.clearUserPref(TorSettingsPrefs.proxy.address);
+      Services.prefs.clearUserPref(TorSettingsPrefs.proxy.port);
+      Services.prefs.clearUserPref(TorSettingsPrefs.proxy.username);
+      Services.prefs.clearUserPref(TorSettingsPrefs.proxy.password);
+    }
+    /* Firewall */
+    Services.prefs.setBoolPref(
+      TorSettingsPrefs.firewall.enabled,
+      this.firewall.enabled
+    );
+    if (this.firewall.enabled) {
+      Services.prefs.setStringPref(
+        TorSettingsPrefs.firewall.allowed_ports,
+        this.firewall.allowed_ports.join(",")
       );
-      if (settings.firewall.enabled) {
-        Services.prefs.setStringPref(
-          TorSettingsPrefs.firewall.allowed_ports,
-          settings.firewall.allowed_ports.join(",")
-        );
-      } else {
-        Services.prefs.clearUserPref(TorSettingsPrefs.firewall.allowed_ports);
-      }
+    } else {
+      Services.prefs.clearUserPref(TorSettingsPrefs.firewall.allowed_ports);
+    }
 
-      // all tor settings now stored in prefs :)
-      Services.prefs.setBoolPref(TorSettingsPrefs.enabled, true);
+    // all tor settings now stored in prefs :)
+    Services.prefs.setBoolPref(TorSettingsPrefs.enabled, true);
 
-      return this;
-    },
-
-    // push our settings down to the tor daemon
-    async applySettings() {
-      console.log("TorSettings: applySettings()");
-      const settings = this._settings;
-      const settingsMap = new Map();
-
-      /* Bridges */
-      const haveBridges =
-        settings.bridges.enabled && !!settings.bridges.bridge_strings.length;
-      settingsMap.set(TorConfigKeys.useBridges, haveBridges);
-      if (haveBridges) {
-        settingsMap.set(
-          TorConfigKeys.bridgeList,
-          settings.bridges.bridge_strings
-        );
-      } else {
-        settingsMap.set(TorConfigKeys.bridgeList, null);
-      }
+    return this;
+  },
 
-      /* Proxy */
-      settingsMap.set(TorConfigKeys.socks4Proxy, null);
-      settingsMap.set(TorConfigKeys.socks5Proxy, null);
-      settingsMap.set(TorConfigKeys.socks5ProxyUsername, null);
-      settingsMap.set(TorConfigKeys.socks5ProxyPassword, null);
-      settingsMap.set(TorConfigKeys.httpsProxy, null);
-      settingsMap.set(TorConfigKeys.httpsProxyAuthenticator, null);
-      if (settings.proxy.enabled) {
-        const address = settings.proxy.address;
-        const port = settings.proxy.port;
-        const username = settings.proxy.username;
-        const password = settings.proxy.password;
-
-        switch (settings.proxy.type) {
-          case TorProxyType.Socks4:
-            settingsMap.set(TorConfigKeys.socks4Proxy, `${address}:${port}`);
-            break;
-          case TorProxyType.Socks5:
-            settingsMap.set(TorConfigKeys.socks5Proxy, `${address}:${port}`);
-            settingsMap.set(TorConfigKeys.socks5ProxyUsername, username);
-            settingsMap.set(TorConfigKeys.socks5ProxyPassword, password);
-            break;
-          case TorProxyType.HTTPS:
-            settingsMap.set(TorConfigKeys.httpsProxy, `${address}:${port}`);
-            settingsMap.set(
-              TorConfigKeys.httpsProxyAuthenticator,
-              `${username}:${password}`
-            );
-            break;
-        }
-      }
+  // push our settings down to the tor daemon
+  async applySettings() {
+    lazy.logger.debug("applySettings()");
+    const settingsMap = new Map();
+
+    /* Bridges */
+    const haveBridges =
+      this.bridges.enabled && !!this.bridges.bridge_strings.length;
+    settingsMap.set(TorConfigKeys.useBridges, haveBridges);
+    if (haveBridges) {
+      settingsMap.set(TorConfigKeys.bridgeList, this.bridges.bridge_strings);
+    } else {
+      settingsMap.set(TorConfigKeys.bridgeList, null);
+    }
 
-      /* Firewall */
-      if (settings.firewall.enabled) {
-        const reachableAddresses = settings.firewall.allowed_ports
-          .map(port => `*:${port}`)
-          .join(",");
-        settingsMap.set(TorConfigKeys.reachableAddresses, reachableAddresses);
-      } else {
-        settingsMap.set(TorConfigKeys.reachableAddresses, null);
+    /* Proxy */
+    settingsMap.set(TorConfigKeys.socks4Proxy, null);
+    settingsMap.set(TorConfigKeys.socks5Proxy, null);
+    settingsMap.set(TorConfigKeys.socks5ProxyUsername, null);
+    settingsMap.set(TorConfigKeys.socks5ProxyPassword, null);
+    settingsMap.set(TorConfigKeys.httpsProxy, null);
+    settingsMap.set(TorConfigKeys.httpsProxyAuthenticator, null);
+    if (this.proxy.enabled) {
+      const address = this.proxy.address;
+      const port = this.proxy.port;
+      const username = this.proxy.username;
+      const password = this.proxy.password;
+
+      switch (this.proxy.type) {
+        case TorProxyType.Socks4:
+          settingsMap.set(TorConfigKeys.socks4Proxy, `${address}:${port}`);
+          break;
+        case TorProxyType.Socks5:
+          settingsMap.set(TorConfigKeys.socks5Proxy, `${address}:${port}`);
+          settingsMap.set(TorConfigKeys.socks5ProxyUsername, username);
+          settingsMap.set(TorConfigKeys.socks5ProxyPassword, password);
+          break;
+        case TorProxyType.HTTPS:
+          settingsMap.set(TorConfigKeys.httpsProxy, `${address}:${port}`);
+          settingsMap.set(
+            TorConfigKeys.httpsProxyAuthenticator,
+            `${username}:${password}`
+          );
+          break;
       }
+    }
 
-      /* Push to Tor */
-      const provider = await lazy.TorProviderBuilder.build();
-      await provider.writeSettings(settingsMap);
+    /* Firewall */
+    if (this.firewall.enabled) {
+      const reachableAddresses = this.firewall.allowed_ports
+        .map(port => `*:${port}`)
+        .join(",");
+      settingsMap.set(TorConfigKeys.reachableAddresses, reachableAddresses);
+    } else {
+      settingsMap.set(TorConfigKeys.reachableAddresses, null);
+    }
 
-      return this;
-    },
+    /* Push to Tor */
+    const provider = await lazy.TorProviderBuilder.build();
+    await provider.writeSettings(settingsMap);
 
-    // set all of our settings at once from a settings object
-    setSettings(settings) {
-      console.log("TorSettings: setSettings()");
-      const backup = this.getSettings();
+    return this;
+  },
 
-      try {
-        this._settings.bridges.enabled = !!settings.bridges.enabled;
-        this._settings.bridges.source = settings.bridges.source;
-        switch (settings.bridges.source) {
-          case TorBridgeSource.BridgeDB:
-          case TorBridgeSource.UserProvided:
-            this._settings.bridges.bridge_strings =
-              settings.bridges.bridge_strings;
-            break;
-          case TorBridgeSource.BuiltIn: {
-            this._settings.bridges.builtin_type = settings.bridges.builtin_type;
-            settings.bridges.bridge_strings = getBuiltinBridgeStrings(
-              settings.bridges.builtin_type
+  // set all of our settings at once from a settings object
+  setSettings(settings) {
+    lazy.logger.debug("setSettings()");
+    const backup = this.getSettings();
+    const backup_notifications = [...this._notificationQueue];
+
+    // Hold off on lots of notifications until all settings are changed.
+    this.freezeNotifications();
+    try {
+      this.bridges.enabled = !!settings.bridges.enabled;
+      this.bridges.source = settings.bridges.source;
+      switch (settings.bridges.source) {
+        case TorBridgeSource.BridgeDB:
+        case TorBridgeSource.UserProvided:
+          this.bridges.bridge_strings = settings.bridges.bridge_strings;
+          break;
+        case TorBridgeSource.BuiltIn: {
+          this.bridges.builtin_type = settings.bridges.builtin_type;
+          if (!this.bridges.bridge_strings.length) {
+            // No bridges were found when setting the builtin_type.
+            throw new Error(
+              `No available builtin bridges of type ${settings.bridges.builtin_type}`
             );
-            if (
-              !settings.bridges.bridge_strings.length &&
-              settings.bridges.enabled
-            ) {
-              throw new Error(
-                `No available builtin bridges of type ${settings.bridges.builtin_type}`
-              );
-            }
-            this._settings.bridges.bridge_strings =
-              settings.bridges.bridge_strings;
-            break;
           }
-          case TorBridgeSource.Invalid:
-            break;
-          default:
-            if (settings.bridges.enabled) {
-              throw new Error(
-                `Bridge source '${settings.source}' is not a valid source`
-              );
-            }
-            break;
+          break;
         }
-
-        // TODO: proxy and firewall
-      } catch (ex) {
-        this._settings = backup;
-        console.log(`TorSettings: setSettings failed => ${ex.message}`);
-      }
-
-      console.log("TorSettings: setSettings result");
-      console.log(this._settings);
-    },
-
-    // get a copy of all our settings
-    getSettings() {
-      console.log("TorSettings: getSettings()");
-      // TODO: replace with structuredClone someday (post esr94): https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
-      return JSON.parse(JSON.stringify(this._settings));
-    },
-
-    /* Getters and Setters */
-
-    // Quickstart
-    get quickstart() {
-      return {
-        get enabled() {
-          return self._settings.quickstart.enabled;
-        },
-        set enabled(val) {
-          if (val != self._settings.quickstart.enabled) {
-            self._settings.quickstart.enabled = val;
-            Services.obs.notifyObservers(
-              { value: val },
-              TorSettingsTopics.SettingChanged,
-              TorSettingsData.QuickStartEnabled
+        case TorBridgeSource.Invalid:
+          break;
+        default:
+          if (settings.bridges.enabled) {
+            throw new Error(
+              `Bridge source '${settings.source}' is not a valid source`
             );
           }
-        },
-      };
-    },
+          break;
+      }
 
-    // Bridges
-    get bridges() {
-      return {
-        get enabled() {
-          return self._settings.bridges.enabled;
-        },
-        set enabled(val) {
-          self._settings.bridges.enabled = val;
-        },
-        get source() {
-          return self._settings.bridges.source;
-        },
-        set source(val) {
-          self._settings.bridges.source = val;
-        },
-        get builtin_type() {
-          return self._settings.bridges.builtin_type;
-        },
-        set builtin_type(val) {
-          const bridgeStrings = getBuiltinBridgeStrings(val);
-          if (bridgeStrings.length) {
-            self._settings.bridges.builtin_type = val;
-            self._settings.bridges.bridge_strings = bridgeStrings;
-          } else {
-            self._settings.bridges.builtin_type = "";
-            if (self._settings.bridges.source === TorBridgeSource.BuiltIn) {
-              self._settings.bridges.source = TorBridgeSource.Invalid;
-            }
-          }
-        },
-        get bridge_strings() {
-          return arrayCopy(self._settings.bridges.bridge_strings);
-        },
-        set bridge_strings(val) {
-          self._settings.bridges.bridge_strings = parseBridgeStrings(val);
-        },
-      };
-    },
+      // TODO: proxy and firewall
+    } catch (ex) {
+      // Restore the old settings without any new notifications generated from
+      // the above code.
+      // NOTE: Since this code is not async, it should not be possible for
+      // some other call to TorSettings to change anything whilst we are
+      // in this context (other than lower down in this call stack), so it is
+      // safe to discard all changes to settings and notifications.
+      this._settings = backup;
+      this._notificationQueue.clear();
+      for (const notification of backup_notifications) {
+        this._notificationQueue.add(notification);
+      }
 
-    // Proxy
-    get proxy() {
-      return {
-        get enabled() {
-          return self._settings.proxy.enabled;
-        },
-        set enabled(val) {
-          self._settings.proxy.enabled = val;
-          // reset proxy settings
-          self._settings.proxy.type = TorProxyType.Invalid;
-          self._settings.proxy.address = null;
-          self._settings.proxy.port = 0;
-          self._settings.proxy.username = null;
-          self._settings.proxy.password = null;
-        },
-        get type() {
-          return self._settings.proxy.type;
-        },
-        set type(val) {
-          self._settings.proxy.type = val;
-        },
-        get address() {
-          return self._settings.proxy.address;
-        },
-        set address(val) {
-          self._settings.proxy.address = val;
-        },
-        get port() {
-          return arrayCopy(self._settings.proxy.port);
-        },
-        set port(val) {
-          self._settings.proxy.port = parsePort(val);
-        },
-        get username() {
-          return self._settings.proxy.username;
-        },
-        set username(val) {
-          self._settings.proxy.username = val;
-        },
-        get password() {
-          return self._settings.proxy.password;
-        },
-        set password(val) {
-          self._settings.proxy.password = val;
-        },
-        get uri() {
-          switch (this.type) {
-            case TorProxyType.Socks4:
-              return `socks4a://${this.address}:${this.port}`;
-            case TorProxyType.Socks5:
-              if (this.username) {
-                return `socks5://${this.username}:${this.password}@${this.address}:${this.port}`;
-              }
-              return `socks5://${this.address}:${this.port}`;
-            case TorProxyType.HTTPS:
-              if (this._proxyUsername) {
-                return `http://${this.username}:${this.password}@${this.address}:${this.port}`;
-              }
-              return `http://${this.address}:${this.port}`;
-          }
-          return null;
-        },
-      };
-    },
+      lazy.logger.error("setSettings failed", ex);
+    } finally {
+      this.thawNotifications();
+    }
 
-    // Firewall
-    get firewall() {
-      return {
-        get enabled() {
-          return self._settings.firewall.enabled;
-        },
-        set enabled(val) {
-          self._settings.firewall.enabled = val;
-          // reset firewall settings
-          self._settings.firewall.allowed_ports = [];
-        },
-        get allowed_ports() {
-          return self._settings.firewall.allowed_ports;
-        },
-        set allowed_ports(val) {
-          self._settings.firewall.allowed_ports = parsePortList(val);
-        },
-      };
-    },
-  };
-  return self;
-})();
+    lazy.logger.debug("setSettings result", this._settings);
+  },
+
+  // get a copy of all our settings
+  getSettings() {
+    lazy.logger.debug("getSettings()");
+    return structuredClone(this._settings);
+  },
+};



View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/0c6e4916f9e64bf90df7e072dc4cf4534dbede7f...910bb90614def1be17fe71afa78df680ff03d898

-- 
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/0c6e4916f9e64bf90df7e072dc4cf4534dbede7f...910bb90614def1be17fe71afa78df680ff03d898
You're receiving this email because of your account on gitlab.torproject.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.torproject.org/pipermail/tor-commits/attachments/20231213/0a18f548/attachment-0001.htm>


More information about the tor-commits mailing list