| ... | ... | @@ -67,16 +67,6 @@ const TorSettingsPrefs = Object.freeze({ | 
| 67 | 67 |    },
 | 
| 68 | 68 |  });
 | 
| 69 | 69 |  
 | 
| 70 |  | -/* Legacy tor-launcher prefs and pref branches*/
 | 
| 71 |  | -const TorLauncherPrefs = Object.freeze({
 | 
| 72 |  | -  quickstart: "extensions.torlauncher.quickstart",
 | 
| 73 |  | -  default_bridge_type: "extensions.torlauncher.default_bridge_type",
 | 
| 74 |  | -  default_bridge: "extensions.torlauncher.default_bridge.",
 | 
| 75 |  | -  default_bridge_recommended_type:
 | 
| 76 |  | -    "extensions.torlauncher.default_bridge_recommended_type",
 | 
| 77 |  | -  bridgedb_bridge: "extensions.torlauncher.bridgedb_bridge.",
 | 
| 78 |  | -});
 | 
| 79 |  | -
 | 
| 80 | 70 |  /* Config Keys used to configure tor daemon */
 | 
| 81 | 71 |  const TorConfigKeys = Object.freeze({
 | 
| 82 | 72 |    useBridges: "UseBridges",
 | 
| ... | ... | @@ -105,101 +95,41 @@ export const TorProxyType = Object.freeze({ | 
| 105 | 95 |    HTTPS: 2,
 | 
| 106 | 96 |  });
 | 
| 107 | 97 |  
 | 
| 108 |  | -export const TorBuiltinBridgeTypes = Object.freeze(
 | 
| 109 |  | -  (() => {
 | 
| 110 |  | -    const bridgeListBranch = Services.prefs.getBranch(
 | 
| 111 |  | -      TorLauncherPrefs.default_bridge
 | 
| 112 |  | -    );
 | 
| 113 |  | -    const bridgePrefs = bridgeListBranch.getChildList("");
 | 
| 114 |  | -
 | 
| 115 |  | -    // an unordered set for shoving bridge types into
 | 
| 116 |  | -    const bridgeTypes = new Set();
 | 
| 117 |  | -    // look for keys ending in ".N" and treat string before that as the bridge type
 | 
| 118 |  | -    const pattern = /\.[0-9]+$/;
 | 
| 119 |  | -    for (const key of bridgePrefs) {
 | 
| 120 |  | -      const offset = key.search(pattern);
 | 
| 121 |  | -      if (offset != -1) {
 | 
| 122 |  | -        const bt = key.substring(0, offset);
 | 
| 123 |  | -        bridgeTypes.add(bt);
 | 
| 124 |  | -      }
 | 
| 125 |  | -    }
 | 
| 126 |  | -
 | 
| 127 |  | -    // recommended bridge type goes first in the list
 | 
| 128 |  | -    const recommendedBridgeType = Services.prefs.getCharPref(
 | 
| 129 |  | -      TorLauncherPrefs.default_bridge_recommended_type,
 | 
| 130 |  | -      null
 | 
| 131 |  | -    );
 | 
| 132 |  | -
 | 
| 133 |  | -    const retval = [];
 | 
| 134 |  | -    if (recommendedBridgeType && bridgeTypes.has(recommendedBridgeType)) {
 | 
| 135 |  | -      retval.push(recommendedBridgeType);
 | 
| 136 |  | -    }
 | 
| 137 |  | -
 | 
| 138 |  | -    for (const bridgeType of bridgeTypes.values()) {
 | 
| 139 |  | -      if (bridgeType != recommendedBridgeType) {
 | 
| 140 |  | -        retval.push(bridgeType);
 | 
| 141 |  | -      }
 | 
| 142 |  | -    }
 | 
| 143 |  | -    return retval;
 | 
| 144 |  | -  })()
 | 
| 145 |  | -);
 | 
| 146 |  | -
 | 
| 147 |  | -/* Parsing Methods */
 | 
| 148 |  | -
 | 
| 149 |  | -// expects a '\n' or '\r\n' delimited bridge string, which we split and trim
 | 
| 150 |  | -// each bridge string can also optionally have 'bridge' at the beginning ie:
 | 
| 151 |  | -// bridge $(type) $(address):$(port) $(certificate)
 | 
| 152 |  | -// we strip out the 'bridge' prefix here
 | 
| 153 |  | -const parseBridgeStrings = function (aBridgeStrings) {
 | 
|  | 98 | +/**
 | 
|  | 99 | + * Split a blob of bridge lines into an array with single lines.
 | 
|  | 100 | + * Lines are delimited by \r\n or \n and each bridge string can also optionally
 | 
|  | 101 | + * have 'bridge' at the beginning.
 | 
|  | 102 | + * We split the text by \r\n, we trim the lines, remove the bridge prefix and
 | 
|  | 103 | + * filter out any remaiing empty item.
 | 
|  | 104 | + *
 | 
|  | 105 | + * @param {string} aBridgeStrings The text with the lines
 | 
|  | 106 | + * @returns {string[]} An array where each bridge line is an item
 | 
|  | 107 | + */
 | 
|  | 108 | +function parseBridgeStrings(aBridgeStrings) {
 | 
| 154 | 109 |    // replace carriage returns ('\r') with new lines ('\n')
 | 
| 155 | 110 |    aBridgeStrings = aBridgeStrings.replace(/\r/g, "\n");
 | 
| 156 | 111 |    // then replace contiguous new lines ('\n') with a single one
 | 
| 157 | 112 |    aBridgeStrings = aBridgeStrings.replace(/[\n]+/g, "\n");
 | 
| 158 | 113 |  
 | 
| 159 |  | -  // split on the newline and for each bridge string: trim, remove starting 'bridge' string
 | 
| 160 |  | -  // finally discard entries that are empty strings; empty strings could occur if we receive
 | 
| 161 |  | -  // a new line containing only whitespace
 | 
|  | 114 | +  // Split on the newline and for each bridge string: trim, remove starting
 | 
|  | 115 | +  // 'bridge' string.
 | 
|  | 116 | +  // Finally, discard entries that are empty strings; empty strings could occur
 | 
|  | 117 | +  // if we receive a new line containing only whitespace.
 | 
| 162 | 118 |    const splitStrings = aBridgeStrings.split("\n");
 | 
| 163 | 119 |    return splitStrings
 | 
| 164 | 120 |      .map(val => val.trim().replace(/^bridge\s+/i, ""))
 | 
| 165 |  | -    .filter(bridgeString => bridgeString != "");
 | 
| 166 |  | -};
 | 
| 167 |  | -
 | 
| 168 |  | -const getBuiltinBridgeStrings = function (builtinType) {
 | 
| 169 |  | -  if (!builtinType) {
 | 
| 170 |  | -    return [];
 | 
| 171 |  | -  }
 | 
| 172 |  | -
 | 
| 173 |  | -  const bridgeBranch = Services.prefs.getBranch(
 | 
| 174 |  | -    TorLauncherPrefs.default_bridge
 | 
| 175 |  | -  );
 | 
| 176 |  | -  const bridgeBranchPrefs = bridgeBranch.getChildList("");
 | 
| 177 |  | -  const retval = [];
 | 
| 178 |  | -
 | 
| 179 |  | -  // regex matches against strings ending in ".N" where N is a positive integer
 | 
| 180 |  | -  const pattern = /\.[0-9]+$/;
 | 
| 181 |  | -  for (const key of bridgeBranchPrefs) {
 | 
| 182 |  | -    // verify the location of the match is the correct offset required for aBridgeType
 | 
| 183 |  | -    // to fit, and that the string begins with aBridgeType
 | 
| 184 |  | -    if (
 | 
| 185 |  | -      key.search(pattern) == builtinType.length &&
 | 
| 186 |  | -      key.startsWith(builtinType)
 | 
| 187 |  | -    ) {
 | 
| 188 |  | -      const bridgeStr = bridgeBranch.getCharPref(key);
 | 
| 189 |  | -      retval.push(bridgeStr);
 | 
| 190 |  | -    }
 | 
| 191 |  | -  }
 | 
| 192 |  | -
 | 
| 193 |  | -  // shuffle so that Tor Browser users don't all try the built-in bridges in the same order
 | 
| 194 |  | -  arrayShuffle(retval);
 | 
| 195 |  | -
 | 
| 196 |  | -  return retval;
 | 
| 197 |  | -};
 | 
| 198 |  | -
 | 
| 199 |  | -/* Helper methods */
 | 
| 200 |  | -
 | 
| 201 |  | -const arrayShuffle = function (array) {
 | 
| 202 |  | -  // fisher-yates shuffle
 | 
|  | 121 | +    .filter(bridgeString => bridgeString !== "");
 | 
|  | 122 | +}
 | 
|  | 123 | +
 | 
|  | 124 | +/**
 | 
|  | 125 | + * Return a shuffled (Fisher-Yates) copy of an array.
 | 
|  | 126 | + *
 | 
|  | 127 | + * @template T
 | 
|  | 128 | + * @param {T[]} array
 | 
|  | 129 | + * @returns {T[]}
 | 
|  | 130 | + */
 | 
|  | 131 | +function arrayShuffle(array) {
 | 
|  | 132 | +  array = [...array];
 | 
| 203 | 133 |    for (let i = array.length - 1; i > 0; --i) {
 | 
| 204 | 134 |      // number n such that 0.0 <= n < 1.0
 | 
| 205 | 135 |      const n = Math.random();
 | 
| ... | ... | @@ -211,17 +141,18 @@ const arrayShuffle = function (array) { | 
| 211 | 141 |      array[i] = array[j];
 | 
| 212 | 142 |      array[j] = tmp;
 | 
| 213 | 143 |    }
 | 
| 214 |  | -};
 | 
|  | 144 | +  return array;
 | 
|  | 145 | +}
 | 
| 215 | 146 |  
 | 
| 216 | 147 |  /* TorSettings module */
 | 
| 217 | 148 |  
 | 
| 218 |  | -export const TorSettings = {
 | 
|  | 149 | +class TorSettingsImpl {
 | 
| 219 | 150 |    /**
 | 
| 220 | 151 |     * The underlying settings values.
 | 
| 221 | 152 |     *
 | 
| 222 | 153 |     * @type {object}
 | 
| 223 | 154 |     */
 | 
| 224 |  | -  _settings: {
 | 
|  | 155 | +  #settings = {
 | 
| 225 | 156 |      quickstart: {
 | 
| 226 | 157 |        enabled: false,
 | 
| 227 | 158 |      },
 | 
| ... | ... | @@ -243,34 +174,207 @@ export const TorSettings = { | 
| 243 | 174 |        enabled: false,
 | 
| 244 | 175 |        allowed_ports: [],
 | 
| 245 | 176 |      },
 | 
| 246 |  | -  },
 | 
|  | 177 | +  };
 | 
|  | 178 | +
 | 
|  | 179 | +  /**
 | 
|  | 180 | +   * The recommended pluggable transport.
 | 
|  | 181 | +   *
 | 
|  | 182 | +   * @type {string}
 | 
|  | 183 | +   */
 | 
|  | 184 | +  #recommendedPT = "";
 | 
|  | 185 | +
 | 
|  | 186 | +  /**
 | 
|  | 187 | +   * The bridge lines for built-in bridges.
 | 
|  | 188 | +   * Keys are pluggable transports, and values are arrays of bridge lines.
 | 
|  | 189 | +   *
 | 
|  | 190 | +   * @type {Object.<string, string[]>}
 | 
|  | 191 | +   */
 | 
|  | 192 | +  #builtinBridges = {};
 | 
|  | 193 | +
 | 
|  | 194 | +  /**
 | 
|  | 195 | +   * Resolve callback of the initializedPromise.
 | 
|  | 196 | +   */
 | 
|  | 197 | +  #initComplete;
 | 
|  | 198 | +  /**
 | 
|  | 199 | +   * Reject callback of the initializedPromise.
 | 
|  | 200 | +   */
 | 
|  | 201 | +  #initFailed;
 | 
|  | 202 | +  /**
 | 
|  | 203 | +   * Tell whether the initializedPromise has been resolved.
 | 
|  | 204 | +   * We keep this additional member to avoid making everything async.
 | 
|  | 205 | +   *
 | 
|  | 206 | +   * @type {boolean}
 | 
|  | 207 | +   */
 | 
|  | 208 | +  #initialized = false;
 | 
|  | 209 | +  /**
 | 
|  | 210 | +   * During some phases of the initialization, allow calling setters and
 | 
|  | 211 | +   * getters without throwing errors.
 | 
|  | 212 | +   *
 | 
|  | 213 | +   * @type {boolean}
 | 
|  | 214 | +   */
 | 
|  | 215 | +  #allowUninitialized = false;
 | 
|  | 216 | +
 | 
|  | 217 | +  constructor() {
 | 
|  | 218 | +    this.initializedPromise = new Promise((resolve, reject) => {
 | 
|  | 219 | +      this.#initComplete = resolve;
 | 
|  | 220 | +      this.#initFailed = reject;
 | 
|  | 221 | +    });
 | 
|  | 222 | +
 | 
|  | 223 | +    this.#addProperties("quickstart", {
 | 
|  | 224 | +      enabled: {},
 | 
|  | 225 | +    });
 | 
|  | 226 | +    this.#addProperties("bridges", {
 | 
|  | 227 | +      enabled: {},
 | 
|  | 228 | +      source: {
 | 
|  | 229 | +        transform: val => {
 | 
|  | 230 | +          if (Object.values(TorBridgeSource).includes(val)) {
 | 
|  | 231 | +            return val;
 | 
|  | 232 | +          }
 | 
|  | 233 | +          lazy.logger.error(`Not a valid bridge source: "${val}"`);
 | 
|  | 234 | +          return TorBridgeSource.Invalid;
 | 
|  | 235 | +        },
 | 
|  | 236 | +      },
 | 
|  | 237 | +      bridge_strings: {
 | 
|  | 238 | +        transform: val => {
 | 
|  | 239 | +          if (Array.isArray(val)) {
 | 
|  | 240 | +            return [...val];
 | 
|  | 241 | +          }
 | 
|  | 242 | +          return parseBridgeStrings(val);
 | 
|  | 243 | +        },
 | 
|  | 244 | +        copy: val => [...val],
 | 
|  | 245 | +        equal: (val1, val2) => this.#arrayEqual(val1, val2),
 | 
|  | 246 | +      },
 | 
|  | 247 | +      builtin_type: {
 | 
|  | 248 | +        callback: val => {
 | 
|  | 249 | +          if (!val) {
 | 
|  | 250 | +            // Make sure that the source is not BuiltIn
 | 
|  | 251 | +            if (this.bridges.source === TorBridgeSource.BuiltIn) {
 | 
|  | 252 | +              this.bridges.source = TorBridgeSource.Invalid;
 | 
|  | 253 | +            }
 | 
|  | 254 | +            return;
 | 
|  | 255 | +          }
 | 
|  | 256 | +          const bridgeStrings = this.#getBuiltinBridges(val);
 | 
|  | 257 | +          if (bridgeStrings.length) {
 | 
|  | 258 | +            this.bridges.bridge_strings = bridgeStrings;
 | 
|  | 259 | +            return;
 | 
|  | 260 | +          }
 | 
|  | 261 | +          lazy.logger.error(`No built-in ${val} bridges found`);
 | 
|  | 262 | +          // Change to be empty, this will trigger this callback again,
 | 
|  | 263 | +          // but with val as "".
 | 
|  | 264 | +          this.bridges.builtin_type == "";
 | 
|  | 265 | +        },
 | 
|  | 266 | +      },
 | 
|  | 267 | +    });
 | 
|  | 268 | +    this.#addProperties("proxy", {
 | 
|  | 269 | +      enabled: {
 | 
|  | 270 | +        callback: val => {
 | 
|  | 271 | +          if (val) {
 | 
|  | 272 | +            return;
 | 
|  | 273 | +          }
 | 
|  | 274 | +          // Reset proxy settings.
 | 
|  | 275 | +          this.proxy.type = TorProxyType.Invalid;
 | 
|  | 276 | +          this.proxy.address = "";
 | 
|  | 277 | +          this.proxy.port = 0;
 | 
|  | 278 | +          this.proxy.username = "";
 | 
|  | 279 | +          this.proxy.password = "";
 | 
|  | 280 | +        },
 | 
|  | 281 | +      },
 | 
|  | 282 | +      type: {
 | 
|  | 283 | +        transform: val => {
 | 
|  | 284 | +          if (Object.values(TorProxyType).includes(val)) {
 | 
|  | 285 | +            return val;
 | 
|  | 286 | +          }
 | 
|  | 287 | +          lazy.logger.error(`Not a valid proxy type: "${val}"`);
 | 
|  | 288 | +          return TorProxyType.Invalid;
 | 
|  | 289 | +        },
 | 
|  | 290 | +      },
 | 
|  | 291 | +      address: {},
 | 
|  | 292 | +      port: {
 | 
|  | 293 | +        transform: val => {
 | 
|  | 294 | +          if (val === 0) {
 | 
|  | 295 | +            // This is a valid value that "unsets" the port.
 | 
|  | 296 | +            // Keep this value without giving a warning.
 | 
|  | 297 | +            // NOTE: In contrast, "0" is not valid.
 | 
|  | 298 | +            return 0;
 | 
|  | 299 | +          }
 | 
|  | 300 | +          // Unset to 0 if invalid null is returned.
 | 
|  | 301 | +          return this.#parsePort(val, false) ?? 0;
 | 
|  | 302 | +        },
 | 
|  | 303 | +      },
 | 
|  | 304 | +      username: {},
 | 
|  | 305 | +      password: {},
 | 
|  | 306 | +      uri: {
 | 
|  | 307 | +        getter: () => {
 | 
|  | 308 | +          const { type, address, port, username, password } = this.proxy;
 | 
|  | 309 | +          switch (type) {
 | 
|  | 310 | +            case TorProxyType.Socks4:
 | 
|  | 311 | +              return `socks4a://${address}:${port}`;
 | 
|  | 312 | +            case TorProxyType.Socks5:
 | 
|  | 313 | +              if (username) {
 | 
|  | 314 | +                return `socks5://${username}:${password}@${address}:${port}`;
 | 
|  | 315 | +              }
 | 
|  | 316 | +              return `socks5://${address}:${port}`;
 | 
|  | 317 | +            case TorProxyType.HTTPS:
 | 
|  | 318 | +              if (username) {
 | 
|  | 319 | +                return `http://${username}:${password}@${address}:${port}`;
 | 
|  | 320 | +              }
 | 
|  | 321 | +              return `http://${address}:${port}`;
 | 
|  | 322 | +          }
 | 
|  | 323 | +          return null;
 | 
|  | 324 | +        },
 | 
|  | 325 | +      },
 | 
|  | 326 | +    });
 | 
|  | 327 | +    this.#addProperties("firewall", {
 | 
|  | 328 | +      enabled: {
 | 
|  | 329 | +        callback: val => {
 | 
|  | 330 | +          if (!val) {
 | 
|  | 331 | +            this.firewall.allowed_ports = "";
 | 
|  | 332 | +          }
 | 
|  | 333 | +        },
 | 
|  | 334 | +      },
 | 
|  | 335 | +      allowed_ports: {
 | 
|  | 336 | +        transform: val => {
 | 
|  | 337 | +          if (!Array.isArray(val)) {
 | 
|  | 338 | +            val = val === "" ? [] : val.split(",");
 | 
|  | 339 | +          }
 | 
|  | 340 | +          // parse and remove duplicates
 | 
|  | 341 | +          const portSet = new Set(val.map(p => this.#parsePort(p, true)));
 | 
|  | 342 | +          // parsePort returns null for failed parses, so remove it.
 | 
|  | 343 | +          portSet.delete(null);
 | 
|  | 344 | +          return [...portSet];
 | 
|  | 345 | +        },
 | 
|  | 346 | +        copy: val => [...val],
 | 
|  | 347 | +        equal: (val1, val2) => this.#arrayEqual(val1, val2),
 | 
|  | 348 | +      },
 | 
|  | 349 | +    });
 | 
|  | 350 | +  }
 | 
| 247 | 351 |  
 | 
| 248 | 352 |    /**
 | 
| 249 | 353 |     * The current number of freezes applied to the notifications.
 | 
| 250 | 354 |     *
 | 
| 251 | 355 |     * @type {integer}
 | 
| 252 | 356 |     */
 | 
| 253 |  | -  _freezeNotificationsCount: 0,
 | 
|  | 357 | +  #freezeNotificationsCount = 0;
 | 
| 254 | 358 |    /**
 | 
| 255 | 359 |     * The queue for settings that have changed. To be broadcast in the
 | 
| 256 | 360 |     * notification when not frozen.
 | 
| 257 | 361 |     *
 | 
| 258 | 362 |     * @type {Set<string>}
 | 
| 259 | 363 |     */
 | 
| 260 |  | -  _notificationQueue: new Set(),
 | 
|  | 364 | +  #notificationQueue = new Set();
 | 
| 261 | 365 |    /**
 | 
| 262 | 366 |     * Send a notification if we have any queued and we are not frozen.
 | 
| 263 | 367 |     */
 | 
| 264 |  | -  _tryNotification() {
 | 
| 265 |  | -    if (this._freezeNotificationsCount || !this._notificationQueue.size) {
 | 
|  | 368 | +  #tryNotification() {
 | 
|  | 369 | +    if (this.#freezeNotificationsCount || !this.#notificationQueue.size) {
 | 
| 266 | 370 |        return;
 | 
| 267 | 371 |      }
 | 
| 268 | 372 |      Services.obs.notifyObservers(
 | 
| 269 |  | -      { changes: [...this._notificationQueue] },
 | 
|  | 373 | +      { changes: [...this.#notificationQueue] },
 | 
| 270 | 374 |        TorSettingsTopics.SettingsChanged
 | 
| 271 | 375 |      );
 | 
| 272 |  | -    this._notificationQueue.clear();
 | 
| 273 |  | -  },
 | 
|  | 376 | +    this.#notificationQueue.clear();
 | 
|  | 377 | +  }
 | 
| 274 | 378 |    /**
 | 
| 275 | 379 |     * Pause notifications for changes in setting values. This is useful if you
 | 
| 276 | 380 |     * need to make batch changes to settings.
 | 
| ... | ... | @@ -281,8 +385,8 @@ export const TorSettings = { | 
| 281 | 385 |     * `finally` block.
 | 
| 282 | 386 |     */
 | 
| 283 | 387 |    freezeNotifications() {
 | 
| 284 |  | -    this._freezeNotificationsCount++;
 | 
| 285 |  | -  },
 | 
|  | 388 | +    this.#freezeNotificationsCount++;
 | 
|  | 389 | +  }
 | 
| 286 | 390 |    /**
 | 
| 287 | 391 |     * Release the hold on notifications so they may be sent out.
 | 
| 288 | 392 |     *
 | 
| ... | ... | @@ -290,9 +394,9 @@ export const TorSettings = { | 
| 290 | 394 |     * only release them once it has also called this method.
 | 
| 291 | 395 |     */
 | 
| 292 | 396 |    thawNotifications() {
 | 
| 293 |  | -    this._freezeNotificationsCount--;
 | 
| 294 |  | -    this._tryNotification();
 | 
| 295 |  | -  },
 | 
|  | 397 | +    this.#freezeNotificationsCount--;
 | 
|  | 398 | +    this.#tryNotification();
 | 
|  | 399 | +  }
 | 
| 296 | 400 |    /**
 | 
| 297 | 401 |     * @typedef {object} TorSettingProperty
 | 
| 298 | 402 |     *
 | 
| ... | ... | @@ -316,22 +420,32 @@ export const TorSettings = { | 
| 316 | 420 |     * @param {string} groupname - The name of the setting group. The given
 | 
| 317 | 421 |     *   settings will be accessible from the TorSettings property of the same
 | 
| 318 | 422 |     *   name.
 | 
| 319 |  | -   * @param {object<string, TorSettingProperty>} propParams - An object that
 | 
|  | 423 | +   * @param {object.<string, TorSettingProperty>} propParams - An object that
 | 
| 320 | 424 |     *   defines the settings to add to this group. The object property names
 | 
| 321 | 425 |     *   will be mapped to properties of TorSettings under the given groupname
 | 
| 322 | 426 |     *   property. Details about the setting should be described in the
 | 
| 323 | 427 |     *   TorSettingProperty property value.
 | 
| 324 | 428 |     */
 | 
| 325 |  | -  _addProperties(groupname, propParams) {
 | 
|  | 429 | +  #addProperties(groupname, propParams) {
 | 
| 326 | 430 |      // Create a new object to hold all these settings.
 | 
| 327 | 431 |      const group = {};
 | 
| 328 | 432 |      for (const name in propParams) {
 | 
| 329 | 433 |        const { getter, transform, callback, copy, equal } = propParams[name];
 | 
| 330 | 434 |        Object.defineProperty(group, name, {
 | 
| 331 | 435 |          get: getter
 | 
| 332 |  | -          ? getter
 | 
|  | 436 | +          ? () => {
 | 
|  | 437 | +              // Allow getting in loadFromPrefs before we are initialized.
 | 
|  | 438 | +              if (!this.#allowUninitialized) {
 | 
|  | 439 | +                this.#checkIfInitialized();
 | 
|  | 440 | +              }
 | 
|  | 441 | +              return getter();
 | 
|  | 442 | +            }
 | 
| 333 | 443 |            : () => {
 | 
| 334 |  | -              let val = this._settings[groupname][name];
 | 
|  | 444 | +              // Allow getting in loadFromPrefs before we are initialized.
 | 
|  | 445 | +              if (!this.#allowUninitialized) {
 | 
|  | 446 | +                this.#checkIfInitialized();
 | 
|  | 447 | +              }
 | 
|  | 448 | +              let val = this.#settings[groupname][name];
 | 
| 335 | 449 |                if (copy) {
 | 
| 336 | 450 |                  val = copy(val);
 | 
| 337 | 451 |                }
 | 
| ... | ... | @@ -341,7 +455,11 @@ export const TorSettings = { | 
| 341 | 455 |          set: getter
 | 
| 342 | 456 |            ? undefined
 | 
| 343 | 457 |            : val => {
 | 
| 344 |  | -              const prevVal = this._settings[groupname][name];
 | 
|  | 458 | +              // Allow setting in loadFromPrefs before we are initialized.
 | 
|  | 459 | +              if (!this.#allowUninitialized) {
 | 
|  | 460 | +                this.#checkIfInitialized();
 | 
|  | 461 | +              }
 | 
|  | 462 | +              const prevVal = this.#settings[groupname][name];
 | 
| 345 | 463 |                this.freezeNotifications();
 | 
| 346 | 464 |                try {
 | 
| 347 | 465 |                  if (transform) {
 | 
| ... | ... | @@ -352,8 +470,8 @@ export const TorSettings = { | 
| 352 | 470 |                    if (callback) {
 | 
| 353 | 471 |                      callback(val);
 | 
| 354 | 472 |                    }
 | 
| 355 |  | -                  this._settings[groupname][name] = val;
 | 
| 356 |  | -                  this._notificationQueue.add(`${groupname}.${name}`);
 | 
|  | 473 | +                  this.#settings[groupname][name] = val;
 | 
|  | 474 | +                  this.#notificationQueue.add(`${groupname}.${name}`);
 | 
| 357 | 475 |                  }
 | 
| 358 | 476 |                } finally {
 | 
| 359 | 477 |                  this.thawNotifications();
 | 
| ... | ... | @@ -367,14 +485,14 @@ export const TorSettings = { | 
| 367 | 485 |        writable: false,
 | 
| 368 | 486 |        value: group,
 | 
| 369 | 487 |      });
 | 
| 370 |  | -  },
 | 
|  | 488 | +  }
 | 
| 371 | 489 |  
 | 
| 372 | 490 |    /**
 | 
| 373 | 491 |     * Regular _expression_ for a decimal non-negative integer.
 | 
| 374 | 492 |     *
 | 
| 375 | 493 |     * @type {RegExp}
 | 
| 376 | 494 |     */
 | 
| 377 |  | -  _portRegex: /^[0-9]+$/,
 | 
|  | 495 | +  #portRegex = /^[0-9]+$/;
 | 
| 378 | 496 |    /**
 | 
| 379 | 497 |     * Parse a string as a port number.
 | 
| 380 | 498 |     *
 | 
| ... | ... | @@ -385,13 +503,13 @@ export const TorSettings = { | 
| 385 | 503 |     * @return {integer?} - The port number, or null if the given value was not
 | 
| 386 | 504 |     *   valid.
 | 
| 387 | 505 |     */
 | 
| 388 |  | -  _parsePort(val, trim) {
 | 
|  | 506 | +  #parsePort(val, trim) {
 | 
| 389 | 507 |      if (typeof val === "string") {
 | 
| 390 | 508 |        if (trim) {
 | 
| 391 | 509 |          val = val.trim();
 | 
| 392 | 510 |        }
 | 
| 393 | 511 |        // ensure port string is a valid positive integer
 | 
| 394 |  | -      if (this._portRegex.test(val)) {
 | 
|  | 512 | +      if (this.#portRegex.test(val)) {
 | 
| 395 | 513 |          val = Number.parseInt(val, 10);
 | 
| 396 | 514 |        } else {
 | 
| 397 | 515 |          lazy.logger.error(`Invalid port string "${val}"`);
 | 
| ... | ... | @@ -403,7 +521,7 @@ export const TorSettings = { | 
| 403 | 521 |        return null;
 | 
| 404 | 522 |      }
 | 
| 405 | 523 |      return val;
 | 
| 406 |  | -  },
 | 
|  | 524 | +  }
 | 
| 407 | 525 |    /**
 | 
| 408 | 526 |     * Test whether two arrays have equal members and order.
 | 
| 409 | 527 |     *
 | 
| ... | ... | @@ -412,142 +530,57 @@ export const TorSettings = { | 
| 412 | 530 |     *
 | 
| 413 | 531 |     * @return {boolean} - Whether the two arrays are equal.
 | 
| 414 | 532 |     */
 | 
| 415 |  | -  _arrayEqual(val1, val2) {
 | 
|  | 533 | +  #arrayEqual(val1, val2) {
 | 
| 416 | 534 |      if (val1.length !== val2.length) {
 | 
| 417 | 535 |        return false;
 | 
| 418 | 536 |      }
 | 
| 419 | 537 |      return val1.every((v, i) => v === val2[i]);
 | 
| 420 |  | -  },
 | 
|  | 538 | +  }
 | 
|  | 539 | +
 | 
|  | 540 | +  /**
 | 
|  | 541 | +   * Return the bridge lines associated to a certain pluggable transport.
 | 
|  | 542 | +   *
 | 
|  | 543 | +   * @param {string} pt The pluggable transport to return the lines for
 | 
|  | 544 | +   * @returns {string[]} The bridge lines in random order
 | 
|  | 545 | +   */
 | 
|  | 546 | +  #getBuiltinBridges(pt) {
 | 
|  | 547 | +    // Shuffle so that Tor Browser users do not all try the built-in bridges in
 | 
|  | 548 | +    // the same order.
 | 
|  | 549 | +    return arrayShuffle(this.#builtinBridges[pt] ?? []);
 | 
|  | 550 | +  }
 | 
| 421 | 551 |  
 | 
| 422 |  | -  /* load or init our settings, and register observers */
 | 
|  | 552 | +  /**
 | 
|  | 553 | +   * Load or init our settings, and register observers.
 | 
|  | 554 | +   */
 | 
| 423 | 555 |    async init() {
 | 
| 424 |  | -    this._addProperties("quickstart", {
 | 
| 425 |  | -      enabled: {},
 | 
| 426 |  | -    });
 | 
| 427 |  | -    this._addProperties("bridges", {
 | 
| 428 |  | -      enabled: {},
 | 
| 429 |  | -      source: {
 | 
| 430 |  | -        transform: val => {
 | 
| 431 |  | -          if (Object.values(TorBridgeSource).includes(val)) {
 | 
| 432 |  | -            return val;
 | 
| 433 |  | -          }
 | 
| 434 |  | -          lazy.logger.error(`Not a valid bridge source: "${val}"`);
 | 
| 435 |  | -          return TorBridgeSource.Invalid;
 | 
| 436 |  | -        },
 | 
| 437 |  | -      },
 | 
| 438 |  | -      bridge_strings: {
 | 
| 439 |  | -        transform: val => {
 | 
| 440 |  | -          if (Array.isArray(val)) {
 | 
| 441 |  | -            return [...val];
 | 
| 442 |  | -          }
 | 
| 443 |  | -          return parseBridgeStrings(val);
 | 
| 444 |  | -        },
 | 
| 445 |  | -        copy: val => [...val],
 | 
| 446 |  | -        equal: (val1, val2) => this._arrayEqual(val1, val2),
 | 
| 447 |  | -      },
 | 
| 448 |  | -      builtin_type: {
 | 
| 449 |  | -        callback: val => {
 | 
| 450 |  | -          if (!val) {
 | 
| 451 |  | -            // Make sure that the source is not BuiltIn
 | 
| 452 |  | -            if (this.bridges.source === TorBridgeSource.BuiltIn) {
 | 
| 453 |  | -              this.bridges.source = TorBridgeSource.Invalid;
 | 
| 454 |  | -            }
 | 
| 455 |  | -            return;
 | 
| 456 |  | -          }
 | 
| 457 |  | -          const bridgeStrings = getBuiltinBridgeStrings(val);
 | 
| 458 |  | -          if (bridgeStrings.length) {
 | 
| 459 |  | -            this.bridges.bridge_strings = bridgeStrings;
 | 
| 460 |  | -            return;
 | 
| 461 |  | -          }
 | 
| 462 |  | -          lazy.logger.error(`No built-in ${val} bridges found`);
 | 
| 463 |  | -          // Change to be empty, this will trigger this callback again,
 | 
| 464 |  | -          // but with val as "".
 | 
| 465 |  | -          this.bridges.builtin_type == "";
 | 
| 466 |  | -        },
 | 
| 467 |  | -      },
 | 
| 468 |  | -    });
 | 
| 469 |  | -    this._addProperties("proxy", {
 | 
| 470 |  | -      enabled: {
 | 
| 471 |  | -        callback: val => {
 | 
| 472 |  | -          if (val) {
 | 
| 473 |  | -            return;
 | 
| 474 |  | -          }
 | 
| 475 |  | -          // Reset proxy settings.
 | 
| 476 |  | -          this.proxy.type = TorProxyType.Invalid;
 | 
| 477 |  | -          this.proxy.address = "";
 | 
| 478 |  | -          this.proxy.port = 0;
 | 
| 479 |  | -          this.proxy.username = "";
 | 
| 480 |  | -          this.proxy.password = "";
 | 
| 481 |  | -        },
 | 
| 482 |  | -      },
 | 
| 483 |  | -      type: {
 | 
| 484 |  | -        transform: val => {
 | 
| 485 |  | -          if (Object.values(TorProxyType).includes(val)) {
 | 
| 486 |  | -            return val;
 | 
| 487 |  | -          }
 | 
| 488 |  | -          lazy.logger.error(`Not a valid proxy type: "${val}"`);
 | 
| 489 |  | -          return TorProxyType.Invalid;
 | 
| 490 |  | -        },
 | 
| 491 |  | -      },
 | 
| 492 |  | -      address: {},
 | 
| 493 |  | -      port: {
 | 
| 494 |  | -        transform: val => {
 | 
| 495 |  | -          if (val === 0) {
 | 
| 496 |  | -            // This is a valid value that "unsets" the port.
 | 
| 497 |  | -            // Keep this value without giving a warning.
 | 
| 498 |  | -            // NOTE: In contrast, "0" is not valid.
 | 
| 499 |  | -            return 0;
 | 
| 500 |  | -          }
 | 
| 501 |  | -          // Unset to 0 if invalid null is returned.
 | 
| 502 |  | -          return this._parsePort(val, false) ?? 0;
 | 
| 503 |  | -        },
 | 
| 504 |  | -      },
 | 
| 505 |  | -      username: {},
 | 
| 506 |  | -      password: {},
 | 
| 507 |  | -      uri: {
 | 
| 508 |  | -        getter: () => {
 | 
| 509 |  | -          const { type, address, port, username, password } = this.proxy;
 | 
| 510 |  | -          switch (type) {
 | 
| 511 |  | -            case TorProxyType.Socks4:
 | 
| 512 |  | -              return `socks4a://${address}:${port}`;
 | 
| 513 |  | -            case TorProxyType.Socks5:
 | 
| 514 |  | -              if (username) {
 | 
| 515 |  | -                return `socks5://${username}:${password}@${address}:${port}`;
 | 
| 516 |  | -              }
 | 
| 517 |  | -              return `socks5://${address}:${port}`;
 | 
| 518 |  | -            case TorProxyType.HTTPS:
 | 
| 519 |  | -              if (username) {
 | 
| 520 |  | -                return `http://${username}:${password}@${address}:${port}`;
 | 
| 521 |  | -              }
 | 
| 522 |  | -              return `http://${address}:${port}`;
 | 
| 523 |  | -          }
 | 
| 524 |  | -          return null;
 | 
| 525 |  | -        },
 | 
| 526 |  | -      },
 | 
| 527 |  | -    });
 | 
| 528 |  | -    this._addProperties("firewall", {
 | 
| 529 |  | -      enabled: {
 | 
| 530 |  | -        callback: val => {
 | 
| 531 |  | -          if (!val) {
 | 
| 532 |  | -            this.firewall.allowed_ports = "";
 | 
| 533 |  | -          }
 | 
| 534 |  | -        },
 | 
| 535 |  | -      },
 | 
| 536 |  | -      allowed_ports: {
 | 
| 537 |  | -        transform: val => {
 | 
| 538 |  | -          if (!Array.isArray(val)) {
 | 
| 539 |  | -            val = val === "" ? [] : val.split(",");
 | 
| 540 |  | -          }
 | 
| 541 |  | -          // parse and remove duplicates
 | 
| 542 |  | -          const portSet = new Set(val.map(p => this._parsePort(p, true)));
 | 
| 543 |  | -          // parsePort returns null for failed parses, so remove it.
 | 
| 544 |  | -          portSet.delete(null);
 | 
| 545 |  | -          return [...portSet];
 | 
| 546 |  | -        },
 | 
| 547 |  | -        copy: val => [...val],
 | 
| 548 |  | -        equal: (val1, val2) => this._arrayEqual(val1, val2),
 | 
| 549 |  | -      },
 | 
| 550 |  | -    });
 | 
|  | 556 | +    if (this.#initialized) {
 | 
|  | 557 | +      lazy.logger.warn("Called init twice.");
 | 
|  | 558 | +      return;
 | 
|  | 559 | +    }
 | 
|  | 560 | +    try {
 | 
|  | 561 | +      await this.#initInternal();
 | 
|  | 562 | +      this.#initialized = true;
 | 
|  | 563 | +      this.#initComplete();
 | 
|  | 564 | +    } catch (e) {
 | 
|  | 565 | +      this.#initFailed(e);
 | 
|  | 566 | +      throw e;
 | 
|  | 567 | +    }
 | 
|  | 568 | +  }
 | 
|  | 569 | +
 | 
|  | 570 | +  /**
 | 
|  | 571 | +   * The actual implementation of the initialization, which is wrapped to make
 | 
|  | 572 | +   * it easier to update initializatedPromise.
 | 
|  | 573 | +   */
 | 
|  | 574 | +  async #initInternal() {
 | 
|  | 575 | +    try {
 | 
|  | 576 | +      const req = await fetch("chrome://global/content/pt_config.json");
 | 
|  | 577 | +      const config = await req.json();
 | 
|  | 578 | +      lazy.logger.debug("Loaded pt_config.json", config);
 | 
|  | 579 | +      this.#recommendedPT = config.recommendedDefault;
 | 
|  | 580 | +      this.#builtinBridges = config.bridges;
 | 
|  | 581 | +    } catch (e) {
 | 
|  | 582 | +      lazy.logger.error("Could not load the built-in PT config.", e);
 | 
|  | 583 | +    }
 | 
| 551 | 584 |  
 | 
| 552 | 585 |      // TODO: We could use a shared promise, and wait for it to be fullfilled
 | 
| 553 | 586 |      // instead of Service.obs.
 | 
| ... | ... | @@ -557,16 +590,18 @@ export const TorSettings = { | 
| 557 | 590 |          // Do not want notifications for initially loaded prefs.
 | 
| 558 | 591 |          this.freezeNotifications();
 | 
| 559 | 592 |          try {
 | 
| 560 |  | -          this.loadFromPrefs();
 | 
|  | 593 | +          this.#allowUninitialized = true;
 | 
|  | 594 | +          this.#loadFromPrefs();
 | 
| 561 | 595 |          } finally {
 | 
| 562 |  | -          this._notificationQueue.clear();
 | 
|  | 596 | +          this.#allowUninitialized = false;
 | 
|  | 597 | +          this.#notificationQueue.clear();
 | 
| 563 | 598 |            this.thawNotifications();
 | 
| 564 | 599 |          }
 | 
| 565 | 600 |        }
 | 
| 566 | 601 |        try {
 | 
| 567 | 602 |          const provider = await lazy.TorProviderBuilder.build();
 | 
| 568 | 603 |          if (provider.isRunning) {
 | 
| 569 |  | -          this.handleProcessReady();
 | 
|  | 604 | +          this.#handleProcessReady();
 | 
| 570 | 605 |            // No need to add an observer to call this again.
 | 
| 571 | 606 |            return;
 | 
| 572 | 607 |          }
 | 
| ... | ... | @@ -574,9 +609,33 @@ export const TorSettings = { | 
| 574 | 609 |  
 | 
| 575 | 610 |        Services.obs.addObserver(this, lazy.TorProviderTopics.ProcessIsReady);
 | 
| 576 | 611 |      }
 | 
| 577 |  | -  },
 | 
|  | 612 | +  }
 | 
|  | 613 | +
 | 
|  | 614 | +  /**
 | 
|  | 615 | +   * Check whether the object has been successfully initialized, and throw if
 | 
|  | 616 | +   * it has not.
 | 
|  | 617 | +   */
 | 
|  | 618 | +  #checkIfInitialized() {
 | 
|  | 619 | +    if (!this.#initialized) {
 | 
|  | 620 | +      lazy.logger.trace("Not initialized code path.");
 | 
|  | 621 | +      throw new Error(
 | 
|  | 622 | +        "TorSettings has not been initialized yet, or its initialization failed"
 | 
|  | 623 | +      );
 | 
|  | 624 | +    }
 | 
|  | 625 | +  }
 | 
|  | 626 | +
 | 
|  | 627 | +  /**
 | 
|  | 628 | +   * Tell whether TorSettings has been successfully initialized.
 | 
|  | 629 | +   *
 | 
|  | 630 | +   * @returns {boolean}
 | 
|  | 631 | +   */
 | 
|  | 632 | +  get initialized() {
 | 
|  | 633 | +    return this.#initialized;
 | 
|  | 634 | +  }
 | 
| 578 | 635 |  
 | 
| 579 |  | -  /* wait for relevant life-cycle events to apply saved settings */
 | 
|  | 636 | +  /**
 | 
|  | 637 | +   * Wait for relevant life-cycle events to apply saved settings.
 | 
|  | 638 | +   */
 | 
| 580 | 639 |    async observe(subject, topic, data) {
 | 
| 581 | 640 |      lazy.logger.debug(`Observed ${topic}`);
 | 
| 582 | 641 |  
 | 
| ... | ... | @@ -586,21 +645,26 @@ export const TorSettings = { | 
| 586 | 645 |            this,
 | 
| 587 | 646 |            lazy.TorProviderTopics.ProcessIsReady
 | 
| 588 | 647 |          );
 | 
| 589 |  | -        await this.handleProcessReady();
 | 
|  | 648 | +        await this.#handleProcessReady();
 | 
| 590 | 649 |          break;
 | 
| 591 | 650 |      }
 | 
| 592 |  | -  },
 | 
|  | 651 | +  }
 | 
| 593 | 652 |  
 | 
| 594 |  | -  // once the tor daemon is ready, we need to apply our settings
 | 
| 595 |  | -  async handleProcessReady() {
 | 
|  | 653 | +  /**
 | 
|  | 654 | +   * Apply the settings once the tor provider is ready and notify any observer
 | 
|  | 655 | +   * that the settings can be used.
 | 
|  | 656 | +   */
 | 
|  | 657 | +  async #handleProcessReady() {
 | 
| 596 | 658 |      // push down settings to tor
 | 
| 597 |  | -    await this.applySettings();
 | 
|  | 659 | +    await this.#applySettings(true);
 | 
| 598 | 660 |      lazy.logger.info("Ready");
 | 
| 599 | 661 |      Services.obs.notifyObservers(null, TorSettingsTopics.Ready);
 | 
| 600 |  | -  },
 | 
|  | 662 | +  }
 | 
| 601 | 663 |  
 | 
| 602 |  | -  // load our settings from prefs
 | 
| 603 |  | -  loadFromPrefs() {
 | 
|  | 664 | +  /**
 | 
|  | 665 | +   * Load our settings from prefs.
 | 
|  | 666 | +   */
 | 
|  | 667 | +  #loadFromPrefs() {
 | 
| 604 | 668 |      lazy.logger.debug("loadFromPrefs()");
 | 
| 605 | 669 |  
 | 
| 606 | 670 |      /* Quickstart */
 | 
| ... | ... | @@ -671,12 +735,16 @@ export const TorSettings = { | 
| 671 | 735 |          ""
 | 
| 672 | 736 |        );
 | 
| 673 | 737 |      }
 | 
| 674 |  | -  },
 | 
|  | 738 | +  }
 | 
| 675 | 739 |  
 | 
| 676 |  | -  // save our settings to prefs
 | 
|  | 740 | +  /**
 | 
|  | 741 | +   * Save our settings to prefs.
 | 
|  | 742 | +   */
 | 
| 677 | 743 |    saveToPrefs() {
 | 
| 678 | 744 |      lazy.logger.debug("saveToPrefs()");
 | 
| 679 | 745 |  
 | 
|  | 746 | +    this.#checkIfInitialized();
 | 
|  | 747 | +
 | 
| 680 | 748 |      /* Quickstart */
 | 
| 681 | 749 |      Services.prefs.setBoolPref(
 | 
| 682 | 750 |        TorSettingsPrefs.quickstart.enabled,
 | 
| ... | ... | @@ -758,77 +826,100 @@ export const TorSettings = { | 
| 758 | 826 |      Services.prefs.setBoolPref(TorSettingsPrefs.enabled, true);
 | 
| 759 | 827 |  
 | 
| 760 | 828 |      return this;
 | 
| 761 |  | -  },
 | 
|  | 829 | +  }
 | 
| 762 | 830 |  
 | 
| 763 |  | -  // push our settings down to the tor daemon
 | 
|  | 831 | +  /**
 | 
|  | 832 | +   * Push our settings down to the tor provider.
 | 
|  | 833 | +   */
 | 
| 764 | 834 |    async applySettings() {
 | 
| 765 |  | -    lazy.logger.debug("applySettings()");
 | 
|  | 835 | +    this.#checkIfInitialized();
 | 
|  | 836 | +    return this.#applySettings(false);
 | 
|  | 837 | +  }
 | 
|  | 838 | +
 | 
|  | 839 | +  /**
 | 
|  | 840 | +   * Internal implementation of applySettings that does not check if we are
 | 
|  | 841 | +   * initialized.
 | 
|  | 842 | +   */
 | 
|  | 843 | +  async #applySettings(allowUninitialized) {
 | 
|  | 844 | +    lazy.logger.debug("#applySettings()");
 | 
|  | 845 | +
 | 
| 766 | 846 |      const settingsMap = new Map();
 | 
| 767 | 847 |  
 | 
| 768 |  | -    /* Bridges */
 | 
| 769 |  | -    const haveBridges =
 | 
| 770 |  | -      this.bridges.enabled && !!this.bridges.bridge_strings.length;
 | 
| 771 |  | -    settingsMap.set(TorConfigKeys.useBridges, haveBridges);
 | 
| 772 |  | -    if (haveBridges) {
 | 
| 773 |  | -      settingsMap.set(TorConfigKeys.bridgeList, this.bridges.bridge_strings);
 | 
| 774 |  | -    } else {
 | 
| 775 |  | -      settingsMap.set(TorConfigKeys.bridgeList, null);
 | 
| 776 |  | -    }
 | 
|  | 848 | +    // #applySettings can be called only when #allowUninitialized is false
 | 
|  | 849 | +    this.#allowUninitialized = allowUninitialized;
 | 
| 777 | 850 |  
 | 
| 778 |  | -    /* Proxy */
 | 
| 779 |  | -    settingsMap.set(TorConfigKeys.socks4Proxy, null);
 | 
| 780 |  | -    settingsMap.set(TorConfigKeys.socks5Proxy, null);
 | 
| 781 |  | -    settingsMap.set(TorConfigKeys.socks5ProxyUsername, null);
 | 
| 782 |  | -    settingsMap.set(TorConfigKeys.socks5ProxyPassword, null);
 | 
| 783 |  | -    settingsMap.set(TorConfigKeys.httpsProxy, null);
 | 
| 784 |  | -    settingsMap.set(TorConfigKeys.httpsProxyAuthenticator, null);
 | 
| 785 |  | -    if (this.proxy.enabled) {
 | 
| 786 |  | -      const address = this.proxy.address;
 | 
| 787 |  | -      const port = this.proxy.port;
 | 
| 788 |  | -      const username = this.proxy.username;
 | 
| 789 |  | -      const password = this.proxy.password;
 | 
| 790 |  | -
 | 
| 791 |  | -      switch (this.proxy.type) {
 | 
| 792 |  | -        case TorProxyType.Socks4:
 | 
| 793 |  | -          settingsMap.set(TorConfigKeys.socks4Proxy, `${address}:${port}`);
 | 
| 794 |  | -          break;
 | 
| 795 |  | -        case TorProxyType.Socks5:
 | 
| 796 |  | -          settingsMap.set(TorConfigKeys.socks5Proxy, `${address}:${port}`);
 | 
| 797 |  | -          settingsMap.set(TorConfigKeys.socks5ProxyUsername, username);
 | 
| 798 |  | -          settingsMap.set(TorConfigKeys.socks5ProxyPassword, password);
 | 
| 799 |  | -          break;
 | 
| 800 |  | -        case TorProxyType.HTTPS:
 | 
| 801 |  | -          settingsMap.set(TorConfigKeys.httpsProxy, `${address}:${port}`);
 | 
| 802 |  | -          settingsMap.set(
 | 
| 803 |  | -            TorConfigKeys.httpsProxyAuthenticator,
 | 
| 804 |  | -            `${username}:${password}`
 | 
| 805 |  | -          );
 | 
| 806 |  | -          break;
 | 
|  | 851 | +    try {
 | 
|  | 852 | +      /* Bridges */
 | 
|  | 853 | +      const haveBridges =
 | 
|  | 854 | +        this.bridges.enabled && !!this.bridges.bridge_strings.length;
 | 
|  | 855 | +      settingsMap.set(TorConfigKeys.useBridges, haveBridges);
 | 
|  | 856 | +      if (haveBridges) {
 | 
|  | 857 | +        settingsMap.set(TorConfigKeys.bridgeList, this.bridges.bridge_strings);
 | 
|  | 858 | +      } else {
 | 
|  | 859 | +        settingsMap.set(TorConfigKeys.bridgeList, null);
 | 
| 807 | 860 |        }
 | 
| 808 |  | -    }
 | 
| 809 | 861 |  
 | 
| 810 |  | -    /* Firewall */
 | 
| 811 |  | -    if (this.firewall.enabled) {
 | 
| 812 |  | -      const reachableAddresses = this.firewall.allowed_ports
 | 
| 813 |  | -        .map(port => `*:${port}`)
 | 
| 814 |  | -        .join(",");
 | 
| 815 |  | -      settingsMap.set(TorConfigKeys.reachableAddresses, reachableAddresses);
 | 
| 816 |  | -    } else {
 | 
| 817 |  | -      settingsMap.set(TorConfigKeys.reachableAddresses, null);
 | 
|  | 862 | +      /* Proxy */
 | 
|  | 863 | +      settingsMap.set(TorConfigKeys.socks4Proxy, null);
 | 
|  | 864 | +      settingsMap.set(TorConfigKeys.socks5Proxy, null);
 | 
|  | 865 | +      settingsMap.set(TorConfigKeys.socks5ProxyUsername, null);
 | 
|  | 866 | +      settingsMap.set(TorConfigKeys.socks5ProxyPassword, null);
 | 
|  | 867 | +      settingsMap.set(TorConfigKeys.httpsProxy, null);
 | 
|  | 868 | +      settingsMap.set(TorConfigKeys.httpsProxyAuthenticator, null);
 | 
|  | 869 | +      if (this.proxy.enabled) {
 | 
|  | 870 | +        const address = this.proxy.address;
 | 
|  | 871 | +        const port = this.proxy.port;
 | 
|  | 872 | +        const username = this.proxy.username;
 | 
|  | 873 | +        const password = this.proxy.password;
 | 
|  | 874 | +
 | 
|  | 875 | +        switch (this.proxy.type) {
 | 
|  | 876 | +          case TorProxyType.Socks4:
 | 
|  | 877 | +            settingsMap.set(TorConfigKeys.socks4Proxy, `${address}:${port}`);
 | 
|  | 878 | +            break;
 | 
|  | 879 | +          case TorProxyType.Socks5:
 | 
|  | 880 | +            settingsMap.set(TorConfigKeys.socks5Proxy, `${address}:${port}`);
 | 
|  | 881 | +            settingsMap.set(TorConfigKeys.socks5ProxyUsername, username);
 | 
|  | 882 | +            settingsMap.set(TorConfigKeys.socks5ProxyPassword, password);
 | 
|  | 883 | +            break;
 | 
|  | 884 | +          case TorProxyType.HTTPS:
 | 
|  | 885 | +            settingsMap.set(TorConfigKeys.httpsProxy, `${address}:${port}`);
 | 
|  | 886 | +            settingsMap.set(
 | 
|  | 887 | +              TorConfigKeys.httpsProxyAuthenticator,
 | 
|  | 888 | +              `${username}:${password}`
 | 
|  | 889 | +            );
 | 
|  | 890 | +            break;
 | 
|  | 891 | +        }
 | 
|  | 892 | +      }
 | 
|  | 893 | +
 | 
|  | 894 | +      /* Firewall */
 | 
|  | 895 | +      if (this.firewall.enabled) {
 | 
|  | 896 | +        const reachableAddresses = this.firewall.allowed_ports
 | 
|  | 897 | +          .map(port => `*:${port}`)
 | 
|  | 898 | +          .join(",");
 | 
|  | 899 | +        settingsMap.set(TorConfigKeys.reachableAddresses, reachableAddresses);
 | 
|  | 900 | +      } else {
 | 
|  | 901 | +        settingsMap.set(TorConfigKeys.reachableAddresses, null);
 | 
|  | 902 | +      }
 | 
|  | 903 | +    } finally {
 | 
|  | 904 | +      this.#allowUninitialized = false;
 | 
| 818 | 905 |      }
 | 
| 819 | 906 |  
 | 
| 820 | 907 |      /* Push to Tor */
 | 
| 821 | 908 |      const provider = await lazy.TorProviderBuilder.build();
 | 
| 822 | 909 |      await provider.writeSettings(settingsMap);
 | 
|  | 910 | +  }
 | 
| 823 | 911 |  
 | 
| 824 |  | -    return this;
 | 
| 825 |  | -  },
 | 
| 826 |  | -
 | 
| 827 |  | -  // set all of our settings at once from a settings object
 | 
|  | 912 | +  /**
 | 
|  | 913 | +   * Set all of our settings at once from a settings object.
 | 
|  | 914 | +   *
 | 
|  | 915 | +   * @param {object} settings The settings object to set
 | 
|  | 916 | +   */
 | 
| 828 | 917 |    setSettings(settings) {
 | 
| 829 | 918 |      lazy.logger.debug("setSettings()");
 | 
|  | 919 | +    this.#checkIfInitialized();
 | 
|  | 920 | +
 | 
| 830 | 921 |      const backup = this.getSettings();
 | 
| 831 |  | -    const backup_notifications = [...this._notificationQueue];
 | 
|  | 922 | +    const backupNotifications = [...this.#notificationQueue];
 | 
| 832 | 923 |  
 | 
| 833 | 924 |      // Hold off on lots of notifications until all settings are changed.
 | 
| 834 | 925 |      this.freezeNotifications();
 | 
| ... | ... | @@ -869,10 +960,10 @@ export const TorSettings = { | 
| 869 | 960 |        // some other call to TorSettings to change anything whilst we are
 | 
| 870 | 961 |        // in this context (other than lower down in this call stack), so it is
 | 
| 871 | 962 |        // safe to discard all changes to settings and notifications.
 | 
| 872 |  | -      this._settings = backup;
 | 
| 873 |  | -      this._notificationQueue.clear();
 | 
| 874 |  | -      for (const notification of backup_notifications) {
 | 
| 875 |  | -        this._notificationQueue.add(notification);
 | 
|  | 963 | +      this.#settings = backup;
 | 
|  | 964 | +      this.#notificationQueue.clear();
 | 
|  | 965 | +      for (const notification of backupNotifications) {
 | 
|  | 966 | +        this.#notificationQueue.add(notification);
 | 
| 876 | 967 |        }
 | 
| 877 | 968 |  
 | 
| 878 | 969 |        lazy.logger.error("setSettings failed", ex);
 | 
| ... | ... | @@ -880,12 +971,36 @@ export const TorSettings = { | 
| 880 | 971 |        this.thawNotifications();
 | 
| 881 | 972 |      }
 | 
| 882 | 973 |  
 | 
| 883 |  | -    lazy.logger.debug("setSettings result", this._settings);
 | 
| 884 |  | -  },
 | 
|  | 974 | +    lazy.logger.debug("setSettings result", this.#settings);
 | 
|  | 975 | +  }
 | 
| 885 | 976 |  
 | 
| 886 |  | -  // get a copy of all our settings
 | 
|  | 977 | +  /**
 | 
|  | 978 | +   * Get a copy of all our settings.
 | 
|  | 979 | +   *
 | 
|  | 980 | +   * @returns {object} A copy of the settings object
 | 
|  | 981 | +   */
 | 
| 887 | 982 |    getSettings() {
 | 
| 888 | 983 |      lazy.logger.debug("getSettings()");
 | 
| 889 |  | -    return structuredClone(this._settings);
 | 
| 890 |  | -  },
 | 
| 891 |  | -}; | 
|  | 984 | +    this.#checkIfInitialized();
 | 
|  | 985 | +    return structuredClone(this.#settings);
 | 
|  | 986 | +  }
 | 
|  | 987 | +
 | 
|  | 988 | +  /**
 | 
|  | 989 | +   * Return an array with the pluggable transports for which we have at least a
 | 
|  | 990 | +   * built-in bridge line.
 | 
|  | 991 | +   *
 | 
|  | 992 | +   * @returns {string[]} An array with PT identifiers
 | 
|  | 993 | +   */
 | 
|  | 994 | +  get builtinBridgeTypes() {
 | 
|  | 995 | +    this.#checkIfInitialized();
 | 
|  | 996 | +    const types = Object.keys(this.#builtinBridges);
 | 
|  | 997 | +    const recommendedIndex = types.indexOf(this.#recommendedPT);
 | 
|  | 998 | +    if (recommendedIndex > 0) {
 | 
|  | 999 | +      types.splice(recommendedIndex, 1);
 | 
|  | 1000 | +      types.unshift(this.#recommendedPT);
 | 
|  | 1001 | +    }
 | 
|  | 1002 | +    return types;
 | 
|  | 1003 | +  }
 | 
|  | 1004 | +}
 | 
|  | 1005 | +
 | 
|  | 1006 | +export const TorSettings = new TorSettingsImpl(); |