/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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
);
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);

  override.addIPOverride("foo.example.com", IP1);
  override.addIPOverride("foo.example.com", IP2);
  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();
});

add_task(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.
add_task(
  { skip_if: () => AppConstants.platform == "android" },
  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();
  }
);
