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

gitolite role git at cupani.torproject.org
Tue Aug 9 10:53:23 UTC 2022


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

pierov pushed a commit to branch tor-browser-102.0.1-12.0-1
in repository tor-browser.

commit 17fc9bc27dcdcfb75fcc7deda337b2f6969a6328
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/components/sessionstore/SessionStore.jsm   |   4 +
 browser/modules/BridgeDB.jsm                       |  61 ++
 browser/modules/Moat.jsm                           | 814 +++++++++++++++++++
 browser/modules/TorConnect.jsm                     | 901 +++++++++++++++++++++
 browser/modules/TorProtocolService.jsm             | 502 ++++++++++++
 browser/modules/TorSettings.jsm                    | 674 +++++++++++++++
 browser/modules/moz.build                          |   4 +
 .../processsingleton/MainProcessSingleton.jsm      |   5 +
 8 files changed, 2965 insertions(+)

diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm
index 4611737ca9180..46f07c2a11357 100644
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -234,6 +234,10 @@ ChromeUtils.defineModuleGetter(
   "resource://gre/modules/sessionstore/SessionHistory.jsm"
 );
 
+const { TorProtocolService } = ChromeUtils.import(
+    "resource:///modules/TorProtocolService.jsm"
+);
+
 XPCOMUtils.defineLazyServiceGetters(this, {
   gScreenManager: ["@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"],
 });
diff --git a/browser/modules/BridgeDB.jsm b/browser/modules/BridgeDB.jsm
new file mode 100644
index 0000000000000..50665710ebf4c
--- /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.log(`error : ${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 0000000000000..90a6ae4e521cc
--- /dev/null
+++ b/browser/modules/Moat.jsm
@@ -0,0 +1,814 @@
+"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://torlauncher/modules/tl-util.jsm"
+);
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///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);
+      let meekHelperProfileDir = TorLauncherUtil.getTorFile(
+        "pt-profiles-dir",
+        true
+      );
+      ptStateDir.append("pt_state"); // Match what tor uses.
+      meekHelperProfileDir.appendRelativePath("profile.moat-http-helper");
+
+      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,
+        TOR_BROWSER_MEEK_PROFILE: meekHelperProfileDir.path,
+      };
+      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 0000000000000..0edbb7d70c47f
--- /dev/null
+++ b/browser/modules/TorConnect.jsm
@@ -0,0 +1,901 @@
+"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 { TorProtocolService, TorProcessStatus, TorTopics, TorBootstrapRequest } = ChromeUtils.import(
+    "resource:///modules/TorProtocolService.jsm"
+);
+
+const { TorLauncherUtil } = ChromeUtils.import(
+    "resource://torlauncher/modules/tl-util.jsm"
+);
+
+const { TorSettings, TorSettingsTopics, TorBridgeSource, TorBuiltinBridgeTypes, TorProxyType } = ChromeUtils.import(
+    "resource:///modules/TorSettings.jsm"
+);
+
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm");
+
+/* Browser observer topis */
+const BrowserTopics = Object.freeze({
+    ProfileAfterChange: "profile-after-change",
+});
+
+/* 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",
+});
+
+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._status = InternetStatus.Unknown;
+        this._error = null;
+        this._pending = false;
+        this._timeout = setTimeout(() => {
+            this._timeout = null;
+            this.test();
+        }, this.timeoutRand());
+        this.onResult = (online, date) => {}
+        this.onError = (err) => {};
+    }
+
+    test() {
+        if (this._pending) {
+            return;
+        }
+        this.cancel();
+        this._pending = true;
+
+        this._testAsync()
+            .then((status) => {
+                this._pending = false;
+                this._status = status.successful ? InternetStatus.Online : InternetStatus.Offline;
+                this.onResult(this.status, status.date);
+            })
+            .catch(error => {
+                this._error = error;
+                this._pending = false;
+                this.onError(error);
+            });
+    }
+
+    cancel() {
+        if (this._timeout !== null) {
+            clearTimeout(this._timeout);
+            this._timeout = null;
+        }
+    }
+
+    async _testAsync() {
+        // Callbacks for the Internet test are desirable, because we will be
+        // waiting both for the bootstrap, and for the Internet test.
+        // However, managing Moat with async/await is much easier as it avoids a
+        // callback hell, and it makes extra explicit that we are uniniting it.
+        const mrpc = new MoatRPC();
+        let status = null;
+        let error = null;
+        try {
+            await mrpc.init();
+            status = await mrpc.testInternetConnection();
+        } catch (err) {
+            console.error("Error while checking the Internet connection", err);
+            error = err;
+        } finally {
+            mrpc.uninit();
+        }
+        if (error !== null) {
+            throw error;
+        }
+        return status;
+    }
+
+    get status() {
+        return this._status;
+    }
+
+    get error() {
+        return this._error;
+    }
+
+    // We randomize the Internet test timeout to make fingerprinting it harder, at least a little bit...
+    timeoutRand() {
+        const offset = 30000;
+        const randRange = 5000;
+        return offset + randRange * (Math.random() * 2 - 1);
+    }
+}
+
+const TorConnect = (() => {
+    let retval = {
+
+        _state: TorConnectState.Initial,
+        _bootstrapProgress: 0,
+        _bootstrapStatus: null,
+        _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);
+                        TorProtocolService._torBootstrapDebugSetError();
+                        return;
+                    }
+
+                    const tbr = new TorBootstrapRequest();
+                    const internetTest = new InternetTest();
+
+                    let bootstrapError = "";
+                    let bootstrapErrorDetails = "";
+                    const maybeTransitionToError = () => {
+                        if (internetTest.status === InternetStatus.Unknown && internetTest.error === null) {
+                            // We have been called by a failed bootstrap, but the internet test has not run yet - force
+                            // it to run immediately!
+                            internetTest.test();
+                            // Return from this call, because the Internet test's callback will call us again
+                            return;
+                        }
+                        // Do not transition to the offline error until we are sure that also the bootstrap failed, in
+                        // case Moat is down but the bootstrap can proceed anyway.
+                        if (bootstrapError === "") {
+                            return;
+                        }
+                        if (internetTest.status === InternetStatus.Offline) {
+                            TorConnect._changeState(TorConnectState.Error, TorStrings.torConnect.offline, "", true);
+                        } else {
+                            // Give priority to the bootstrap error, in case the Internet test fails
+                            TorConnect._hasBootstrapEverFailed = true;
+                            TorConnect._changeState(TorConnectState.Error, bootstrapError, bootstrapErrorDetails, true);
+                        }
+                    }
+
+                    this.on_transition = async (nextState) => {
+                        if (nextState === TorConnectState.Configuring) {
+                            // stop bootstrap process if user cancelled
+                            internetTest.cancel();
+                            await tbr.cancel();
+                        }
+                        resolve();
+                    };
+
+                    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;
+                            }
+                            TorProtocolService._torBootstrapDebugSetError();
+                        }
+                    }
+
+                    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 > 0) {
+                            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(err) {
+                                console.log(`TorConnect: Failed to restore original settings => ${err}`);
+                            }
+                            // 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: function(state) {
+            return this._stateCallbacks.get(state);
+        },
+
+        _changeState: function(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: function(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 on app-startup in MainProcessingSingleton.jsm
+        init: function() {
+            console.log("TorConnect: init()");
+
+            // delay remaining init until after profile-after-change
+            Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange);
+
+            this._callback(TorConnectState.Initial).begin();
+        },
+
+        observe: async function(subject, topic, data) {
+            console.log(`TorConnect: Observed ${topic}`);
+
+            switch(topic) {
+
+            /* Determine which state to move to from Initial */
+            case BrowserTopics.ProfileAfterChange: {
+                if (TorLauncherUtil.useLegacyLauncher || !TorProtocolService.ownsTorDaemon) {
+                    // Disabled
+                    this._changeState(TorConnectState.Disabled);
+                } else {
+                    let observeTopic = (topic) => {
+                        Services.obs.addObserver(this, topic);
+                        console.log(`TorConnect: Observing topic '${topic}'`);
+                    };
+
+                   // register the Tor topics we always care about
+                   observeTopic(TorTopics.ProcessExited);
+                   observeTopic(TorTopics.LogHasWarnOrErr);
+                   observeTopic(TorSettingsTopics.Ready);
+                }
+                Services.obs.removeObserver(this, topic);
+                break;
+            }
+            /* 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 (TorProtocolService.ownsTorDaemon &&
+                   // and we're not using the legacy launcher
+                   !TorLauncherUtil.useLegacyLauncher &&
+                   // 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: function() {
+            console.log("TorConnect: beginBootstrap()");
+            this._changeState(TorConnectState.Bootstrapping);
+        },
+
+        cancelBootstrap: function() {
+            console.log("TorConnect: cancelBootstrap()");
+            this._changeState(TorConnectState.Configuring);
+        },
+
+        beginAutoBootstrap: function(countryCode) {
+            console.log("TorConnect: beginAutoBootstrap()");
+            this._changeState(TorConnectState.AutoBootstrapping, countryCode);
+        },
+
+        cancelAutoBootstrap: function() {
+            console.log("TorConnect: cancelAutoBootstrap()");
+            this._changeState(TorConnectState.Configuring);
+        },
+
+        /*
+        Further external commands and helper methods
+        */
+        openTorPreferences: function() {
+            const win = BrowserWindowTracker.getTopWindow();
+            win.switchToTabHavingURI("about:preferences#connection", true);
+        },
+
+        openTorConnect: function() {
+            const win = BrowserWindowTracker.getTopWindow();
+            win.switchToTabHavingURI("about:torconnect", true, {ignoreQueryString: true});
+        },
+
+        viewTorLogs: function() {
+            const win = BrowserWindowTracker.getTopWindow();
+            win.switchToTabHavingURI("about:preferences#connection-viewlogs", true);
+        },
+
+        getCountryCodes: async function() {
+            // 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: function(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: function(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;
+        },
+    };
+    retval.init();
+    return retval;
+})(); /* TorConnect */
diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm
new file mode 100644
index 0000000000000..aa27e13e11711
--- /dev/null
+++ b/browser/modules/TorProtocolService.jsm
@@ -0,0 +1,502 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "TorProtocolService",
+  "TorProcessStatus",
+  "TorTopics",
+  "TorBootstrapRequest",
+];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { setTimeout, clearTimeout } = ChromeUtils.import(
+  "resource://gre/modules/Timer.jsm"
+);
+
+const { TorLauncherUtil } = ChromeUtils.import(
+  "resource://torlauncher/modules/tl-util.jsm"
+);
+
+// see tl-process.js
+const TorProcessStatus = Object.freeze({
+  Unknown: 0,
+  Starting: 1,
+  Running: 2,
+  Exited: 3,
+});
+
+/* tor-launcher observer topics */
+const TorTopics = Object.freeze({
+  BootstrapStatus: "TorBootstrapStatus",
+  BootstrapError: "TorBootstrapError",
+  ProcessExited: "TorProcessExited",
+  LogHasWarnOrErr: "TorLogHasWarnOrErr",
+});
+
+/* Browser observer topis */
+const BrowserTopics = Object.freeze({
+  ProfileAfterChange: "profile-after-change",
+});
+
+var TorProtocolService = {
+  _TorLauncherProtocolService: null,
+  _TorProcessService: null,
+
+  // maintain a map of tor settings set by Tor Browser so that we don't
+  // repeatedly set the same key/values over and over
+  // this map contains string keys to primitive or array values
+  _settingsCache: new Map(),
+
+  init() {
+    Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange);
+  },
+
+  observe(subject, topic, data) {
+    if (topic === BrowserTopics.ProfileAfterChange) {
+      // we have to delay init'ing this or else the crypto service inits too early without a profile
+      // which breaks the password manager
+      this._TorLauncherProtocolService = Cc[
+        "@torproject.org/torlauncher-protocol-service;1"
+      ].getService(Ci.nsISupports).wrappedJSObject;
+      this._TorProcessService = Cc[
+        "@torproject.org/torlauncher-process-service;1"
+      ].getService(Ci.nsISupports).wrappedJSObject;
+      Services.obs.removeObserver(this, topic);
+    }
+  },
+
+  _typeof(aValue) {
+    switch (typeof aValue) {
+      case "boolean":
+        return "boolean";
+      case "string":
+        return "string";
+      case "object":
+        if (aValue == null) {
+          return "null";
+        } else if (Array.isArray(aValue)) {
+          return "array";
+        }
+        return "object";
+    }
+    return "unknown";
+  },
+
+  _assertValidSettingKey(aSetting) {
+    // ensure the 'key' is a string
+    if (typeof aSetting != "string") {
+      throw new Error(
+        `Expected setting of type string but received ${typeof aSetting}`
+      );
+    }
+  },
+
+  _assertValidSetting(aSetting, aValue) {
+    this._assertValidSettingKey(aSetting);
+
+    const valueType = this._typeof(aValue);
+    switch (valueType) {
+      case "boolean":
+      case "string":
+      case "null":
+        return;
+      case "array":
+        for (const element of aValue) {
+          if (typeof element != "string") {
+            throw new Error(
+              `Setting '${aSetting}' array contains value of invalid type '${typeof element}'`
+            );
+          }
+        }
+        return;
+      default:
+        throw new Error(
+          `Invalid object type received for setting '${aSetting}'`
+        );
+    }
+  },
+
+  // takes a Map containing tor settings
+  // throws on error
+  async writeSettings(aSettingsObj) {
+    // only write settings that have changed
+    let newSettings = new Map();
+    for (const [setting, value] of aSettingsObj) {
+      let saveSetting = false;
+
+      // make sure we have valid data here
+      this._assertValidSetting(setting, value);
+
+      if (!this._settingsCache.has(setting)) {
+        // no cached setting, so write
+        saveSetting = true;
+      } else {
+        const cachedValue = this._settingsCache.get(setting);
+        if (value != cachedValue) {
+          // compare arrays member-wise
+          if (Array.isArray(value) && Array.isArray(cachedValue)) {
+            if (value.length != cachedValue.length) {
+              saveSetting = true;
+            } else {
+              const arrayLength = value.length;
+              for (let i = 0; i < arrayLength; ++i) {
+                if (value[i] != cachedValue[i]) {
+                  saveSetting = true;
+                  break;
+                }
+              }
+            }
+          } else {
+            // some other different values
+            saveSetting = true;
+          }
+        }
+      }
+
+      if (saveSetting) {
+        newSettings.set(setting, value);
+      }
+    }
+
+    // only write if new setting to save
+    if (newSettings.size > 0) {
+      // convert settingsObject map to js object for torlauncher-protocol-service
+      let settingsObject = {};
+      for (const [setting, value] of newSettings) {
+        settingsObject[setting] = value;
+      }
+
+      let errorObject = {};
+      if (
+        !(await this._TorLauncherProtocolService.TorSetConfWithReply(
+          settingsObject,
+          errorObject
+        ))
+      ) {
+        throw new Error(errorObject.details);
+      }
+
+      // save settings to cache after successfully writing to Tor
+      for (const [setting, value] of newSettings) {
+        this._settingsCache.set(setting, value);
+      }
+    }
+  },
+
+  async _readSetting(aSetting) {
+    this._assertValidSettingKey(aSetting);
+    let reply = await this._TorLauncherProtocolService.TorGetConf(aSetting);
+    if (this._TorLauncherProtocolService.TorCommandSucceeded(reply)) {
+      return reply.lineArray;
+    }
+    throw new Error(reply.lineArray.join("\n"));
+  },
+
+  async _readBoolSetting(aSetting) {
+    let lineArray = await this._readSetting(aSetting);
+    if (lineArray.length != 1) {
+      throw new Error(
+        `Expected an array with length 1 but received array of length ${lineArray.length}`
+      );
+    }
+
+    let retval = lineArray[0];
+    switch (retval) {
+      case "0":
+        return false;
+      case "1":
+        return true;
+      default:
+        throw new Error(`Expected boolean (1 or 0) but received '${retval}'`);
+    }
+  },
+
+  async _readStringSetting(aSetting) {
+    let lineArray = await this._readSetting(aSetting);
+    if (lineArray.length != 1) {
+      throw new Error(
+        `Expected an array with length 1 but received array of length ${lineArray.length}`
+      );
+    }
+    return lineArray[0];
+  },
+
+  async _readStringArraySetting(aSetting) {
+    let lineArray = await this._readSetting(aSetting);
+    return lineArray;
+  },
+
+  async readBoolSetting(aSetting) {
+    let value = await this._readBoolSetting(aSetting);
+    this._settingsCache.set(aSetting, value);
+    return value;
+  },
+
+  async readStringSetting(aSetting) {
+    let value = await this._readStringSetting(aSetting);
+    this._settingsCache.set(aSetting, value);
+    return value;
+  },
+
+  async readStringArraySetting(aSetting) {
+    let value = await this._readStringArraySetting(aSetting);
+    this._settingsCache.set(aSetting, value);
+    return value;
+  },
+
+  // writes current tor settings to disk
+  async flushSettings() {
+    await this.sendCommand("SAVECONF");
+  },
+
+  getLog(countObj) {
+    countObj = countObj || { value: 0 };
+    let torLog = this._TorLauncherProtocolService.TorGetLog(countObj);
+    return torLog;
+  },
+
+  // true if we launched and control tor, false if using system tor
+  get ownsTorDaemon() {
+    return TorLauncherUtil.shouldStartAndOwnTor;
+  },
+
+  // Assumes `ownsTorDaemon` is true
+  isNetworkDisabled() {
+    const reply = TorProtocolService._TorLauncherProtocolService.TorGetConfBool(
+      "DisableNetwork",
+      true
+    );
+    if (
+      TorProtocolService._TorLauncherProtocolService.TorCommandSucceeded(reply)
+    ) {
+      return reply.retVal;
+    }
+    return true;
+  },
+
+  async enableNetwork() {
+    let settings = {};
+    settings.DisableNetwork = false;
+    let errorObject = {};
+    if (
+      !(await this._TorLauncherProtocolService.TorSetConfWithReply(
+        settings,
+        errorObject
+      ))
+    ) {
+      throw new Error(errorObject.details);
+    }
+  },
+
+  async sendCommand(cmd) {
+    return this._TorLauncherProtocolService.TorSendCommand(cmd);
+  },
+
+  retrieveBootstrapStatus() {
+    return this._TorLauncherProtocolService.TorRetrieveBootstrapStatus();
+  },
+
+  _GetSaveSettingsErrorMessage(aDetails) {
+    try {
+      return TorLauncherUtil.getSaveSettingsErrorMessage(aDetails);
+    } catch (e) {
+      console.log("GetSaveSettingsErrorMessage error", e);
+      return "Unexpected Error";
+    }
+  },
+
+  async setConfWithReply(settings) {
+    let result = false;
+    const error = {};
+    try {
+      result = await this._TorLauncherProtocolService.TorSetConfWithReply(
+        settings,
+        error
+      );
+    } catch (e) {
+      console.log("TorSetConfWithReply error", e);
+      error.details = this._GetSaveSettingsErrorMessage(e.message);
+    }
+    return { result, error };
+  },
+
+  isBootstrapDone() {
+    return this._TorProcessService.mIsBootstrapDone;
+  },
+
+  clearBootstrapError() {
+    return this._TorProcessService.TorClearBootstrapError();
+  },
+
+  torBootstrapErrorOccurred() {
+    return this._TorProcessService.TorBootstrapErrorOccurred;
+  },
+
+  _torBootstrapDebugSetError() {
+    this._TorProcessService._TorSetBootstrapErrorForDebug();
+  },
+
+  // Resolves to null if ok, or an error otherwise
+  async connect() {
+    const kTorConfKeyDisableNetwork = "DisableNetwork";
+    const settings = {};
+    settings[kTorConfKeyDisableNetwork] = false;
+    const { result, error } = await this.setConfWithReply(settings);
+    if (!result) {
+      return error;
+    }
+    try {
+      await this.sendCommand("SAVECONF");
+      this.clearBootstrapError();
+      this.retrieveBootstrapStatus();
+    } catch (e) {
+      return error;
+    }
+    return null;
+  },
+
+  torLogHasWarnOrErr() {
+    return this._TorLauncherProtocolService.TorLogHasWarnOrErr;
+  },
+
+  async torStopBootstrap() {
+    // Tell tor to disable use of the network; this should stop the bootstrap
+    // process.
+    const kErrorPrefix = "Setting DisableNetwork=1 failed: ";
+    try {
+      let settings = {};
+      settings.DisableNetwork = true;
+      const { result, error } = await this.setConfWithReply(settings);
+      if (!result) {
+        console.log(
+          `Error stopping bootstrap ${kErrorPrefix} ${error.details}`
+        );
+      }
+    } catch (e) {
+      console.log(`Error stopping bootstrap ${kErrorPrefix} ${e}`);
+    }
+    this.retrieveBootstrapStatus();
+  },
+
+  get torProcessStatus() {
+    if (this._TorProcessService) {
+      return this._TorProcessService.TorProcessStatus;
+    }
+    return TorProcessStatus.Unknown;
+  },
+};
+TorProtocolService.init();
+
+// modeled after XMLHttpRequest
+// nicely encapsulates the observer register/unregister logic
+class TorBootstrapRequest {
+  constructor() {
+    // number of ms to wait before we abandon the bootstrap attempt
+    // a value of 0 implies we never wait
+    this.timeout = 0;
+    // callbacks for bootstrap process status updates
+    this.onbootstrapstatus = (progress, status) => {};
+    this.onbootstrapcomplete = () => {};
+    this.onbootstraperror = (message, details) => {};
+
+    // internal resolve() method for bootstrap
+    this._bootstrapPromiseResolve = null;
+    this._bootstrapPromise = null;
+    this._timeoutID = null;
+  }
+
+  async observe(subject, topic, data) {
+    const obj = subject?.wrappedJSObject;
+    switch (topic) {
+      case TorTopics.BootstrapStatus: {
+        const progress = obj.PROGRESS;
+        const status = TorLauncherUtil.getLocalizedBootstrapStatus(obj, "TAG");
+        if (this.onbootstrapstatus) {
+          this.onbootstrapstatus(progress, status);
+        }
+        if (progress === 100) {
+          if (this.onbootstrapcomplete) {
+            this.onbootstrapcomplete();
+          }
+          this._bootstrapPromiseResolve(true);
+          clearTimeout(this._timeoutID);
+        }
+
+        break;
+      }
+      case TorTopics.BootstrapError: {
+        // first stop our bootstrap timeout before handling the error
+        clearTimeout(this._timeoutID);
+
+        await TorProtocolService.torStopBootstrap();
+
+        const message = obj.message;
+        const details = obj.details;
+        if (this.onbootstraperror) {
+          this.onbootstraperror(message, details);
+        }
+        this._bootstrapPromiseResolve(false);
+        break;
+      }
+    }
+  }
+
+  // resolves 'true' if bootstrap succeeds, false otherwise
+  async bootstrap() {
+    if (this._bootstrapPromise) {
+      return this._bootstrapPromise;
+    }
+
+    this._bootstrapPromise = new Promise(async (resolve, reject) => {
+      this._bootstrapPromiseResolve = resolve;
+
+      // register ourselves to listen for bootstrap events
+      Services.obs.addObserver(this, TorTopics.BootstrapStatus);
+      Services.obs.addObserver(this, TorTopics.BootstrapError);
+
+      // optionally cancel bootstrap after a given timeout
+      if (this.timeout > 0) {
+        this._timeoutID = setTimeout(async () => {
+          await TorProtocolService.torStopBootstrap();
+          if (this.onbootstraperror) {
+            this.onbootstraperror(
+              "Tor Bootstrap process timed out",
+              `Bootstrap attempt abandoned after waiting ${this.timeout} ms`
+            );
+          }
+          this._bootstrapPromiseResolve(false);
+        }, this.timeout);
+      }
+
+      // wait for bootstrapping to begin and maybe handle error
+      let err = await TorProtocolService.connect();
+      if (err) {
+        clearTimeout(this._timeoutID);
+        await TorProtocolService.torStopBootstrap();
+
+        const message = err.message;
+        const details = err.details;
+        if (this.onbootstraperror) {
+          this.onbootstraperror(message, details);
+        }
+        this._bootstrapPromiseResolve(false);
+      }
+    }).finally(() => {
+      // and remove ourselves once bootstrap is resolved
+      Services.obs.removeObserver(this, TorTopics.BootstrapStatus);
+      Services.obs.removeObserver(this, TorTopics.BootstrapError);
+    });
+
+    return this._bootstrapPromise;
+  }
+
+  async cancel() {
+    clearTimeout(this._timeoutID);
+
+    await TorProtocolService.torStopBootstrap();
+
+    this._bootstrapPromiseResolve(false);
+  }
+}
diff --git a/browser/modules/TorSettings.jsm b/browser/modules/TorSettings.jsm
new file mode 100644
index 0000000000000..41638f9cbd1b6
--- /dev/null
+++ b/browser/modules/TorSettings.jsm
@@ -0,0 +1,674 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorSettings", "TorSettingsTopics", "TorSettingsData", "TorBridgeSource", "TorBuiltinBridgeTypes", "TorProxyType"];
+
+const { Services } = ChromeUtils.import(
+    "resource://gre/modules/Services.jsm"
+);
+
+const { TorProtocolService, TorProcessStatus } = ChromeUtils.import(
+    "resource:///modules/TorProtocolService.jsm"
+);
+
+/* Browser observer topics */
+const BrowserTopics = Object.freeze({
+    ProfileAfterChange: "profile-after-change",
+});
+
+/* 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(
+    (() => {
+      let bridgeListBranch = Services.prefs.getBranch(TorLauncherPrefs.default_bridge);
+      let bridgePrefs = bridgeListBranch.getChildList("");
+
+      // an unordered set for shoving bridge types into
+      let 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
+      let recommendedBridgeType = Services.prefs.getCharPref(TorLauncherPrefs.default_bridge_recommended_type, null);
+
+      let 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
+let 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
+  let port = Number.parseInt(aPort);
+  if (port < 1 || port > 65535) {
+    return 0;
+  }
+
+  return port;
+};
+// expects a string in the format: "ADDRESS:PORT"
+let parseAddrPort = function(aAddrColonPort) {
+  let tokens = aAddrColonPort.split(":");
+  if (tokens.length != 2) {
+    return ["", 0];
+  }
+  let address = tokens[0];
+  let port = parsePort(tokens[1]);
+  return [address, port];
+};
+
+// expects a string in the format: "USERNAME:PASSWORD"
+// split on the first colon and any subsequent go into password
+let parseUsernamePassword = function(aUsernameColonPassword) {
+  let colonIndex = aUsernameColonPassword.indexOf(":");
+  if (colonIndex < 0) {
+    return ["", ""];
+  }
+
+  let username = aUsernameColonPassword.substring(0, colonIndex);
+  let password = aUsernameColonPassword.substring(colonIndex + 1);
+
+  return [username, password];
+};
+
+// expects a string in the format: ADDRESS:PORT,ADDRESS:PORT,...
+// returns array of ports (as ints)
+let parseAddrPortList = function(aAddrPortList) {
+  let addrPorts = aAddrPortList.split(",");
+  // parse ADDRESS:PORT string and only keep the port (second element in returned array)
+  let retval = addrPorts.map(addrPort => parseAddrPort(addrPort)[1]);
+  return retval;
+};
+
+// 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
+let 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
+  let 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
+let parsePortList = function(aPortListString) {
+  let splitStrings = aPortListString.split(",");
+  // parse and remove duplicates
+  let 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);
+};
+
+let getBuiltinBridgeStrings = function(builtinType) {
+    if (!builtinType) {
+        return [];
+    }
+
+    let bridgeBranch = Services.prefs.getBranch(TorLauncherPrefs.default_bridge);
+    let bridgeBranchPrefs = bridgeBranch.getChildList("");
+    let retval = [];
+
+    // regex matches against strings ending in ".N" where N is a positive integer
+    let 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)) {
+        let 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 */
+
+let 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;
+    }
+}
+
+let arrayCopy = function(array) {
+    return [].concat(array);
+}
+
+/* TorSettings module */
+
+const TorSettings = (() => {
+    let self = {
+        _settings: null,
+
+        // tor daemon related settings
+        defaultSettings: function() {
+            let 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: function() {
+            if (TorProtocolService.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, BrowserTopics.ProfileAfterChange);
+                Services.obs.addObserver(this, TorTopics.ProcessIsReady);
+            }
+        },
+
+        /* wait for relevant life-cycle events to apply saved settings */
+        observe: async function(subject, topic, data) {
+            console.log(`TorSettings: Observed ${topic}`);
+
+            // once the tor daemon is ready, we need to apply our settings
+            let handleProcessReady = async () => {
+                // push down settings to tor
+                await this.applySettings();
+                console.log("TorSettings: Ready");
+                Services.obs.notifyObservers(null, TorSettingsTopics.Ready);
+            };
+
+            switch (topic) {
+                case BrowserTopics.ProfileAfterChange: {
+                    Services.obs.removeObserver(this, BrowserTopics.ProfileAfterChange);
+                    if (TorProtocolService.torProcessStatus == TorProcessStatus.Running) {
+                        await handleProcessReady();
+                    }
+                }
+                break;
+                case TorTopics.ProcessIsReady: {
+                    Services.obs.removeObserver(this, TorTopics.ProcessIsReady);
+                    await handleProcessReady();
+                }
+                break;
+            }
+        },
+
+        // load our settings from prefs
+        loadFromPrefs: function() {
+            console.log("TorSettings: loadFromPrefs()");
+
+            let 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) {
+                let 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 == 0) {
+                    // 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 = [];
+                let 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) {
+                let 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: function() {
+            console.log("TorSettings: saveToPrefs()");
+
+            let 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
+            let 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
+        applySettings: async function() {
+            console.log("TorSettings: applySettings()");
+            let settings = this._settings;
+            let settingsMap = new Map();
+
+            /* Bridges */
+            const haveBridges = settings.bridges.enabled && settings.bridges.bridge_strings.length  > 0;
+            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) {
+                let address = settings.proxy.address;
+                let port = settings.proxy.port;
+                let username = settings.proxy.username;
+                let 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) {
+                let 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: function(settings) {
+            console.log("TorSettings: setSettings()");
+            let 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 == 0 && 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: function() {
+            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 > 0) {
+                        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); },
+            };
+        },
+    };
+    self.init();
+    return self;
+})();
diff --git a/browser/modules/moz.build b/browser/modules/moz.build
index dc73d9fbccdd5..b29d496879d6d 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",
+    "TorProtocolService.jsm",
+    "TorSettings.jsm",
     "TransientPrefs.jsm",
     "webrtcUI.jsm",
     "ZoomUI.jsm",
diff --git a/toolkit/components/processsingleton/MainProcessSingleton.jsm b/toolkit/components/processsingleton/MainProcessSingleton.jsm
index 4f800b93fbce4..b2b3bc4598871 100644
--- a/toolkit/components/processsingleton/MainProcessSingleton.jsm
+++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm
@@ -20,6 +20,11 @@ MainProcessSingleton.prototype = {
         // Imported for side-effects.
         ChromeUtils.import("resource://gre/modules/CustomElementsListener.jsm");
 
+        ChromeUtils.import(
+          "resource:///modules/TorSettings.jsm",
+          null
+        );
+
         Services.ppmm.loadProcessScript(
           "chrome://global/content/process-content.js",
           true

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


More information about the tbb-commits mailing list