/* 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/. */

"use strict";

const { NodeHTTP2Server } = ChromeUtils.importESModule(
  "resource://testing-common/NodeServer.sys.mjs"
);

const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);

const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
  Ci.nsINativeDNSResolverOverride
);

let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
  Ci.nsIX509CertDB
);

add_setup(async function setup() {
  addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
});

async function createServer() {
  let server = new NodeHTTP2Server();
  await server.start();
  registerCleanupFunction(async () => {
    await server.stop();
  });
  await server.registerPathHandler("/", (req, resp) => {
    let content = `hello from ${req.authority} | ${req.socket.remotePort}`;
    resp.writeHead(200, {
      "Content-Type": "text/plain",
      "Content-Length": `${content.length}`,
    });
    resp.end(content);
  });
  return server;
}

let IP1 = "127.0.0.1";
let IP2 = "127.0.0.2";
if (AppConstants.platform == "macosx") {
  // OSX doesn't use 127.0.0.2 as a local interface
  IP2 = "::1";
} else if (AppConstants.platform == "android") {
  IP2 = "10.0.2.2";
}

async function openChan(uri) {
  let chan = NetUtil.newChannel({
    uri,
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);
  chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;

  let { req, buffer } = await new Promise(resolve => {
    function finish(r, b) {
      resolve({ req: r, buffer: b });
    }
    chan.asyncOpen(new ChannelListener(finish, null, CL_ALLOW_UNKNOWN_CL));
  });

  return {
    buffer,
    port: buffer.split("|")[1],
    addr: req.QueryInterface(Ci.nsIHttpChannelInternal).remoteAddress,
    status: req.QueryInterface(Ci.nsIHttpChannel).responseStatus,
  };
}

add_task(async function test_dontCoalesce() {
  let server = await createServer();
  Services.prefs.setBoolPref("network.http.http2.aggressive_coalescing", false);
  override.clearOverrides();
  Services.dns.clearCache(true);

  // foo and alt1 resolve to different single addresses, so Happy Eyeballs
  // connects them to different IPs and they must not coalesce.
  override.addIPOverride("foo.example.com", IP1);
  override.addIPOverride("alt1.example.com", IP2);

  let { addr: addr1 } = await openChan(
    `https://foo.example.com:${server.port()}/`
  );
  let { addr: addr2 } = await openChan(
    `https://alt1.example.com:${server.port()}/`
  );

  Assert.notEqual(addr1, addr2);
  await server.stop();
});

add_task(async function test_doCoalesce() {
  let server = await createServer();
  Services.prefs.setBoolPref("network.http.http2.aggressive_coalescing", false);
  override.clearOverrides();
  Services.dns.clearCache(true);

  override.addIPOverride("foo.example.com", IP1);
  override.addIPOverride("foo.example.com", IP2);
  override.addIPOverride("alt2.example.com", IP1);
  override.addIPOverride("alt2.example.com", IP2);

  let { port: port1, addr: addr1 } = await openChan(
    `https://foo.example.com:${server.port()}/`
  );
  let { port: port2, addr: addr2 } = await openChan(
    `https://alt2.example.com:${server.port()}/`
  );

  Assert.equal(addr1, addr2);
  Assert.equal(port1, port2);
  await server.stop();
});

// Aggressive coalescing across different IPs relies on a connection being
// registered under every resolved IP (legacy multi-address coalescing keys), so
// that alt1 (resolving only to IP2) coalesces onto foo's connection on IP1.
// Happy Eyeballs registers only the winning connection's single address, so it
// only coalesces connections on the same IP. Since aggressive_coalescing is off
// by default, skip this case when Happy Eyeballs is enabled.
add_task(
  {
    skip_if: () =>
      Services.prefs.getBoolPref("network.http.happy_eyeballs_enabled", false),
  },
  async function test_doCoalesceAggresive() {
    let server = await createServer();

    Services.prefs.setBoolPref(
      "network.http.http2.aggressive_coalescing",
      true
    );
    override.clearOverrides();
    Services.dns.clearCache(true);

    override.addIPOverride("foo.example.com", IP1);
    override.addIPOverride("foo.example.com", IP2);
    override.addIPOverride("alt1.example.com", IP2);

    let { port: port1, addr: addr1 } = await openChan(
      `https://foo.example.com:${server.port()}/`
    );
    let { port: port2, addr: addr2 } = await openChan(
      `https://alt1.example.com:${server.port()}/`
    );

    Assert.equal(addr1, addr2);
    Assert.equal(port1, port2);
    await server.stop();
  }
);

// On android because of the way networking is set up the
// localAddress is always ::ffff:127.0.0.1 so it can't be
// used to make a decision.
//
// Like test_doCoalesceAggresive, this case relies on legacy multi-address
// coalescing keys (a connection registered under an IP it didn't connect to),
// so that alt1 aggressively coalesces onto foo's connection on a different IP
// and the server replies 421. Happy Eyeballs registers only the winning
// connection's single address, so this 421-then-retry path never occurs. Since
// aggressive_coalescing is off by default, skip this case when Happy Eyeballs
// is enabled.
add_task(
  {
    skip_if: () =>
      AppConstants.platform == "android" ||
      Services.prefs.getBoolPref("network.http.happy_eyeballs_enabled", false),
  },
  async function test_doCoalesceAggresive421() {
    let server = await createServer();

    await server.execute(`global.rightIP = "${IP2}"`);

    await server.registerPathHandler("/", (req, resp) => {
      let content = `hello from ${req.authority} | ${req.socket.remotePort}`;
      // Check that returning 421 when aggresively coalescing
      // makes Firefox not coalesce the connections.
      if (
        req.authority.startsWith("alt1.example.com") &&
        req.socket.localAddress != global.rightIP &&
        req.socket.localAddress != `::ffff:${global.rightIP}`
      ) {
        resp.writeHead(421, {
          "Content-Type": "text/plain",
          "Content-Length": `${content.length}`,
        });
        resp.end(content);
        return;
      }
      resp.writeHead(200, {
        "Content-Type": "text/plain",
        "Content-Length": `${content.length}`,
      });
      resp.end(content);
    });

    Services.prefs.setBoolPref(
      "network.http.http2.aggressive_coalescing",
      true
    );
    override.clearOverrides();
    Services.dns.clearCache(true);

    override.addIPOverride("foo.example.com", IP1);
    override.addIPOverride("foo.example.com", IP2);
    override.addIPOverride("alt1.example.com", IP2);

    let {
      addr: addr1,
      status: status1,
      port: port1,
    } = await openChan(`https://foo.example.com:${server.port()}/`);
    Assert.equal(status1, 200);
    Assert.equal(addr1, IP1);
    let {
      addr: addr2,
      status: status2,
      port: port2,
    } = await openChan(`https://alt1.example.com:${server.port()}/`);

    Assert.equal(status2, 200);
    Assert.equal(addr2, IP2);
    Assert.notEqual(port1, port2);
    await server.stop();
  }
);
