"use strict";

const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
  Ci.nsINativeDNSResolverOverride
);
const mockNetwork = Cc[
  "@mozilla.org/network/mock-network-controller;1"
].getService(Ci.nsIMockNetworkLayerController);
const certOverrideService = Cc[
  "@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);

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

const DOMAIN = "example.org";

function makeChan(url) {
  let chan = NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
  }).QueryInterface(Ci.nsIHttpChannel);
  return chan;
}

function channelOpenPromise(chan, flags) {
  return new Promise(resolve => {
    function finish(req, buffer) {
      resolve([req, buffer]);
      certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
        false
      );
    }
    certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
      true
    );
    chan.asyncOpen(new ChannelListener(finish, null, flags));
  });
}

let server;

add_setup(async function setup() {
  Services.prefs.setBoolPref("network.socket.attach_mock_network_layer", true);

  Services.fog.initializeFOG();

  server = new NodeHTTPServer();
  await server.start();
  registerCleanupFunction(async () => {
    Services.prefs.clearUserPref("network.disable-localhost-when-offline");
    Services.prefs.clearUserPref("network.dns.use_override_as_peer_address");
    Services.prefs.clearUserPref("dom.security.https_only_mode");
    Services.prefs.clearUserPref("dom.security.https_first");
    Services.prefs.clearUserPref("dom.security.https_first_schemeless");
    Services.prefs.clearUserPref("network.socket.attach_mock_network_layer");
    await server.stop();
  });
});

function verifyGleanValues(aDescription, aExpected) {
  info(aDescription);

  let loadIsHttps = aExpected.loadIsHttps || null;
  let loadIsHttp = aExpected.loadIsHttp || null;
  let loadIsHttpForLocalDomain = aExpected.loadIsHttpForLocalDomain || null;

  let glean = Glean.networking.httpsHttpOrLocal;
  Assert.equal(
    glean.load_is_https.testGetValue(),
    loadIsHttps,
    "verify load_is_https"
  );
  Assert.equal(
    glean.load_is_http.testGetValue(),
    loadIsHttp,
    "verify load_is_http"
  );
  Assert.equal(
    glean.load_is_http_for_local_domain.testGetValue(),
    loadIsHttpForLocalDomain,
    "verify load_is_http_for_local_domain"
  );
}

async function do_test(ip, expected, srcPort, dstPort) {
  await Services.fog.testFlushAllChildren();
  Services.fog.testResetFOG();

  override.addIPOverride(DOMAIN, ip);
  let fromAddr = mockNetwork.createScriptableNetAddr(ip, srcPort ?? 80);
  let toAddr = mockNetwork.createScriptableNetAddr(
    fromAddr.family == Ci.nsINetAddr.FAMILY_INET ? "127.0.0.1" : "::1",
    dstPort ?? server.port()
  );

  mockNetwork.addNetAddrOverride(fromAddr, toAddr);

  let chan = makeChan(`http://${DOMAIN}`);
  let [req] = await channelOpenPromise(chan);
  info(
    "req.remoteAddress=" +
      req.QueryInterface(Ci.nsIHttpChannelInternal).remoteAddress
  );
  verifyGleanValues(`test ip=${ip}`, expected);

  Services.dns.clearCache(false);
  override.clearOverrides();
  mockNetwork.clearNetAddrOverrides();
  Services.obs.notifyObservers(null, "net:prune-all-connections");
}

add_task(async function test_ipv4_local() {
  Services.prefs.setBoolPref("dom.security.https_only_mode", false);
  Services.prefs.setBoolPref("dom.security.https_first", false);
  Services.prefs.setBoolPref("dom.security.https_first_schemeless", false);

  await do_test("10.0.0.1", { loadIsHttpForLocalDomain: 1 });
  await do_test("172.16.0.1", { loadIsHttpForLocalDomain: 1 });
  await do_test("192.168.0.1", { loadIsHttpForLocalDomain: 1 });
  await do_test("169.254.0.1", { loadIsHttpForLocalDomain: 1 });
  await do_test("127.0.0.1", { loadIsHttpForLocalDomain: 1 });
});

add_task(async function test_ipv6_local() {
  await do_test("::1", { loadIsHttpForLocalDomain: 1 });
  await do_test("fc00::1", { loadIsHttpForLocalDomain: 1 });
  await do_test("fe80::1", { loadIsHttpForLocalDomain: 1 });
});

add_task(async function test_http() {
  await do_test("1.1.1.1", { loadIsHttp: 1 });
});

add_task(async function test_https() {
  Services.prefs.setBoolPref("dom.security.https_only_mode", true);
  let httpsServer = new NodeHTTPSServer();
  await httpsServer.start();
  registerCleanupFunction(async () => {
    await httpsServer.stop();
  });
  await do_test("1.1.1.1", { loadIsHttps: 1 }, 443, httpsServer.port());
});
