"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 { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);

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

const DOMAIN = "example.org";

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

  if (
    expected.PublicToPrivateHttp !== undefined ||
    expected.PublicToLocalHttp !== undefined ||
    expected.PublicToPublicHttp !== undefined ||
    expected.PublicToPrivateHttps !== undefined ||
    expected.PublicToLocalHttps !== undefined ||
    expected.PublicToPublicHttp !== undefined
  ) {
    chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
  } else if (
    expected.PrivateToLocalHttp !== undefined ||
    expected.PrivateToPrivateHttp !== undefined ||
    expected.PrivateToLocalHttps !== undefined ||
    expected.PrivateToPrivateHttps !== undefined
  ) {
    chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Private;
  }
  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 privateToLocalHttp = aExpected.PrivateToLocalHttp || null;
  let publicToPrivateHttp = aExpected.PublicToPrivateHttp || null;
  let publicToLocalHttp = aExpected.PublicToLocalHttp || null;
  let privateToLocalHttps = aExpected.PrivateToLocalHttps || null;
  let publicToPrivateHttps = aExpected.PublicToPrivateHttps || null;
  let publicToLocalHttps = aExpected.PublicToLocalHttps || null;

  let glean = Glean.networking.localNetworkAccess;
  Assert.equal(
    glean.private_to_local_http.testGetValue(),
    privateToLocalHttp,
    "verify private_to_local_http"
  );
  Assert.equal(
    glean.public_to_private_http.testGetValue(),
    publicToPrivateHttp,
    "verify public_to_private_http"
  );
  Assert.equal(
    glean.public_to_local_http.testGetValue(),
    publicToLocalHttp,
    "verify public_to_local_http"
  );
  Assert.equal(
    glean.private_to_local_https.testGetValue(),
    privateToLocalHttps,
    "verify private_to_local_http"
  );
  Assert.equal(
    glean.public_to_private_https.testGetValue(),
    publicToPrivateHttps,
    "verify public_to_private_http"
  );
  Assert.equal(
    glean.public_to_local_https.testGetValue(),
    publicToLocalHttps,
    "verify public_to_local_http"
  );

  Assert.equal(
    glean.public_to_local_https.testGetValue(),
    publicToLocalHttps,
    "verify public_to_local_http"
  );

  if (
    privateToLocalHttp ||
    publicToPrivateHttp ||
    publicToLocalHttp ||
    privateToLocalHttps ||
    publicToPrivateHttps ||
    publicToLocalHttps
  ) {
    Assert.equal(
      glean.success.testGetValue(),
      1,
      "verify local_network_access_success"
    );
    // XXX (sunil) add test for local_network_access_failure cases
  }
}

async function do_test(ip, expected, srcPort, dstPort) {
  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);

  info(`do_test ${ip}, ${fromAddr} -> ${toAddr}`);

  let chan = makeChan(`http://${DOMAIN}`, expected);
  let [req] = await channelOpenPromise(chan);

  info(
    "req.remoteAddress=" +
      req.QueryInterface(Ci.nsIHttpChannelInternal).remoteAddress
  );

  if (expected.PublicToPrivateHttp) {
    Assert.equal(chan.loadInfo.ipAddressSpace, Ci.nsILoadInfo.Private);
  } else if (expected.PrivateToLocalHttp || expected.PrivateToLocalHttp) {
    Assert.equal(chan.loadInfo.ipAddressSpace, Ci.nsILoadInfo.Local);
  }

  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_lna_http() {
  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", { PublicToPrivateHttp: 1 });

  // NO LNA access do not increment
  await do_test("10.0.0.1", { PrivateToPrivateHttp: 0 });
  await do_test("2.2.2.2", { PublicToPublicHttp: 0 });

  await do_test("100.64.0.1", { PublicToPrivateHttp: 1 });
  await do_test("127.0.0.1", { PublicToLocalHttp: 1 });
  await do_test("127.0.0.1", { PrivateToLocalHttp: 1 });
  if (AppConstants.platform != "android") {
    await do_test("::1", { PrivateToLocalHttp: 1 });
  }
});

add_task(async function test_lna_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(
    "10.0.0.1",
    { PublicToPrivateHttps: 1 },
    443,
    httpsServer.port()
  );

  // NO LNA access do not increment
  await do_test(
    "10.0.0.1",
    { PrivateToPrivateHttps: 0 },
    443,
    httpsServer.port()
  );
  await do_test(
    "2.2.2.2",
    { PublicToPublicHttps: 0 },
    443,
    httpsServer.port(),
    true
  );

  await do_test(
    "100.64.0.1",
    { PublicToPrivateHttps: 1 },
    443,
    httpsServer.port()
  );
  await do_test(
    "127.0.0.1",
    { PublicToLocalHttps: 1 },
    443,
    httpsServer.port()
  );
  await do_test(
    "127.0.0.1",
    { PrivateToLocalHttps: 1 },
    443,
    httpsServer.port()
  );
  if (AppConstants.platform != "android") {
    await do_test("::1", { PrivateToLocalHttps: 1 }, 443, httpsServer.port());
  }
});
