[tbb-commits] [tor-browser] 70/90: Bug 40597: Implement TorSettings module

gitolite role git at cupani.torproject.org
Tue Nov 22 09:58:45 UTC 2022


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

richard pushed a commit to branch tor-browser-102.5.0esr-12.0-1
in repository tor-browser.

commit e6d8037ee72b7026c70a19cd44d7e2f3e02987bd
Author: Richard Pospesel <richard at torproject.org>
AuthorDate: Fri Aug 6 16:39:03 2021 +0200

    Bug 40597: Implement TorSettings module
    
    - migrated in-page settings read/write implementation from about:preferences#tor
      to the TorSettings module
    - TorSettings initially loads settings from the tor daemon, and saves them to
      firefox prefs
    - TorSettings notifies observers when a setting has changed; currently only
      QuickStart notification is implemented for parity with previous preference
      notify logic in about:torconnect and about:preferences#tor
    - about:preferences#tor, and about:torconnect now read and write settings
      thorugh the TorSettings module
    - all tor settings live in the torbrowser.settings.* preference branch
    - removed unused pref modify permission for about:torconnect content page from
      AsyncPrefs.jsm
    
    Bug 40645: Migrate Moat APIs to Moat.jsm module
---
 browser/modules/BridgeDB.jsm                       |   61 ++
 browser/modules/Moat.jsm                           |  808 +++++++++++++++
 browser/modules/TorConnect.jsm                     | 1081 ++++++++++++++++++++
 browser/modules/TorSettings.jsm                    |  782 ++++++++++++++
 browser/modules/moz.build                          |    4 +
 .../components/tor-launcher/TorStartupService.jsm  |   14 +
 6 files changed, 2750 insertions(+)

diff --git a/browser/modules/BridgeDB.jsm b/browser/modules/BridgeDB.jsm
new file mode 100644
index 000000000000..3110dfbbf20f
--- /dev/null
+++ b/browser/modules/BridgeDB.jsm
@@ -0,0 +1,61 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["BridgeDB"];
+
+const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm");
+
+var BridgeDB = {
+  _moatRPC: null,
+  _challenge: null,
+  _image: null,
+  _bridges: null,
+
+  get currentCaptchaImage() {
+    return this._image;
+  },
+
+  get currentBridges() {
+    return this._bridges;
+  },
+
+  async submitCaptchaGuess(solution) {
+    if (!this._moatRPC) {
+      this._moatRPC = new MoatRPC();
+      await this._moatRPC.init();
+    }
+
+    const response = await this._moatRPC.check(
+      "obfs4",
+      this._challenge,
+      solution,
+      false
+    );
+    this._bridges = response?.bridges;
+    return this._bridges;
+  },
+
+  async requestNewCaptchaImage() {
+    try {
+      if (!this._moatRPC) {
+        this._moatRPC = new MoatRPC();
+        await this._moatRPC.init();
+      }
+
+      const response = await this._moatRPC.fetch(["obfs4"]);
+      this._challenge = response.challenge;
+      this._image =
+        "data:image/jpeg;base64," + encodeURIComponent(response.image);
+    } catch (err) {
+      console.error("Could not request a captcha image", err);
+    }
+    return this._image;
+  },
+
+  close() {
+    this._moatRPC?.uninit();
+    this._moatRPC = null;
+    this._challenge = null;
+    this._image = null;
+    this._bridges = null;
+  },
+};
diff --git a/browser/modules/Moat.jsm b/browser/modules/Moat.jsm
new file mode 100644
index 000000000000..2527e0db5551
--- /dev/null
+++ b/browser/modules/Moat.jsm
@@ -0,0 +1,808 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["MoatRPC"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { Subprocess } = ChromeUtils.import(
+  "resource://gre/modules/Subprocess.jsm"
+);
+
+const { TorLauncherUtil } = ChromeUtils.import(
+  "resource://gre/modules/TorLauncherUtil.jsm"
+);
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource://gre/modules/TorProtocolService.jsm"
+);
+
+const { TorSettings, TorBridgeSource } = ChromeUtils.import(
+  "resource:///modules/TorSettings.jsm"
+);
+
+const TorLauncherPrefs = Object.freeze({
+  bridgedb_front: "extensions.torlauncher.bridgedb_front",
+  bridgedb_reflector: "extensions.torlauncher.bridgedb_reflector",
+  moat_service: "extensions.torlauncher.moat_service",
+});
+
+// Config keys used to query tor daemon properties
+const TorConfigKeys = Object.freeze({
+  clientTransportPlugin: "ClientTransportPlugin",
+});
+
+//
+// Launches and controls the PT process lifetime
+//
+class MeekTransport {
+  constructor() {
+    this._inited = false;
+    this._meekClientProcess = null;
+    this._meekProxyType = null;
+    this._meekProxyAddress = null;
+    this._meekProxyPort = 0;
+    this._meekProxyUsername = null;
+    this._meekProxyPassword = null;
+  }
+
+  // launches the meekprocess
+  async init() {
+    // ensure we haven't already init'd
+    if (this._inited) {
+      throw new Error("MeekTransport: Already initialized");
+    }
+
+    // cleanup function for killing orphaned pt process
+    let onException = () => {};
+    try {
+      // figure out which pluggable transport to use
+      const supportedTransports = ["meek", "meek_lite"];
+      let transportPlugins = await TorProtocolService.readStringArraySetting(
+        TorConfigKeys.clientTransportPlugin
+      );
+
+      let { meekTransport, meekClientPath, meekClientArgs } = (() => {
+        for (const line of transportPlugins) {
+          let tokens = line.split(" ");
+          if (tokens.length > 2 && tokens[1] == "exec") {
+            let transportArray = tokens[0].split(",").map(aStr => aStr.trim());
+            let transport = transportArray.find(aTransport =>
+              supportedTransports.includes(aTransport)
+            );
+
+            if (transport != undefined) {
+              return {
+                meekTransport: transport,
+                meekClientPath: tokens[2],
+                meekClientArgs: tokens.slice(3),
+              };
+            }
+          }
+        }
+
+        return {
+          meekTransport: null,
+          meekClientPath: null,
+          meekClientArgs: null,
+        };
+      })();
+
+      // Convert meek client path to absolute path if necessary
+      let meekWorkDir = await TorLauncherUtil.getTorFile(
+        "pt-startup-dir",
+        false
+      );
+      let re = TorLauncherUtil.isWindows ? /^[A-Za-z]:\\/ : /^\//;
+      if (!re.test(meekClientPath)) {
+        let meekPath = meekWorkDir.clone();
+        meekPath.appendRelativePath(meekClientPath);
+        meekClientPath = meekPath.path;
+      }
+
+      // Construct the per-connection arguments.
+      let meekClientEscapedArgs = "";
+      const meekReflector = Services.prefs.getStringPref(
+        TorLauncherPrefs.bridgedb_reflector
+      );
+
+      // Escape aValue per section 3.5 of the PT specification:
+      //   First the "<Key>=<Value>" formatted arguments MUST be escaped,
+      //   such that all backslash, equal sign, and semicolon characters
+      //   are escaped with a backslash.
+      let escapeArgValue = aValue => {
+        if (!aValue) {
+          return "";
+        }
+
+        let rv = aValue.replace(/\\/g, "\\\\");
+        rv = rv.replace(/=/g, "\\=");
+        rv = rv.replace(/;/g, "\\;");
+        return rv;
+      };
+
+      if (meekReflector) {
+        meekClientEscapedArgs += "url=";
+        meekClientEscapedArgs += escapeArgValue(meekReflector);
+      }
+      const meekFront = Services.prefs.getStringPref(
+        TorLauncherPrefs.bridgedb_front
+      );
+      if (meekFront) {
+        if (meekClientEscapedArgs.length) {
+          meekClientEscapedArgs += ";";
+        }
+        meekClientEscapedArgs += "front=";
+        meekClientEscapedArgs += escapeArgValue(meekFront);
+      }
+
+      // Setup env and start meek process
+      let ptStateDir = TorLauncherUtil.getTorFile("tordatadir", false);
+      ptStateDir.append("pt_state"); // Match what tor uses.
+
+      let envAdditions = {
+        TOR_PT_MANAGED_TRANSPORT_VER: "1",
+        TOR_PT_STATE_LOCATION: ptStateDir.path,
+        TOR_PT_EXIT_ON_STDIN_CLOSE: "1",
+        TOR_PT_CLIENT_TRANSPORTS: meekTransport,
+      };
+      if (TorSettings.proxy.enabled) {
+        envAdditions.TOR_PT_PROXY = TorSettings.proxy.uri;
+      }
+
+      let opts = {
+        command: meekClientPath,
+        arguments: meekClientArgs,
+        workdir: meekWorkDir.path,
+        environmentAppend: true,
+        environment: envAdditions,
+        stderr: "pipe",
+      };
+
+      // Launch meek client
+      let meekClientProcess = await Subprocess.call(opts);
+      // kill our process if exception is thrown
+      onException = () => {
+        meekClientProcess.kill();
+      };
+
+      // Callback chain for reading stderr
+      let stderrLogger = async () => {
+        if (this._meekClientProcess) {
+          let errString = await this._meekClientProcess.stderr.readString();
+          console.log(`MeekTransport: stderr => ${errString}`);
+          await stderrLogger();
+        }
+      };
+      stderrLogger();
+
+      // Read pt's stdout until terminal (CMETHODS DONE) is reached
+      // returns array of lines for parsing
+      let getInitLines = async (stdout = "") => {
+        let string = await meekClientProcess.stdout.readString();
+        stdout += string;
+
+        // look for the final message
+        const CMETHODS_DONE = "CMETHODS DONE";
+        let endIndex = stdout.lastIndexOf(CMETHODS_DONE);
+        if (endIndex != -1) {
+          endIndex += CMETHODS_DONE.length;
+          return stdout.substr(0, endIndex).split("\n");
+        }
+        return getInitLines(stdout);
+      };
+
+      // read our lines from pt's stdout
+      let meekInitLines = await getInitLines();
+      // tokenize our pt lines
+      let meekInitTokens = meekInitLines.map(line => {
+        let tokens = line.split(" ");
+        return {
+          keyword: tokens[0],
+          args: tokens.slice(1),
+        };
+      });
+
+      let meekProxyType = null;
+      let meekProxyAddr = null;
+      let meekProxyPort = 0;
+
+      // parse our pt tokens
+      for (const { keyword, args } of meekInitTokens) {
+        const argsJoined = args.join(" ");
+        let keywordError = false;
+        switch (keyword) {
+          case "VERSION": {
+            if (args.length != 1 || args[0] !== "1") {
+              keywordError = true;
+            }
+            break;
+          }
+          case "PROXY": {
+            if (args.length != 1 || args[0] !== "DONE") {
+              keywordError = true;
+            }
+            break;
+          }
+          case "CMETHOD": {
+            if (args.length != 3) {
+              keywordError = true;
+              break;
+            }
+            const transport = args[0];
+            const proxyType = args[1];
+            const addrPortString = args[2];
+            const addrPort = addrPortString.split(":");
+
+            if (transport !== meekTransport) {
+              throw new Error(
+                `MeekTransport: Expected ${meekTransport} but found ${transport}`
+              );
+            }
+            if (!["socks4", "socks4a", "socks5"].includes(proxyType)) {
+              throw new Error(
+                `MeekTransport: Invalid proxy type => ${proxyType}`
+              );
+            }
+            if (addrPort.length != 2) {
+              throw new Error(
+                `MeekTransport: Invalid proxy address => ${addrPortString}`
+              );
+            }
+            const addr = addrPort[0];
+            const port = parseInt(addrPort[1]);
+            if (port < 1 || port > 65535) {
+              throw new Error(`MeekTransport: Invalid proxy port => ${port}`);
+            }
+
+            // convert proxy type to strings used by protocol-proxy-servce
+            meekProxyType = proxyType === "socks5" ? "socks" : "socks4";
+            meekProxyAddr = addr;
+            meekProxyPort = port;
+
+            break;
+          }
+          // terminal
+          case "CMETHODS": {
+            if (args.length != 1 || args[0] !== "DONE") {
+              keywordError = true;
+            }
+            break;
+          }
+          // errors (all fall through):
+          case "VERSION-ERROR":
+          case "ENV-ERROR":
+          case "PROXY-ERROR":
+          case "CMETHOD-ERROR":
+            throw new Error(`MeekTransport: ${keyword} => '${argsJoined}'`);
+        }
+        if (keywordError) {
+          throw new Error(
+            `MeekTransport: Invalid ${keyword} keyword args => '${argsJoined}'`
+          );
+        }
+      }
+
+      this._meekClientProcess = meekClientProcess;
+      // register callback to cleanup on process exit
+      this._meekClientProcess.wait().then(exitObj => {
+        this._meekClientProcess = null;
+        this.uninit();
+      });
+
+      this._meekProxyType = meekProxyType;
+      this._meekProxyAddress = meekProxyAddr;
+      this._meekProxyPort = meekProxyPort;
+
+      // socks5
+      if (meekProxyType === "socks") {
+        if (meekClientEscapedArgs.length <= 255) {
+          this._meekProxyUsername = meekClientEscapedArgs;
+          this._meekProxyPassword = "\x00";
+        } else {
+          this._meekProxyUsername = meekClientEscapedArgs.substring(0, 255);
+          this._meekProxyPassword = meekClientEscapedArgs.substring(255);
+        }
+        // socks4
+      } else {
+        this._meekProxyUsername = meekClientEscapedArgs;
+        this._meekProxyPassword = undefined;
+      }
+
+      this._inited = true;
+    } catch (ex) {
+      onException();
+      throw ex;
+    }
+  }
+
+  async uninit() {
+    this._inited = false;
+
+    await this._meekClientProcess?.kill();
+    this._meekClientProcess = null;
+    this._meekProxyType = null;
+    this._meekProxyAddress = null;
+    this._meekProxyPort = 0;
+    this._meekProxyUsername = null;
+    this._meekProxyPassword = null;
+  }
+}
+
+//
+// Callback object with a cached promise for the returned Moat data
+//
+class MoatResponseListener {
+  constructor() {
+    this._response = "";
+    // we need this promise here because await nsIHttpChannel::asyncOpen does
+    // not return only once the request is complete, it seems to return
+    // after it begins, so we have to get the result from this listener object.
+    // This promise is only resolved once onStopRequest is called
+    this._responsePromise = new Promise((resolve, reject) => {
+      this._resolve = resolve;
+      this._reject = reject;
+    });
+  }
+
+  // callers wait on this for final response
+  response() {
+    return this._responsePromise;
+  }
+
+  // noop
+  onStartRequest(request) {}
+
+  // resolve or reject our Promise
+  onStopRequest(request, status) {
+    try {
+      if (!Components.isSuccessCode(status)) {
+        const errorMessage = TorLauncherUtil.getLocalizedStringForError(status);
+        this._reject(new Error(errorMessage));
+      }
+      if (request.responseStatus != 200) {
+        this._reject(new Error(request.responseStatusText));
+      }
+    } catch (err) {
+      this._reject(err);
+    }
+    this._resolve(this._response);
+  }
+
+  // read response data
+  onDataAvailable(request, stream, offset, length) {
+    const scriptableStream = Cc[
+      "@mozilla.org/scriptableinputstream;1"
+    ].createInstance(Ci.nsIScriptableInputStream);
+    scriptableStream.init(stream);
+    this._response += scriptableStream.read(length);
+  }
+}
+
+class InternetTestResponseListener {
+  constructor() {
+    this._promise = new Promise((resolve, reject) => {
+      this._resolve = resolve;
+      this._reject = reject;
+    });
+  }
+
+  // callers wait on this for final response
+  get status() {
+    return this._promise;
+  }
+
+  onStartRequest(request) {}
+
+  // resolve or reject our Promise
+  onStopRequest(request, status) {
+    let statuses = {};
+    try {
+      statuses = {
+        components: status,
+        successful: Components.isSuccessCode(status),
+      };
+      try {
+        if (statuses.successful) {
+          statuses.http = request.responseStatus;
+          statuses.date = request.getResponseHeader("Date");
+        }
+      } catch (err) {
+        console.warn(
+          "Successful request, but could not get the HTTP status or date",
+          err
+        );
+      }
+    } catch (err) {
+      this._reject(err);
+    }
+    this._resolve(statuses);
+  }
+
+  onDataAvailable(request, stream, offset, length) {
+    //  We do not care of the actual data, as long as we have a successful
+    // connection
+  }
+}
+
+// constructs the json objects and sends the request over moat
+class MoatRPC {
+  constructor() {
+    this._meekTransport = null;
+    this._inited = false;
+  }
+
+  get inited() {
+    return this._inited;
+  }
+
+  async init() {
+    if (this._inited) {
+      throw new Error("MoatRPC: Already initialized");
+    }
+
+    let meekTransport = new MeekTransport();
+    await meekTransport.init();
+    this._meekTransport = meekTransport;
+    this._inited = true;
+  }
+
+  async uninit() {
+    await this._meekTransport?.uninit();
+    this._meekTransport = null;
+    this._inited = false;
+  }
+
+  _makeHttpHandler(uriString) {
+    if (!this._inited) {
+      throw new Error("MoatRPC: Not initialized");
+    }
+
+    const proxyType = this._meekTransport._meekProxyType;
+    const proxyAddress = this._meekTransport._meekProxyAddress;
+    const proxyPort = this._meekTransport._meekProxyPort;
+    const proxyUsername = this._meekTransport._meekProxyUsername;
+    const proxyPassword = this._meekTransport._meekProxyPassword;
+
+    const proxyPS = Cc[
+      "@mozilla.org/network/protocol-proxy-service;1"
+    ].getService(Ci.nsIProtocolProxyService);
+    const flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+    const noTimeout = 0xffffffff; // UINT32_MAX
+    const proxyInfo = proxyPS.newProxyInfoWithAuth(
+      proxyType,
+      proxyAddress,
+      proxyPort,
+      proxyUsername,
+      proxyPassword,
+      undefined,
+      undefined,
+      flags,
+      noTimeout,
+      undefined
+    );
+
+    const uri = Services.io.newURI(uriString);
+    // There does not seem to be a way to directly create an nsILoadInfo from
+    // JavaScript, so we create a throw away non-proxied channel to get one.
+    const secFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
+    const loadInfo = Services.io.newChannelFromURI(
+      uri,
+      undefined,
+      Services.scriptSecurityManager.getSystemPrincipal(),
+      undefined,
+      secFlags,
+      Ci.nsIContentPolicy.TYPE_OTHER
+    ).loadInfo;
+
+    const httpHandler = Services.io
+      .getProtocolHandler("http")
+      .QueryInterface(Ci.nsIHttpProtocolHandler);
+    const ch = httpHandler
+      .newProxiedChannel(uri, proxyInfo, 0, undefined, loadInfo)
+      .QueryInterface(Ci.nsIHttpChannel);
+
+    // remove all headers except for 'Host"
+    const headers = [];
+    ch.visitRequestHeaders({
+      visitHeader: (key, val) => {
+        if (key !== "Host") {
+          headers.push(key);
+        }
+      },
+    });
+    headers.forEach(key => ch.setRequestHeader(key, "", false));
+
+    return ch;
+  }
+
+  async _makeRequest(procedure, args) {
+    const procedureURIString = `${Services.prefs.getStringPref(
+      TorLauncherPrefs.moat_service
+    )}/${procedure}`;
+    const ch = this._makeHttpHandler(procedureURIString);
+
+    // Arrange for the POST data to be sent.
+    const argsJson = JSON.stringify(args);
+
+    const inStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+      Ci.nsIStringInputStream
+    );
+    inStream.setData(argsJson, argsJson.length);
+    const upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
+    const contentType = "application/vnd.api+json";
+    upChannel.setUploadStream(inStream, contentType, argsJson.length);
+    ch.requestMethod = "POST";
+
+    // Make request
+    const listener = new MoatResponseListener();
+    await ch.asyncOpen(listener, ch);
+
+    // wait for response
+    const responseJSON = await listener.response();
+
+    // parse that JSON
+    return JSON.parse(responseJSON);
+  }
+
+  async testInternetConnection() {
+    const uri = `${Services.prefs.getStringPref(
+      TorLauncherPrefs.moat_service
+    )}/circumvention/countries`;
+    const ch = this._makeHttpHandler(uri);
+    ch.requestMethod = "HEAD";
+
+    const listener = new InternetTestResponseListener();
+    await ch.asyncOpen(listener, ch);
+    return listener.status;
+  }
+
+  //
+  // Moat APIs
+  //
+
+  // Receive a CAPTCHA challenge, takes the following parameters:
+  // - transports: array of transport strings available to us eg: ["obfs4", "meek"]
+  //
+  // returns an object with the following fields:
+  // - transport: a transport string the moat server decides it will send you selected
+  //   from the list of provided transports
+  // - image: a base64 encoded jpeg with the captcha to complete
+  // - challenge: a nonce/cookie string associated with this request
+  async fetch(transports) {
+    if (
+      // ensure this is an array
+      Array.isArray(transports) &&
+      // ensure array has values
+      !!transports.length &&
+      // ensure each value in the array is a string
+      transports.reduce((acc, cur) => acc && typeof cur === "string", true)
+    ) {
+      const args = {
+        data: [
+          {
+            version: "0.1.0",
+            type: "client-transports",
+            supported: transports,
+          },
+        ],
+      };
+      const response = await this._makeRequest("fetch", args);
+      if ("errors" in response) {
+        const code = response.errors[0].code;
+        const detail = response.errors[0].detail;
+        throw new Error(`MoatRPC: ${detail} (${code})`);
+      }
+
+      const transport = response.data[0].transport;
+      const image = response.data[0].image;
+      const challenge = response.data[0].challenge;
+
+      return { transport, image, challenge };
+    }
+    throw new Error("MoatRPC: fetch() expects a non-empty array of strings");
+  }
+
+  // Submit an answer for a CAPTCHA challenge and get back bridges, takes the following
+  // parameters:
+  // - transport: the transport string associated with a previous fetch request
+  // - challenge: the nonce string associated with the fetch request
+  // - solution: solution to the CAPTCHA associated with the fetch request
+  // - qrcode: true|false whether we want to get back a qrcode containing the bridge strings
+  //
+  // returns an object with the following fields:
+  // - bridges: an array of bridge line strings
+  // - qrcode: base64 encoded jpeg of bridges if requested, otherwise null
+  // if the provided solution is incorrect, returns an empty object
+  async check(transport, challenge, solution, qrcode) {
+    const args = {
+      data: [
+        {
+          id: "2",
+          version: "0.1.0",
+          type: "moat-solution",
+          transport,
+          challenge,
+          solution,
+          qrcode: qrcode ? "true" : "false",
+        },
+      ],
+    };
+    const response = await this._makeRequest("check", args);
+    if ("errors" in response) {
+      const code = response.errors[0].code;
+      const detail = response.errors[0].detail;
+      if (code == 419 && detail === "The CAPTCHA solution was incorrect.") {
+        return {};
+      }
+
+      throw new Error(`MoatRPC: ${detail} (${code})`);
+    }
+
+    const bridges = response.data[0].bridges;
+    const qrcodeImg = qrcode ? response.data[0].qrcode : null;
+
+    return { bridges, qrcode: qrcodeImg };
+  }
+
+  // Convert received settings object to format used by TorSettings module
+  // In the event of error, just return null
+  _fixupSettings(settings) {
+    try {
+      let retval = TorSettings.defaultSettings();
+      if ("bridges" in settings) {
+        retval.bridges.enabled = true;
+        switch (settings.bridges.source) {
+          case "builtin":
+            retval.bridges.source = TorBridgeSource.BuiltIn;
+            retval.bridges.builtin_type = settings.bridges.type;
+            // Tor Browser will periodically update the built-in bridge strings list using the
+            // circumvention_builtin() function, so we can ignore the bridge strings we have received here;
+            // BridgeDB only returns a subset of the available built-in bridges through the circumvention_settings()
+            // function which is fine for our 3rd parties, but we're better off ignoring them in Tor Browser, otherwise
+            // we get in a weird situation of needing to update our built-in bridges in a piece-meal fashion which
+            // seems over-complicated/error-prone
+            break;
+          case "bridgedb":
+            retval.bridges.source = TorBridgeSource.BridgeDB;
+            if (settings.bridges.bridge_strings) {
+              retval.bridges.bridge_strings = settings.bridges.bridge_strings;
+              retval.bridges.disabled_strings = [];
+            } else {
+              throw new Error(
+                "MoatRPC::_fixupSettings(): Received no bridge-strings for BridgeDB bridge source"
+              );
+            }
+            break;
+          default:
+            throw new Error(
+              `MoatRPC::_fixupSettings(): Unexpected bridge source '${settings.bridges.source}'`
+            );
+        }
+      }
+      if ("proxy" in settings) {
+        // TODO: populate proxy settings
+      }
+      if ("firewall" in settings) {
+        // TODO: populate firewall settings
+      }
+      return retval;
+    } catch (ex) {
+      console.log(ex.message);
+      return null;
+    }
+  }
+
+  // Converts a list of settings objects received from BridgeDB to a list of settings objects
+  // understood by the TorSettings module
+  // In the event of error, returns and empty list
+  _fixupSettingsList(settingsList) {
+    try {
+      let retval = [];
+      for (let settings of settingsList) {
+        settings = this._fixupSettings(settings);
+        if (settings != null) {
+          retval.push(settings);
+        }
+      }
+      return retval;
+    } catch (ex) {
+      console.log(ex.message);
+      return [];
+    }
+  }
+
+  // Request tor settings for the user optionally based on their location (derived
+  // from their IP), takes the following parameters:
+  // - transports: optional, an array of transports available to the client; if empty (or not
+  //   given) returns settings using all working transports known to the server
+  // - country: optional, an ISO 3166-1 alpha-2 country code to request settings for;
+  //   if not provided the country is determined by the user's IP address
+  //
+  // returns an array of settings objects in roughly the same format as the _settings
+  // object on the TorSettings module.
+  // - If the server cannot determine the user's country (and no country code is provided),
+  //   then null is returned
+  // - If the country has no associated settings, an empty array is returned
+  async circumvention_settings(transports, country) {
+    const args = {
+      transports: transports ? transports : [],
+      country,
+    };
+    const response = await this._makeRequest("circumvention/settings", args);
+    let settings = {};
+    if ("errors" in response) {
+      const code = response.errors[0].code;
+      const detail = response.errors[0].detail;
+      if (code == 406) {
+        console.log(
+          "MoatRPC::circumvention_settings(): Cannot automatically determine user's country-code"
+        );
+        // cannot determine user's country
+        return null;
+      }
+
+      throw new Error(`MoatRPC: ${detail} (${code})`);
+    } else if ("settings" in response) {
+      settings.settings = this._fixupSettingsList(response.settings);
+    }
+    if ("country" in response) {
+      settings.country = response.country;
+    }
+    return settings;
+  }
+
+  // Request a list of country codes with available censorship circumvention settings
+  //
+  // returns an array of ISO 3166-1 alpha-2 country codes which we can query settings
+  // for
+  async circumvention_countries() {
+    const args = {};
+    return this._makeRequest("circumvention/countries", args);
+  }
+
+  // Request a copy of the builtin bridges, takes the following parameters:
+  // - transports: optional, an array of transports we would like the latest bridge strings
+  //   for; if empty (or not given) returns all of them
+  //
+  // returns a map whose keys are pluggable transport types and whose values are arrays of
+  // bridge strings for that type
+  async circumvention_builtin(transports) {
+    const args = {
+      transports: transports ? transports : [],
+    };
+    const response = await this._makeRequest("circumvention/builtin", args);
+    if ("errors" in response) {
+      const code = response.errors[0].code;
+      const detail = response.errors[0].detail;
+      throw new Error(`MoatRPC: ${detail} (${code})`);
+    }
+
+    let map = new Map();
+    for (const [transport, bridge_strings] of Object.entries(response)) {
+      map.set(transport, bridge_strings);
+    }
+
+    return map;
+  }
+
+  // Request a copy of the defaul/fallback bridge settings, takes the following parameters:
+  // - transports: optional, an array of transports available to the client; if empty (or not
+  //   given) returns settings using all working transports known to the server
+  //
+  // returns an array of settings objects in roughly the same format as the _settings
+  // object on the TorSettings module
+  async circumvention_defaults(transports) {
+    const args = {
+      transports: transports ? transports : [],
+    };
+    const response = await this._makeRequest("circumvention/defaults", args);
+    if ("errors" in response) {
+      const code = response.errors[0].code;
+      const detail = response.errors[0].detail;
+      throw new Error(`MoatRPC: ${detail} (${code})`);
+    } else if ("settings" in response) {
+      return this._fixupSettingsList(response.settings);
+    }
+    return [];
+  }
+}
diff --git a/browser/modules/TorConnect.jsm b/browser/modules/TorConnect.jsm
new file mode 100644
index 000000000000..cb09c1dbefef
--- /dev/null
+++ b/browser/modules/TorConnect.jsm
@@ -0,0 +1,1081 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "InternetStatus",
+  "TorConnect",
+  "TorConnectTopics",
+  "TorConnectState",
+];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { setTimeout, clearTimeout } = ChromeUtils.import(
+  "resource://gre/modules/Timer.jsm"
+);
+
+const { BrowserWindowTracker } = ChromeUtils.import(
+  "resource:///modules/BrowserWindowTracker.jsm"
+);
+
+const { TorMonitorService } = ChromeUtils.import(
+  "resource://gre/modules/TorMonitorService.jsm"
+);
+const { TorBootstrapRequest } = ChromeUtils.import(
+  "resource://gre/modules/TorBootstrapRequest.jsm"
+);
+
+const {
+  TorSettings,
+  TorSettingsTopics,
+  TorBuiltinBridgeTypes,
+} = ChromeUtils.import("resource:///modules/TorSettings.jsm");
+
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm");
+
+const TorTopics = Object.freeze({
+  LogHasWarnOrErr: "TorLogHasWarnOrErr",
+  ProcessExited: "TorProcessExited",
+});
+
+/* Relevant prefs used by tor-launcher */
+const TorLauncherPrefs = Object.freeze({
+  prompt_at_startup: "extensions.torlauncher.prompt_at_startup",
+});
+
+const TorConnectPrefs = Object.freeze({
+  censorship_level: "torbrowser.debug.censorship_level",
+  allow_internet_test: "torbrowser.bootstrap.allow_internet_test",
+});
+
+const TorConnectState = Object.freeze({
+  /* Our initial state */
+  Initial: "Initial",
+  /* In-between initial boot and bootstrapping, users can change tor network settings during this state */
+  Configuring: "Configuring",
+  /* Tor is attempting to bootstrap with settings from censorship-circumvention db */
+  AutoBootstrapping: "AutoBootstrapping",
+  /* Tor is bootstrapping */
+  Bootstrapping: "Bootstrapping",
+  /* Passthrough state back to Configuring */
+  Error: "Error",
+  /* Final state, after successful bootstrap */
+  Bootstrapped: "Bootstrapped",
+  /* If we are using System tor or the legacy Tor-Launcher */
+  Disabled: "Disabled",
+});
+
+/*
+                             TorConnect State Transitions
+
+    ┌─────────┐                                                       ┌────────┐
+    │         ▼                                                       ▼        │
+    │       ┌──────────────────────────────────────────────────────────┐       │
+  ┌─┼────── │                           Error                          │ ◀───┐ │
+  │ │       └──────────────────────────────────────────────────────────┘     │ │
+  │ │         ▲                                                              │ │
+  │ │         │                                                              │ │
+  │ │         │                                                              │ │
+  │ │       ┌───────────────────────┐                       ┌──────────┐     │ │
+  │ │ ┌──── │        Initial        │ ────────────────────▶ │ Disabled │     │ │
+  │ │ │     └───────────────────────┘                       └──────────┘     │ │
+  │ │ │       │                                                              │ │
+  │ │ │       │ beginBootstrap()                                             │ │
+  │ │ │       ▼                                                              │ │
+  │ │ │     ┌──────────────────────────────────────────────────────────┐     │ │
+  │ │ │     │                      Bootstrapping                       │ ────┘ │
+  │ │ │     └──────────────────────────────────────────────────────────┘       │
+  │ │ │       │                        ▲                             │         │
+  │ │ │       │ cancelBootstrap()      │ beginBootstrap()            └────┐    │
+  │ │ │       ▼                        │                                  │    │
+  │ │ │     ┌──────────────────────────────────────────────────────────┐  │    │
+  │ │ └───▶ │                                                          │ ─┼────┘
+  │ │       │                                                          │  │
+  │ │       │                                                          │  │
+  │ │       │                       Configuring                        │  │
+  │ │       │                                                          │  │
+  │ │       │                                                          │  │
+  └─┼─────▶ │                                                          │  │
+    │       └──────────────────────────────────────────────────────────┘  │
+    │         │                        ▲                                  │
+    │         │ beginAutoBootstrap()   │ cancelAutoBootstrap()            │
+    │         ▼                        │                                  │
+    │       ┌───────────────────────┐  │                                  │
+    └────── │   AutoBootstrapping   │ ─┘                                  │
+            └───────────────────────┘                                     │
+              │                                                           │
+              │                                                           │
+              ▼                                                           │
+            ┌───────────────────────┐                                     │
+            │     Bootstrapped      │ ◀───────────────────────────────────┘
+            └───────────────────────┘
+*/
+
+/* Maps allowed state transitions
+   TorConnectStateTransitions[state] maps to an array of allowed states to transition to
+   This is just an encoding of the above transition diagram that we verify at runtime
+*/
+const TorConnectStateTransitions = Object.freeze(
+  new Map([
+    [
+      TorConnectState.Initial,
+      [
+        TorConnectState.Disabled,
+        TorConnectState.Bootstrapping,
+        TorConnectState.Configuring,
+        TorConnectState.Error,
+      ],
+    ],
+    [
+      TorConnectState.Configuring,
+      [
+        TorConnectState.AutoBootstrapping,
+        TorConnectState.Bootstrapping,
+        TorConnectState.Error,
+      ],
+    ],
+    [
+      TorConnectState.AutoBootstrapping,
+      [
+        TorConnectState.Configuring,
+        TorConnectState.Bootstrapped,
+        TorConnectState.Error,
+      ],
+    ],
+    [
+      TorConnectState.Bootstrapping,
+      [
+        TorConnectState.Configuring,
+        TorConnectState.Bootstrapped,
+        TorConnectState.Error,
+      ],
+    ],
+    [TorConnectState.Error, [TorConnectState.Configuring]],
+    // terminal states
+    [TorConnectState.Bootstrapped, []],
+    [TorConnectState.Disabled, []],
+  ])
+);
+
+/* Topics Notified by the TorConnect module */
+const TorConnectTopics = Object.freeze({
+  StateChange: "torconnect:state-change",
+  BootstrapProgress: "torconnect:bootstrap-progress",
+  BootstrapComplete: "torconnect:bootstrap-complete",
+  BootstrapError: "torconnect:bootstrap-error",
+});
+
+// The StateCallback is a wrapper around an async function which executes during
+// the lifetime of a TorConnect State. A system is also provided to allow this
+// ongoing function to early-out via a per StateCallback on_transition callback
+// which may be called externally when we need to early-out and move on to another
+// state (for example, from Bootstrapping to Configuring in the event the user
+// cancels a bootstrap attempt)
+class StateCallback {
+  constructor(state, callback) {
+    this._state = state;
+    this._callback = callback;
+    this._init();
+  }
+
+  _init() {
+    // this context object is bound to the callback each time transition is
+    // attempted via begin()
+    this._context = {
+      // This callback may be overwritten in the _callback for each state
+      // States may have various pieces of work which need to occur
+      // before they can be exited (eg resource cleanup)
+      // See the _stateCallbacks map for examples
+      on_transition: nextState => {},
+
+      // flag used to determine if a StateCallback should early-out
+      // its work
+      _transitioning: false,
+
+      // may be called within the StateCallback to determine if exit is possible
+      get transitioning() {
+        return this._transitioning;
+      },
+    };
+  }
+
+  async begin(...args) {
+    console.log(`TorConnect: Entering ${this._state} state`);
+    this._init();
+    try {
+      // this Promise will block until this StateCallback has completed its work
+      await Promise.resolve(this._callback.call(this._context, ...args));
+      console.log(`TorConnect: Exited ${this._state} state`);
+
+      // handled state transition
+      Services.obs.notifyObservers(
+        { state: this._nextState },
+        TorConnectTopics.StateChange
+      );
+      TorConnect._callback(this._nextState).begin(...this._nextStateArgs);
+    } catch (obj) {
+      TorConnect._changeState(
+        TorConnectState.Error,
+        obj?.message,
+        obj?.details
+      );
+    }
+  }
+
+  transition(nextState, ...args) {
+    this._nextState = nextState;
+    this._nextStateArgs = [...args];
+
+    // calls the on_transition callback to resolve any async work or do per-state cleanup
+    // this call to on_transition should resolve the async work currentlying going on in this.begin()
+    this._context.on_transition(nextState);
+    this._context._transitioning = true;
+  }
+}
+
+// async method to sleep for a given amount of time
+const debug_sleep = async ms => {
+  return new Promise((resolve, reject) => {
+    setTimeout(resolve, ms);
+  });
+};
+
+const InternetStatus = Object.freeze({
+  Unknown: -1,
+  Offline: 0,
+  Online: 1,
+});
+
+class InternetTest {
+  constructor() {
+    this._enabled = Services.prefs.getBoolPref(
+      TorConnectPrefs.allow_internet_test,
+      true
+    );
+
+    this._status = InternetStatus.Unknown;
+    this._error = null;
+    this._pending = false;
+    if (this._enabled) {
+      this._timeout = setTimeout(() => {
+        this._timeout = null;
+        this.test();
+      }, this.timeoutRand());
+    }
+    this.onResult = (online, date) => {};
+    this.onError = err => {};
+  }
+
+  test() {
+    if (this._pending || !this._enabled) {
+      return;
+    }
+    this.cancel();
+    this._pending = true;
+
+    console.log("TorConnect: starting the Internet test");
+    this._testAsync()
+      .then(status => {
+        this._pending = false;
+        this._status = status.successful
+          ? InternetStatus.Online
+          : InternetStatus.Offline;
+        console.log(
+          `TorConnect: performed Internet test, outcome ${this._status}`
+        );
+        this.onResult(this.status, status.date);
+      })
+      .catch(error => {
+        this._error = error;
+        this._pending = false;
+        this.onError(error);
+      });
+  }
+
+  cancel() {
+    if (this._timeout !== null) {
+      clearTimeout(this._timeout);
+      this._timeout = null;
+    }
+  }
+
+  async _testAsync() {
+    // Callbacks for the Internet test are desirable, because we will be
+    // waiting both for the bootstrap, and for the Internet test.
+    // However, managing Moat with async/await is much easier as it avoids a
+    // callback hell, and it makes extra explicit that we are uniniting it.
+    const mrpc = new MoatRPC();
+    let status = null;
+    let error = null;
+    try {
+      await mrpc.init();
+      status = await mrpc.testInternetConnection();
+    } catch (err) {
+      console.error("Error while checking the Internet connection", err);
+      error = err;
+    } finally {
+      mrpc.uninit();
+    }
+    if (error !== null) {
+      throw error;
+    }
+    return status;
+  }
+
+  get status() {
+    return this._status;
+  }
+
+  get error() {
+    return this._error;
+  }
+
+  get enabled() {
+    return this._enabled;
+  }
+
+  // We randomize the Internet test timeout to make fingerprinting it harder, at least a little bit...
+  timeoutRand() {
+    const offset = 30000;
+    const randRange = 5000;
+    return offset + randRange * (Math.random() * 2 - 1);
+  }
+}
+
+const TorConnect = (() => {
+  let retval = {
+    _state: TorConnectState.Initial,
+    _bootstrapProgress: 0,
+    _bootstrapStatus: null,
+    _internetStatus: InternetStatus.Unknown,
+    // list of country codes Moat has settings for
+    _countryCodes: [],
+    _countryNames: Object.freeze(
+      (() => {
+        const codes = Services.intl.getAvailableLocaleDisplayNames("region");
+        const names = Services.intl.getRegionDisplayNames(undefined, codes);
+        let codesNames = {};
+        for (let i = 0; i < codes.length; i++) {
+          codesNames[codes[i]] = names[i];
+        }
+        return codesNames;
+      })()
+    ),
+    _detectedLocation: "",
+    _errorMessage: null,
+    _errorDetails: null,
+    _logHasWarningOrError: false,
+    _hasBootstrapEverFailed: false,
+    _transitionPromise: null,
+
+    // This is used as a helper to make the state of about:torconnect persistent
+    // during a session, but TorConnect does not use this data at all.
+    _uiState: {},
+
+    /* These functions represent ongoing work associated with one of our states
+           Some of these functions are mostly empty, apart from defining an
+           on_transition function used to resolve their Promise */
+    _stateCallbacks: Object.freeze(
+      new Map([
+        /* Initial is never transitioned to */
+        [
+          TorConnectState.Initial,
+          new StateCallback(TorConnectState.Initial, async function() {
+            // The initial state doesn't actually do anything, so here is a skeleton for other
+            // states which do perform work
+            await new Promise(async (resolve, reject) => {
+              // This function is provided to signal to the callback that it is complete.
+              // It is called as a result of _changeState and at the very least must
+              // resolve the root Promise object within the StateCallback function
+              // The on_transition callback may also perform necessary cleanup work
+              this.on_transition = nextState => {
+                resolve();
+              };
+
+              try {
+                // each state may have a sequence of async work to do
+                let asyncWork = async () => {};
+                await asyncWork();
+
+                // after each block we may check for an opportunity to early-out
+                if (this.transitioning) {
+                  return;
+                }
+
+                // repeat the above pattern as necessary
+              } catch (err) {
+                // any thrown exceptions here will trigger a transition to the Error state
+                TorConnect._changeState(
+                  TorConnectState.Error,
+                  err?.message,
+                  err?.details
+                );
+              }
+            });
+          }),
+        ],
+        /* Configuring */
+        [
+          TorConnectState.Configuring,
+          new StateCallback(TorConnectState.Configuring, async function() {
+            await new Promise(async (resolve, reject) => {
+              this.on_transition = nextState => {
+                resolve();
+              };
+            });
+          }),
+        ],
+        /* Bootstrapping */
+        [
+          TorConnectState.Bootstrapping,
+          new StateCallback(TorConnectState.Bootstrapping, async function() {
+            // wait until bootstrap completes or we get an error
+            await new Promise(async (resolve, reject) => {
+              // debug hook to simulate censorship preventing bootstrapping
+              if (
+                Services.prefs.getIntPref(TorConnectPrefs.censorship_level, 0) >
+                0
+              ) {
+                this.on_transition = nextState => {
+                  resolve();
+                };
+                await debug_sleep(1500);
+                TorConnect._hasBootstrapEverFailed = true;
+                if (
+                  Services.prefs.getIntPref(
+                    TorConnectPrefs.censorship_level,
+                    0
+                  ) === 2
+                ) {
+                  const codes = Object.keys(TorConnect._countryNames);
+                  TorConnect._detectedLocation =
+                    codes[Math.floor(Math.random() * codes.length)];
+                }
+                TorConnect._changeState(
+                  TorConnectState.Error,
+                  "Bootstrap failed (for debugging purposes)",
+                  "Error: Censorship simulation",
+                  true
+                );
+                TorMonitorService.setBootstrapError();
+                return;
+              }
+
+              const tbr = new TorBootstrapRequest();
+              const internetTest = new InternetTest();
+
+              let bootstrapError = "";
+              let bootstrapErrorDetails = "";
+              const maybeTransitionToError = () => {
+                if (
+                  internetTest.status === InternetStatus.Unknown &&
+                  internetTest.error === null &&
+                  internetTest.enabled
+                ) {
+                  // We have been called by a failed bootstrap, but the internet test has not run yet - force
+                  // it to run immediately!
+                  internetTest.test();
+                  // Return from this call, because the Internet test's callback will call us again
+                  return;
+                }
+                // Do not transition to the offline error until we are sure that also the bootstrap failed, in
+                // case Moat is down but the bootstrap can proceed anyway.
+                if (bootstrapError === "") {
+                  return;
+                }
+                if (internetTest.status === InternetStatus.Offline) {
+                  TorConnect._changeState(
+                    TorConnectState.Error,
+                    TorStrings.torConnect.offline,
+                    "",
+                    true
+                  );
+                } else {
+                  // Give priority to the bootstrap error, in case the Internet test fails
+                  TorConnect._hasBootstrapEverFailed = true;
+                  TorConnect._changeState(
+                    TorConnectState.Error,
+                    bootstrapError,
+                    bootstrapErrorDetails,
+                    true
+                  );
+                }
+              };
+
+              this.on_transition = async nextState => {
+                if (nextState === TorConnectState.Configuring) {
+                  // stop bootstrap process if user cancelled
+                  internetTest.cancel();
+                  await tbr.cancel();
+                }
+                resolve();
+              };
+
+              tbr.onbootstrapstatus = (progress, status) => {
+                TorConnect._updateBootstrapStatus(progress, status);
+              };
+              tbr.onbootstrapcomplete = () => {
+                internetTest.cancel();
+                TorConnect._changeState(TorConnectState.Bootstrapped);
+              };
+              tbr.onbootstraperror = (message, details) => {
+                // We have to wait for the Internet test to finish before sending the bootstrap error
+                bootstrapError = message;
+                bootstrapErrorDetails = details;
+                maybeTransitionToError();
+              };
+
+              internetTest.onResult = (status, date) => {
+                // TODO: Use the date to save the clock skew?
+                TorConnect._internetStatus = status;
+                maybeTransitionToError();
+              };
+              internetTest.onError = () => {
+                maybeTransitionToError();
+              };
+
+              tbr.bootstrap();
+            });
+          }),
+        ],
+        /* AutoBootstrapping */
+        [
+          TorConnectState.AutoBootstrapping,
+          new StateCallback(TorConnectState.AutoBootstrapping, async function(
+            countryCode
+          ) {
+            await new Promise(async (resolve, reject) => {
+              this.on_transition = nextState => {
+                resolve();
+              };
+
+              // debug hook to simulate censorship preventing bootstrapping
+              {
+                const censorshipLevel = Services.prefs.getIntPref(
+                  TorConnectPrefs.censorship_level,
+                  0
+                );
+                if (censorshipLevel > 1) {
+                  this.on_transition = nextState => {
+                    resolve();
+                  };
+                  // always fail even after manually selecting location specific settings
+                  if (censorshipLevel == 3) {
+                    await debug_sleep(2500);
+                    TorConnect._changeState(
+                      TorConnectState.Error,
+                      "Error: censorship simulation",
+                      "",
+                      true
+                    );
+                    return;
+                    // only fail after auto selecting, manually selecting succeeds
+                  } else if (censorshipLevel == 2 && !countryCode) {
+                    await debug_sleep(2500);
+                    TorConnect._changeState(
+                      TorConnectState.Error,
+                      "Error: Severe Censorship simulation",
+                      "",
+                      true
+                    );
+                    return;
+                  }
+                  TorMonitorService.setBootstrapError();
+                }
+              }
+
+              const throw_error = (message, details) => {
+                let err = new Error(message);
+                err.details = details;
+                throw err;
+              };
+
+              // lookup user's potential censorship circumvention settings from Moat service
+              try {
+                this.mrpc = new MoatRPC();
+                await this.mrpc.init();
+
+                if (this.transitioning) {
+                  return;
+                }
+
+                const settings = await this.mrpc.circumvention_settings(
+                  [...TorBuiltinBridgeTypes, "vanilla"],
+                  countryCode
+                );
+
+                if (this.transitioning) {
+                  return;
+                }
+
+                if (settings?.country) {
+                  TorConnect._detectedLocation = settings.country;
+                }
+                if (settings?.settings && settings.settings.length) {
+                  this.settings = settings.settings;
+                } else {
+                  try {
+                    this.settings = await this.mrpc.circumvention_defaults([
+                      ...TorBuiltinBridgeTypes,
+                      "vanilla",
+                    ]);
+                  } catch (err) {
+                    console.error(
+                      "We did not get localized settings, and default settings failed as well",
+                      err
+                    );
+                  }
+                }
+                if (this.settings === null || this.settings.length === 0) {
+                  // The fallback has failed as well, so throw the original error
+                  if (!TorConnect._detectedLocation) {
+                    // unable to determine country
+                    throw_error(
+                      TorStrings.torConnect.autoBootstrappingFailed,
+                      TorStrings.torConnect.cannotDetermineCountry
+                    );
+                  } else {
+                    // no settings available for country
+                    throw_error(
+                      TorStrings.torConnect.autoBootstrappingFailed,
+                      TorStrings.torConnect.noSettingsForCountry
+                    );
+                  }
+                }
+
+                // apply each of our settings and try to bootstrap with each
+                try {
+                  this.originalSettings = TorSettings.getSettings();
+
+                  for (const [
+                    index,
+                    currentSetting,
+                  ] of this.settings.entries()) {
+                    // we want to break here so we can fall through and restore original settings
+                    if (this.transitioning) {
+                      break;
+                    }
+
+                    console.log(
+                      `TorConnect: Attempting Bootstrap with configuration ${index +
+                        1}/${this.settings.length}`
+                    );
+
+                    TorSettings.setSettings(currentSetting);
+                    await TorSettings.applySettings();
+
+                    // build out our bootstrap request
+                    const tbr = new TorBootstrapRequest();
+                    tbr.onbootstrapstatus = (progress, status) => {
+                      TorConnect._updateBootstrapStatus(progress, status);
+                    };
+                    tbr.onbootstraperror = (message, details) => {
+                      console.log(
+                        `TorConnect: Auto-Bootstrap error => ${message}; ${details}`
+                      );
+                    };
+
+                    // update transition callback for user cancel
+                    this.on_transition = async nextState => {
+                      if (nextState === TorConnectState.Configuring) {
+                        await tbr.cancel();
+                      }
+                      resolve();
+                    };
+
+                    // begin bootstrap
+                    if (await tbr.bootstrap()) {
+                      // persist the current settings to preferences
+                      TorSettings.saveToPrefs();
+                      TorConnect._changeState(TorConnectState.Bootstrapped);
+                      return;
+                    }
+                  }
+
+                  // bootstrapped failed for all potential settings, so reset daemon to use original
+                  TorSettings.setSettings(this.originalSettings);
+                  await TorSettings.applySettings();
+                  TorSettings.saveToPrefs();
+
+                  // only explicitly change state here if something else has not transitioned us
+                  if (!this.transitioning) {
+                    throw_error(
+                      TorStrings.torConnect.autoBootstrappingFailed,
+                      TorStrings.torConnect.autoBootstrappingAllFailed
+                    );
+                  }
+                  return;
+                } catch (err) {
+                  // restore original settings in case of error
+                  try {
+                    TorSettings.setSettings(this.originalSettings);
+                    await TorSettings.applySettings();
+                  } catch (errRestore) {
+                    console.log(
+                      `TorConnect: Failed to restore original settings => ${errRestore}`
+                    );
+                  }
+                  // throw to outer catch to transition us
+                  throw err;
+                }
+              } catch (err) {
+                if (this.mrpc?.inited) {
+                  // lookup countries which have settings available
+                  TorConnect._countryCodes = await this.mrpc.circumvention_countries();
+                }
+                TorConnect._changeState(
+                  TorConnectState.Error,
+                  err?.message,
+                  err?.details,
+                  true
+                );
+              } finally {
+                // important to uninit MoatRPC object or else the pt process will live as long as tor-browser
+                this.mrpc?.uninit();
+              }
+            });
+          }),
+        ],
+        /* Bootstrapped */
+        [
+          TorConnectState.Bootstrapped,
+          new StateCallback(TorConnectState.Bootstrapped, async function() {
+            await new Promise((resolve, reject) => {
+              // on_transition not defined because no way to leave Bootstrapped state
+              // notify observers of bootstrap completion
+              Services.obs.notifyObservers(
+                null,
+                TorConnectTopics.BootstrapComplete
+              );
+            });
+          }),
+        ],
+        /* Error */
+        [
+          TorConnectState.Error,
+          new StateCallback(TorConnectState.Error, async function(
+            errorMessage,
+            errorDetails,
+            bootstrappingFailure
+          ) {
+            await new Promise((resolve, reject) => {
+              this.on_transition = async nextState => {
+                resolve();
+              };
+
+              TorConnect._errorMessage = errorMessage;
+              TorConnect._errorDetails = errorDetails;
+
+              Services.obs.notifyObservers(
+                { message: errorMessage, details: errorDetails },
+                TorConnectTopics.BootstrapError
+              );
+
+              TorConnect._changeState(TorConnectState.Configuring);
+            });
+          }),
+        ],
+        /* Disabled */
+        [
+          TorConnectState.Disabled,
+          new StateCallback(TorConnectState.Disabled, async function() {
+            await new Promise((resolve, reject) => {
+              // no-op, on_transition not defined because no way to leave Disabled state
+            });
+          }),
+        ],
+      ])
+    ),
+
+    _callback(state) {
+      return this._stateCallbacks.get(state);
+    },
+
+    _changeState(newState, ...args) {
+      const prevState = this._state;
+
+      // ensure this is a valid state transition
+      if (!TorConnectStateTransitions.get(prevState)?.includes(newState)) {
+        throw Error(
+          `TorConnect: Attempted invalid state transition from ${prevState} to ${newState}`
+        );
+      }
+
+      console.log(
+        `TorConnect: Try transitioning from ${prevState} to ${newState}`
+      );
+
+      // set our new state first so that state transitions can themselves trigger
+      // a state transition
+      this._state = newState;
+
+      // call our state function and forward any args
+      this._callback(prevState).transition(newState, ...args);
+    },
+
+    _updateBootstrapStatus(progress, status) {
+      this._bootstrapProgress = progress;
+      this._bootstrapStatus = status;
+
+      console.log(
+        `TorConnect: Bootstrapping ${this._bootstrapProgress}% complete (${this._bootstrapStatus})`
+      );
+      Services.obs.notifyObservers(
+        {
+          progress: TorConnect._bootstrapProgress,
+          status: TorConnect._bootstrapStatus,
+          hasWarnings: TorConnect._logHasWarningOrError,
+        },
+        TorConnectTopics.BootstrapProgress
+      );
+    },
+
+    // init should be called by TorStartupService
+    init() {
+      console.log("TorConnect: init()");
+      this._callback(TorConnectState.Initial).begin();
+
+      if (!TorMonitorService.ownsTorDaemon) {
+        // Disabled
+        this._changeState(TorConnectState.Disabled);
+      } else {
+        let observeTopic = addTopic => {
+          Services.obs.addObserver(this, addTopic);
+          console.log(`TorConnect: Observing topic '${addTopic}'`);
+        };
+
+        // register the Tor topics we always care about
+        observeTopic(TorTopics.ProcessExited);
+        observeTopic(TorTopics.LogHasWarnOrErr);
+        observeTopic(TorSettingsTopics.Ready);
+      }
+    },
+
+    async observe(subject, topic, data) {
+      console.log(`TorConnect: Observed ${topic}`);
+
+      switch (topic) {
+        /* We need to wait until TorSettings have been loaded and applied before we can Quickstart */
+        case TorSettingsTopics.Ready: {
+          if (this.shouldQuickStart) {
+            // Quickstart
+            this._changeState(TorConnectState.Bootstrapping);
+          } else {
+            // Configuring
+            this._changeState(TorConnectState.Configuring);
+          }
+          break;
+        }
+        case TorTopics.LogHasWarnOrErr: {
+          this._logHasWarningOrError = true;
+          break;
+        }
+        default:
+          // ignore
+          break;
+      }
+    },
+
+    /*
+        Various getters
+        */
+
+    get shouldShowTorConnect() {
+      // TorBrowser must control the daemon
+      return (
+        TorMonitorService.ownsTorDaemon &&
+        // if we have succesfully bootstraped, then no need to show TorConnect
+        this.state !== TorConnectState.Bootstrapped
+      );
+    },
+
+    get shouldQuickStart() {
+      // quickstart must be enabled
+      return (
+        TorSettings.quickstart.enabled &&
+        // and the previous bootstrap attempt must have succeeded
+        !Services.prefs.getBoolPref(TorLauncherPrefs.prompt_at_startup, true)
+      );
+    },
+
+    get state() {
+      return this._state;
+    },
+
+    get bootstrapProgress() {
+      return this._bootstrapProgress;
+    },
+
+    get bootstrapStatus() {
+      return this._bootstrapStatus;
+    },
+
+    get internetStatus() {
+      return this._internetStatus;
+    },
+
+    get countryCodes() {
+      return this._countryCodes;
+    },
+
+    get countryNames() {
+      return this._countryNames;
+    },
+
+    get detectedLocation() {
+      return this._detectedLocation;
+    },
+
+    get errorMessage() {
+      return this._errorMessage;
+    },
+
+    get errorDetails() {
+      return this._errorDetails;
+    },
+
+    get logHasWarningOrError() {
+      return this._logHasWarningOrError;
+    },
+
+    get hasBootstrapEverFailed() {
+      return this._hasBootstrapEverFailed;
+    },
+
+    get uiState() {
+      return this._uiState;
+    },
+    set uiState(newState) {
+      this._uiState = newState;
+    },
+
+    /*
+        These functions allow external consumers to tell TorConnect to transition states
+        */
+
+    beginBootstrap() {
+      console.log("TorConnect: beginBootstrap()");
+      this._changeState(TorConnectState.Bootstrapping);
+    },
+
+    cancelBootstrap() {
+      console.log("TorConnect: cancelBootstrap()");
+      this._changeState(TorConnectState.Configuring);
+    },
+
+    beginAutoBootstrap(countryCode) {
+      console.log("TorConnect: beginAutoBootstrap()");
+      this._changeState(TorConnectState.AutoBootstrapping, countryCode);
+    },
+
+    cancelAutoBootstrap() {
+      console.log("TorConnect: cancelAutoBootstrap()");
+      this._changeState(TorConnectState.Configuring);
+    },
+
+    /*
+        Further external commands and helper methods
+        */
+    openTorPreferences() {
+      const win = BrowserWindowTracker.getTopWindow();
+      win.switchToTabHavingURI("about:preferences#connection", true);
+    },
+
+    openTorConnect() {
+      const win = BrowserWindowTracker.getTopWindow();
+      win.switchToTabHavingURI("about:torconnect", true, {
+        ignoreQueryString: true,
+      });
+    },
+
+    viewTorLogs() {
+      const win = BrowserWindowTracker.getTopWindow();
+      win.switchToTabHavingURI("about:preferences#connection-viewlogs", true);
+    },
+
+    async getCountryCodes() {
+      // Difference with the getter: this is to be called by TorConnectParent, and downloads
+      // the country codes if they are not already in cache.
+      if (this._countryCodes.length) {
+        return this._countryCodes;
+      }
+      const mrpc = new MoatRPC();
+      try {
+        await mrpc.init();
+        this._countryCodes = await mrpc.circumvention_countries();
+      } catch (err) {
+        console.log("An error occurred while fetching country codes", err);
+      } finally {
+        mrpc.uninit();
+      }
+      return this._countryCodes;
+    },
+
+    getRedirectURL(url) {
+      return `about:torconnect?redirect=${encodeURIComponent(url)}`;
+    },
+
+    // called from browser.js on browser startup, passed in either the user's homepage(s)
+    // or uris passed via command-line; we want to replace them with about:torconnect uris
+    // which redirect after bootstrapping
+    getURIsToLoad(uriVariant) {
+      // convert the object we get from browser.js
+      let uriStrings = (v => {
+        // an interop array
+        if (v instanceof Ci.nsIArray) {
+          // Transform the nsIArray of nsISupportsString's into a JS Array of
+          // JS strings.
+          return Array.from(
+            v.enumerate(Ci.nsISupportsString),
+            supportStr => supportStr.data
+          );
+          // an interop string
+        } else if (v instanceof Ci.nsISupportsString) {
+          return [v.data];
+          // a js string
+        } else if (typeof v === "string") {
+          return v.split("|");
+          // a js array of js strings
+        } else if (
+          Array.isArray(v) &&
+          v.reduce((allStrings, entry) => {
+            return allStrings && typeof entry === "string";
+          }, true)
+        ) {
+          return v;
+        }
+        // about:tor as safe fallback
+        console.log(
+          `TorConnect: getURIsToLoad() received unknown variant '${JSON.stringify(
+            v
+          )}'`
+        );
+        return ["about:tor"];
+      })(uriVariant);
+
+      // will attempt to convert user-supplied string to a uri, fallback to about:tor if cannot convert
+      // to valid uri object
+      let uriStringToUri = uriString => {
+        const fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE;
+        let uri = Services.uriFixup.getFixupURIInfo(uriString, fixupFlags)
+          .preferredURI;
+        return uri ? uri : Services.io.newURI("about:tor");
+      };
+      let uris = uriStrings.map(uriStringToUri);
+
+      // assume we have a valid uri and generate an about:torconnect redirect uri
+      let redirectUrls = uris.map(uri => this.getRedirectURL(uri.spec));
+
+      console.log(
+        `TorConnect: Will load after bootstrap => [${uris
+          .map(uri => {
+            return uri.spec;
+          })
+          .join(", ")}]`
+      );
+      return redirectUrls;
+    },
+  };
+  return retval;
+})(); /* TorConnect */
diff --git a/browser/modules/TorSettings.jsm b/browser/modules/TorSettings.jsm
new file mode 100644
index 000000000000..97f1d07a5fef
--- /dev/null
+++ b/browser/modules/TorSettings.jsm
@@ -0,0 +1,782 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "TorSettings",
+  "TorSettingsTopics",
+  "TorSettingsData",
+  "TorBridgeSource",
+  "TorBuiltinBridgeTypes",
+  "TorProxyType",
+];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { TorMonitorService } = ChromeUtils.import(
+  "resource://gre/modules/TorMonitorService.jsm"
+);
+const { TorProtocolService } = ChromeUtils.import(
+  "resource://gre/modules/TorProtocolService.jsm"
+);
+
+/* tor-launcher observer topics */
+const TorTopics = Object.freeze({
+  ProcessIsReady: "TorProcessIsReady",
+});
+
+/* TorSettings observer topics */
+const TorSettingsTopics = Object.freeze({
+  Ready: "torsettings:ready",
+  SettingChanged: "torsettings:setting-changed",
+});
+
+/* TorSettings observer data (for SettingChanged topic) */
+const TorSettingsData = Object.freeze({
+  QuickStartEnabled: "torsettings:quickstart_enabled",
+});
+
+/* Prefs used to store settings in TorBrowser prefs */
+const TorSettingsPrefs = Object.freeze({
+  /* bool: are we pulling tor settings from the preferences */
+  enabled: "torbrowser.settings.enabled",
+  quickstart: {
+    /* bool: does tor connect automatically on launch */
+    enabled: "torbrowser.settings.quickstart.enabled",
+  },
+  bridges: {
+    /* bool:  does tor use bridges */
+    enabled: "torbrowser.settings.bridges.enabled",
+    /* int: -1=invalid|0=builtin|1=bridge_db|2=user_provided */
+    source: "torbrowser.settings.bridges.source",
+    /* string: obfs4|meek_azure|snowflake|etc */
+    builtin_type: "torbrowser.settings.bridges.builtin_type",
+    /* preference branch: each child branch should be a bridge string */
+    bridge_strings: "torbrowser.settings.bridges.bridge_strings",
+  },
+  proxy: {
+    /* bool: does tor use a proxy */
+    enabled: "torbrowser.settings.proxy.enabled",
+    /* -1=invalid|0=socks4,1=socks5,2=https */
+    type: "torbrowser.settings.proxy.type",
+    /* string: proxy server address */
+    address: "torbrowser.settings.proxy.address",
+    /* int: [1,65535], proxy port */
+    port: "torbrowser.settings.proxy.port",
+    /* string: username */
+    username: "torbrowser.settings.proxy.username",
+    /* string: password */
+    password: "torbrowser.settings.proxy.password",
+  },
+  firewall: {
+    /* bool: does tor have a port allow list */
+    enabled: "torbrowser.settings.firewall.enabled",
+    /* string: comma-delimitted list of port numbers */
+    allowed_ports: "torbrowser.settings.firewall.allowed_ports",
+  },
+});
+
+/* Legacy tor-launcher prefs and pref branches*/
+const TorLauncherPrefs = Object.freeze({
+  quickstart: "extensions.torlauncher.quickstart",
+  default_bridge_type: "extensions.torlauncher.default_bridge_type",
+  default_bridge: "extensions.torlauncher.default_bridge.",
+  default_bridge_recommended_type:
+    "extensions.torlauncher.default_bridge_recommended_type",
+  bridgedb_bridge: "extensions.torlauncher.bridgedb_bridge.",
+});
+
+/* Config Keys used to configure tor daemon */
+const TorConfigKeys = Object.freeze({
+  useBridges: "UseBridges",
+  bridgeList: "Bridge",
+  socks4Proxy: "Socks4Proxy",
+  socks5Proxy: "Socks5Proxy",
+  socks5ProxyUsername: "Socks5ProxyUsername",
+  socks5ProxyPassword: "Socks5ProxyPassword",
+  httpsProxy: "HTTPSProxy",
+  httpsProxyAuthenticator: "HTTPSProxyAuthenticator",
+  reachableAddresses: "ReachableAddresses",
+  clientTransportPlugin: "ClientTransportPlugin",
+});
+
+const TorBridgeSource = Object.freeze({
+  Invalid: -1,
+  BuiltIn: 0,
+  BridgeDB: 1,
+  UserProvided: 2,
+});
+
+const TorProxyType = Object.freeze({
+  Invalid: -1,
+  Socks4: 0,
+  Socks5: 1,
+  HTTPS: 2,
+});
+
+const TorBuiltinBridgeTypes = Object.freeze(
+  (() => {
+    const bridgeListBranch = Services.prefs.getBranch(
+      TorLauncherPrefs.default_bridge
+    );
+    const bridgePrefs = bridgeListBranch.getChildList("");
+
+    // an unordered set for shoving bridge types into
+    const bridgeTypes = new Set();
+    // look for keys ending in ".N" and treat string before that as the bridge type
+    const pattern = /\.[0-9]+$/;
+    for (const key of bridgePrefs) {
+      const offset = key.search(pattern);
+      if (offset != -1) {
+        const bt = key.substring(0, offset);
+        bridgeTypes.add(bt);
+      }
+    }
+
+    // recommended bridge type goes first in the list
+    const recommendedBridgeType = Services.prefs.getCharPref(
+      TorLauncherPrefs.default_bridge_recommended_type,
+      null
+    );
+
+    const retval = [];
+    if (recommendedBridgeType && bridgeTypes.has(recommendedBridgeType)) {
+      retval.push(recommendedBridgeType);
+    }
+
+    for (const bridgeType of bridgeTypes.values()) {
+      if (bridgeType != recommendedBridgeType) {
+        retval.push(bridgeType);
+      }
+    }
+    return retval;
+  })()
+);
+
+/* 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)
+// we strip out the 'bridge' prefix here
+const parseBridgeStrings = function(aBridgeStrings) {
+  // replace carriage returns ('\r') with new lines ('\n')
+  aBridgeStrings = aBridgeStrings.replace(/\r/g, "\n");
+  // then replace contiguous new lines ('\n') with a single one
+  aBridgeStrings = aBridgeStrings.replace(/[\n]+/g, "\n");
+
+  // split on the newline and for each bridge string: trim, remove starting 'bridge' string
+  // finally discard entries that are empty strings; empty strings could occur if we receive
+  // a new line containing only whitespace
+  const splitStrings = aBridgeStrings.split("\n");
+  return splitStrings
+    .map(val => val.trim().replace(/^bridge\s+/i, ""))
+    .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 [];
+  }
+
+  const bridgeBranch = Services.prefs.getBranch(
+    TorLauncherPrefs.default_bridge
+  );
+  const bridgeBranchPrefs = bridgeBranch.getChildList("");
+  const retval = [];
+
+  // regex matches against strings ending in ".N" where N is a positive integer
+  const pattern = /\.[0-9]+$/;
+  for (const key of bridgeBranchPrefs) {
+    // verify the location of the match is the correct offset required for aBridgeType
+    // to fit, and that the string begins with aBridgeType
+    if (
+      key.search(pattern) == builtinType.length &&
+      key.startsWith(builtinType)
+    ) {
+      const bridgeStr = bridgeBranch.getCharPref(key);
+      retval.push(bridgeStr);
+    }
+  }
+
+  // shuffle so that Tor Browser users don't all try the built-in bridges in the same order
+  arrayShuffle(retval);
+
+  return retval;
+};
+
+/* Helper methods */
+
+const arrayShuffle = function(array) {
+  // fisher-yates shuffle
+  for (let i = array.length - 1; i > 0; --i) {
+    // number n such that 0.0 <= n < 1.0
+    const n = Math.random();
+    // integer j such that 0 <= j <= i
+    const j = Math.floor(n * (i + 1));
+
+    // swap values at indices i and j
+    const tmp = array[i];
+    array[i] = array[j];
+    array[j] = tmp;
+  }
+};
+
+const arrayCopy = function(array) {
+  return [].concat(array);
+};
+
+/* TorSettings module */
+
+const TorSettings = (() => {
+  const self = {
+    _settings: null,
+
+    // tor daemon related settings
+    defaultSettings() {
+      const settings = {
+        quickstart: {
+          enabled: false,
+        },
+        bridges: {
+          enabled: false,
+          source: TorBridgeSource.Invalid,
+          builtin_type: null,
+          bridge_strings: [],
+        },
+        proxy: {
+          enabled: false,
+          type: TorProxyType.Invalid,
+          address: null,
+          port: 0,
+          username: null,
+          password: null,
+        },
+        firewall: {
+          enabled: false,
+          allowed_ports: [],
+        },
+      };
+      return settings;
+    },
+
+    /* load or init our settings, and register observers */
+    init() {
+      if (TorMonitorService.ownsTorDaemon) {
+        // if the settings branch exists, load settings from prefs
+        if (Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)) {
+          this.loadFromPrefs();
+        } else {
+          // otherwise load defaults
+          this._settings = this.defaultSettings();
+        }
+        Services.obs.addObserver(this, TorTopics.ProcessIsReady);
+
+        if (TorMonitorService.isRunning) {
+          handleProcessReady();
+        }
+      }
+    },
+
+    /* wait for relevant life-cycle events to apply saved settings */
+    async observe(subject, topic, data) {
+      console.log(`TorSettings: Observed ${topic}`);
+
+      // once the tor daemon is ready, we need to apply our settings
+      const handleProcessReady = async () => {
+        // push down settings to tor
+        await this.applySettings();
+        console.log("TorSettings: Ready");
+        Services.obs.notifyObservers(null, TorSettingsTopics.Ready);
+      };
+
+      switch (topic) {
+        case TorTopics.ProcessIsReady:
+          Services.obs.removeObserver(this, TorTopics.ProcessIsReady);
+          await handleProcessReady();
+          break;
+      }
+    },
+
+    // load our settings from prefs
+    loadFromPrefs() {
+      console.log("TorSettings: loadFromPrefs()");
+
+      const settings = this.defaultSettings();
+
+      /* Quickstart */
+      settings.quickstart.enabled = Services.prefs.getBoolPref(
+        TorSettingsPrefs.quickstart.enabled
+      );
+      /* Bridges */
+      settings.bridges.enabled = Services.prefs.getBoolPref(
+        TorSettingsPrefs.bridges.enabled
+      );
+      settings.bridges.source = Services.prefs.getIntPref(
+        TorSettingsPrefs.bridges.source,
+        TorBridgeSource.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
+          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
+      );
+      if (settings.proxy.enabled) {
+        settings.proxy.type = Services.prefs.getIntPref(
+          TorSettingsPrefs.proxy.type
+        );
+        settings.proxy.address = Services.prefs.getStringPref(
+          TorSettingsPrefs.proxy.address
+        );
+        settings.proxy.port = Services.prefs.getIntPref(
+          TorSettingsPrefs.proxy.port
+        );
+        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
+      );
+      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;
+    },
+
+    // save our settings to prefs
+    saveToPrefs() {
+      console.log("TorSettings: saveToPrefs()");
+
+      const settings = this._settings;
+
+      /* Quickstart */
+      Services.prefs.setBoolPref(
+        TorSettingsPrefs.quickstart.enabled,
+        settings.quickstart.enabled
+      );
+      /* Bridges */
+      Services.prefs.setBoolPref(
+        TorSettingsPrefs.bridges.enabled,
+        settings.bridges.enabled
+      );
+      Services.prefs.setIntPref(
+        TorSettingsPrefs.bridges.source,
+        settings.bridges.source
+      );
+      Services.prefs.setStringPref(
+        TorSettingsPrefs.bridges.builtin_type,
+        settings.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}`
+        );
+      });
+      // 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
+      );
+      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
+      );
+      if (settings.firewall.enabled) {
+        Services.prefs.setStringPref(
+          TorSettingsPrefs.firewall.allowed_ports,
+          settings.firewall.allowed_ports.join(",")
+        );
+      } else {
+        Services.prefs.clearUserPref(TorSettingsPrefs.firewall.allowed_ports);
+      }
+
+      // 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);
+      }
+
+      /* 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;
+        }
+      }
+
+      /* 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);
+      }
+
+      /* Push to Tor */
+      await TorProtocolService.writeSettings(settingsMap);
+
+      return this;
+    },
+
+    // set all of our settings at once from a settings object
+    setSettings(settings) {
+      console.log("TorSettings: setSettings()");
+      const backup = this.getSettings();
+
+      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
+            );
+            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;
+        }
+
+        // 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
+            );
+          }
+        },
+      };
+    },
+
+    // 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);
+        },
+      };
+    },
+
+    // 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;
+        },
+      };
+    },
+
+    // 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;
+})();
diff --git a/browser/modules/moz.build b/browser/modules/moz.build
index dc73d9fbccdd..c5d6bb08e9e9 100644
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -121,6 +121,7 @@ EXTRA_JS_MODULES += [
     "AboutNewTab.jsm",
     "AppUpdater.jsm",
     "AsyncTabSwitcher.jsm",
+    "BridgeDB.jsm",
     "BrowserUIUtils.jsm",
     "BrowserUsageTelemetry.jsm",
     "BrowserWindowTracker.jsm",
@@ -131,6 +132,7 @@ EXTRA_JS_MODULES += [
     "FaviconLoader.jsm",
     "HomePage.jsm",
     "LaterRun.jsm",
+    'Moat.jsm',
     "NewTabPagePreloading.jsm",
     "OpenInTabsUtils.jsm",
     "PageActions.jsm",
@@ -144,6 +146,8 @@ EXTRA_JS_MODULES += [
     "SitePermissions.jsm",
     "TabsList.jsm",
     "TabUnloader.jsm",
+    "TorConnect.jsm",
+    "TorSettings.jsm",
     "TransientPrefs.jsm",
     "webrtcUI.jsm",
     "ZoomUI.jsm",
diff --git a/toolkit/components/tor-launcher/TorStartupService.jsm b/toolkit/components/tor-launcher/TorStartupService.jsm
index 31a82d4c9510..c1bbfda19ea3 100644
--- a/toolkit/components/tor-launcher/TorStartupService.jsm
+++ b/toolkit/components/tor-launcher/TorStartupService.jsm
@@ -22,6 +22,17 @@ ChromeUtils.defineModuleGetter(
   "resource://gre/modules/TorProtocolService.jsm"
 );
 
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorConnect",
+  "resource:///modules/TorConnect.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorSettings",
+  "resource:///modules/TorSettings.jsm"
+);
+
 /* Browser observer topis */
 const BrowserTopics = Object.freeze({
   ProfileAfterChange: "profile-after-change",
@@ -53,6 +64,9 @@ class TorStartupService {
     await TorProtocolService.init();
     TorMonitorService.init();
 
+    TorSettings.init();
+    TorConnect.init();
+
     gInited = true;
   }
 

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


More information about the tbb-commits mailing list