Verified Commit bcaeaf97 authored by Richard Pospesel's avatar Richard Pospesel Committed by Pier Angelo Vendrame
Browse files

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
parent 981e4223
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -3,8 +3,10 @@ const lazy = {};
// We will use the modules only when the profile is loaded, so prefer lazy
// loading
ChromeUtils.defineESModuleGetters(lazy, {
  TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
  TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
  TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
});

/* Browser observer topis */
@@ -28,14 +30,18 @@ export class TorStartupService {
    }
  }

  async #init() {
  #init() {
    Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);

    lazy.TorSettings.init();

    // Theoretically, build() is expected to await the initialization of the
    // provider, and anything needing the Tor Provider should be able to just
    // await on TorProviderBuilder.build().
    lazy.TorProviderBuilder.init();

    lazy.TorConnect.init();

    gInited = true;
  }

@@ -44,5 +50,6 @@ export class TorStartupService {

    lazy.TorProviderBuilder.uninit();
    lazy.TorLauncherUtil.cleanupTempDirectories();
    lazy.TorSettings.uninit();
  }
}
+2 −0
Original line number Diff line number Diff line
@@ -137,3 +137,5 @@ toolkit.jar:
# Third party files
   content/global/third_party/d3/d3.js                (/third_party/js/d3/d3.js)
   content/global/third_party/cfworker/json-schema.js (/third_party/js/cfworker/json-schema.js)

   content/global/pt_config.json               (pt_config.json)
+32 −0
Original line number Diff line number Diff line
{
  "_comment": "Used for dev build, replaced for release builds in tor-browser-build. This file is copied from tor-browser-build 0554d981:projects/tor-expert-bundle/pt_config.json",
  "recommendedDefault" : "obfs4",
  "pluggableTransports" : {
    "lyrebird" : "ClientTransportPlugin meek_lite,obfs2,obfs3,obfs4,scramblesuit exec ${pt_path}lyrebird${pt_extension}",
    "snowflake" : "ClientTransportPlugin snowflake exec ${pt_path}snowflake-client${pt_extension}",
    "webtunnel" : "ClientTransportPlugin webtunnel exec ${pt_path}webtunnel-client${pt_extension}",
    "conjure" : "ClientTransportPlugin conjure exec ${pt_path}conjure-client${pt_extension} -registerURL https://registration.refraction.network/api"
  },
  "bridges" : {
    "meek-azure" : [
      "meek_lite 192.0.2.18:80 BE776A53492E1E044A26F17306E1BC46A55A1625 url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com"
    ],
    "obfs4" : [
      "obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1",
      "obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0",
      "obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0",
      "obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0",
      "obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0",
      "obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0",
      "obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0",
      "obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0",
      "obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0",
      "obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0",
      "obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0"
    ],
    "snowflake" : [
      "snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn",
      "snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.net:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn"
    ]
  }
}
+65 −0
Original line number Diff line number Diff line
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  MoatRPC: "resource://gre/modules/Moat.sys.mjs",
});

export 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 lazy.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 lazy.MoatRPC();
        await this._moatRPC.init();
      }

      const response = await this._moatRPC.fetch(["obfs4"]);
      this._challenge = response.challenge;
      this._image =
        "data:image/jpeg;base64," + encodeURIComponent(response.image);
    } catch (err) {
      console.error("Could not request a captcha image", err);
    }
    return this._image;
  },

  close() {
    this._moatRPC?.uninit();
    this._moatRPC = null;
    this._challenge = null;
    this._image = null;
    this._bridges = null;
  },
};
+549 −0
Original line number Diff line number Diff line
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
  Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
  TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
  TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
});

/**
 * The meek pluggable transport takes the reflector URL and front domain as
 * proxy credentials, which can be prepared with this function.
 *
 * @param {string} proxyType The proxy type (socks for socks5 or socks4)
 * @param {string} reflector The URL of the service hosted by the CDN
 * @param {string} front The domain to use as a front
 * @returns {string[]} An array containing [username, password]
 */
function makeMeekCredentials(proxyType, reflector, front) {
  // Construct the per-connection arguments.
  let meekClientEscapedArgs = "";

  // 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.
  const escapeArgValue = aValue =>
    aValue
      ? aValue
          .replaceAll("\\", "\\\\")
          .replaceAll("=", "\\=")
          .replaceAll(";", "\\;")
      : "";

  if (reflector) {
    meekClientEscapedArgs += "url=";
    meekClientEscapedArgs += escapeArgValue(reflector);
  }

  if (front) {
    if (meekClientEscapedArgs.length) {
      meekClientEscapedArgs += ";";
    }
    meekClientEscapedArgs += "front=";
    meekClientEscapedArgs += escapeArgValue(front);
  }

  // socks5
  if (proxyType === "socks") {
    if (meekClientEscapedArgs.length <= 255) {
      return [meekClientEscapedArgs, "\x00"];
    }
    return [
      meekClientEscapedArgs.substring(0, 255),
      meekClientEscapedArgs.substring(255),
    ];
  } else if (proxyType === "socks4") {
    return [meekClientEscapedArgs, undefined];
  }
  throw new Error(`Unsupported proxy type ${proxyType}.`);
}

/**
 * Subprocess-based implementation to launch and control a PT process.
 */
class MeekTransport {
  // These members are used by consumers to setup the proxy to do requests over
  // meek. They are passed to newProxyInfoWithAuth.
  proxyType = null;
  proxyAddress = null;
  proxyPort = 0;
  proxyUsername = null;
  proxyPassword = null;

  #inited = false;
  #meekClientProcess = null;

  // launches the meekprocess
  async init(reflector, front) {
    // ensure we haven't already init'd
    if (this.#inited) {
      throw new Error("MeekTransport: Already initialized");
    }

    try {
      // figure out which pluggable transport to use
      const supportedTransports = ["meek", "meek_lite"];
      const provider = await lazy.TorProviderBuilder.build();
      const proxy = (await provider.getPluggableTransports()).find(
        pt =>
          pt.type === "exec" &&
          supportedTransports.some(t => pt.transports.includes(t))
      );
      if (!proxy) {
        throw new Error("No supported transport found.");
      }

      const meekTransport = proxy.transports.find(t =>
        supportedTransports.includes(t)
      );
      // Convert meek client path to absolute path if necessary
      const meekWorkDir = lazy.TorLauncherUtil.getTorFile(
        "pt-startup-dir",
        false
      );
      if (lazy.TorLauncherUtil.isPathRelative(proxy.pathToBinary)) {
        const meekPath = meekWorkDir.clone();
        meekPath.appendRelativePath(proxy.pathToBinary);
        proxy.pathToBinary = meekPath.path;
      }

      // Setup env and start meek process
      const ptStateDir = lazy.TorLauncherUtil.getTorFile("tordatadir", false);
      ptStateDir.append("pt_state"); // Match what tor uses.

      const envAdditions = {
        TOR_PT_MANAGED_TRANSPORT_VER: "1",
        TOR_PT_STATE_LOCATION: ptStateDir.path,
        TOR_PT_EXIT_ON_STDIN_CLOSE: "1",
        TOR_PT_CLIENT_TRANSPORTS: meekTransport,
      };
      if (lazy.TorSettings.proxy.enabled) {
        envAdditions.TOR_PT_PROXY = lazy.TorSettings.proxy.uri;
      }

      const opts = {
        command: proxy.pathToBinary,
        arguments: proxy.options.split(/s+/),
        workdir: meekWorkDir.path,
        environmentAppend: true,
        environment: envAdditions,
        stderr: "pipe",
      };

      // Launch meek client
      this.#meekClientProcess = await lazy.Subprocess.call(opts);

      // Callback chain for reading stderr
      const stderrLogger = async () => {
        while (this.#meekClientProcess) {
          const errString = await this.#meekClientProcess.stderr.readString();
          if (errString) {
            console.log(`MeekTransport: stderr => ${errString}`);
          }
        }
      };
      stderrLogger();

      // Read pt's stdout until terminal (CMETHODS DONE) is reached
      // returns array of lines for parsing
      const getInitLines = async (stdout = "") => {
        stdout += await this.#meekClientProcess.stdout.readString();

        // 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.substring(0, endIndex).split("\n");
        }
        return getInitLines(stdout);
      };

      // read our lines from pt's stdout
      const meekInitLines = await getInitLines();
      // tokenize our pt lines
      const meekInitTokens = meekInitLines.map(line => {
        const tokens = line.split(" ");
        return {
          keyword: tokens[0],
          args: tokens.slice(1),
        };
      });

      // 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
            this.proxyType = proxyType === "socks5" ? "socks" : "socks4";
            this.proxyAddress = addr;
            this.proxyPort = 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}'`
          );
        }
      }

      // register callback to cleanup on process exit
      this.#meekClientProcess.wait().then(exitObj => {
        this.#meekClientProcess = null;
        this.uninit();
      });
      [this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
        this.proxyType,
        reflector,
        front
      );
      this.#inited = true;
    } catch (ex) {
      if (this.#meekClientProcess) {
        this.#meekClientProcess.kill();
        this.#meekClientProcess = null;
      }
      throw ex;
    }
  }

  async uninit() {
    this.#inited = false;

    await this.#meekClientProcess?.kill();
    this.#meekClientProcess = null;
    this.proxyType = null;
    this.proxyAddress = null;
    this.proxyPort = 0;
    this.proxyUsername = null;
    this.proxyPassword = null;
  }
}

/**
 * Android implementation of the Meek process.
 *
 * GeckoView does not provide the subprocess module, so we have to use the
 * EventDispatcher, and have a Java handler start and stop the proxy process.
 */
class MeekTransportAndroid {
  // These members are used by consumers to setup the proxy to do requests over
  // meek. They are passed to newProxyInfoWithAuth.
  proxyType = null;
  proxyAddress = null;
  proxyPort = 0;
  proxyUsername = null;
  proxyPassword = null;

  /**
   * An id for process this instance is linked to.
   *
   * Since we do not restrict the transport to be a singleton, we need a handle to
   * identify the process we want to stop when the transport owner is done.
   * We use a counter incremented on the Java side for now.
   *
   * This number must be a positive integer (i.e., 0 is an invalid handler).
   *
   * @type {number}
   */
  #id = 0;

  async init(reflector, front) {
    // ensure we haven't already init'd
    if (this.#id) {
      throw new Error("MeekTransport: Already initialized");
    }
    const details = await lazy.EventDispatcher.instance.sendRequestForResult({
      type: "GeckoView:Tor:StartMeek",
    });
    this.#id = details.id;
    this.proxyType = "socks";
    this.proxyAddress = details.address;
    this.proxyPort = details.port;
    [this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
      this.proxyType,
      reflector,
      front
    );
  }

  async uninit() {
    lazy.EventDispatcher.instance.sendRequest({
      type: "GeckoView:Tor:StopMeek",
      id: this.#id,
    });
    this.#id = 0;
    this.proxyType = null;
    this.proxyAddress = null;
    this.proxyPort = 0;
    this.proxyUsername = null;
    this.proxyPassword = null;
  }
}

/**
 * Corresponds to a Network error with the request.
 */
export class DomainFrontRequestNetworkError extends Error {
  constructor(request, statusCode) {
    super(`Error fetching ${request.name}: ${statusCode}`);
    this.name = "DomainFrontRequestNetworkError";
    this.statusCode = statusCode;
  }
}

/**
 * Corresponds to a non-ok response from the server.
 */
export class DomainFrontRequestResponseError extends Error {
  constructor(request) {
    super(
      `Error response from ${request.name} server: ${request.responseStatus}`
    );
    this.name = "DomainFrontRequestResponseError";
    this.status = request.responseStatus;
    this.statusText = request.responseStatusText;
  }
}

/**
 * Callback object to promisify the XPCOM request.
 */
class ResponseListener {
  #response = "";
  #responsePromise;
  #resolve;
  #reject;
  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)) {
        // Assume this is a network error.
        this.#reject(new DomainFrontRequestNetworkError(request, status));
      }
      if (request.responseStatus !== 200) {
        this.#reject(new DomainFrontRequestResponseError(request));
      }
    } 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);
  }
}

// constructs the json objects and sends the request over moat
export class DomainFrontRequestBuilder {
  #inited = false;
  #meekTransport = null;

  get inited() {
    return this.#inited;
  }

  async init(reflector, front) {
    if (this.#inited) {
      throw new Error("MoatRPC: Already initialized");
    }

    const meekTransport =
      Services.appinfo.OS === "Android"
        ? new MeekTransportAndroid()
        : new MeekTransport();
    await meekTransport.init(reflector, front);
    this.#meekTransport = meekTransport;
    this.#inited = true;
  }

  async uninit() {
    await this.#meekTransport?.uninit();
    this.#meekTransport = null;
    this.#inited = false;
  }

  buildHttpHandler(uriString) {
    if (!this.#inited) {
      throw new Error("MoatRPC: Not initialized");
    }

    const { proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword } =
      this.#meekTransport;

    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;
  }

  /**
   * Make a POST request with a JSON body.
   *
   * @param {string} url The URL to load
   * @param {object} args The arguments to send to the procedure. It will be
   * serialized to JSON by this function and then set as POST body
   * @returns {Promise<object>} A promise with the parsed response
   */
  async buildPostRequest(url, args) {
    const ch = this.buildHttpHandler(url);

    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 ResponseListener();
    await ch.asyncOpen(listener, ch);

    // wait for response
    const responseJSON = await listener.response();

    // parse that JSON
    return JSON.parse(responseJSON);
  }
}
Loading