/* 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";

/* import-globals-from head_cache.js */
/* import-globals-from head_cookies.js */
/* import-globals-from head_trr.js */
/* import-globals-from head_http3.js */

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

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

const TRR_Domain = "foo.example.com";

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

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

async function SetParentalControlEnabled(aEnabled) {
  let parentalControlsService = {
    parentalControlsEnabled: aEnabled,
    QueryInterface: ChromeUtils.generateQI(["nsIParentalControlsService"]),
  };
  let cid = MockRegistrar.register(
    "@mozilla.org/parental-controls-service;1",
    parentalControlsService
  );
  Services.dns.reloadParentalControlEnabled();
  MockRegistrar.unregister(cid);
}

let runningOHTTPTests = false;
let h2Port;

function setModeAndURIForODoH(mode, path) {
  Services.prefs.setIntPref("network.trr.mode", mode);
  if (path.substr(0, 4) == "doh?") {
    path = path.replace("doh?", "odoh?");
  }

  Services.prefs.setCharPref("network.trr.odoh.target_path", `${path}`);
}

function setModeAndURIForOHTTP(mode, path, domain) {
  Services.prefs.setIntPref("network.trr.mode", mode);
  if (domain) {
    Services.prefs.setCharPref(
      "network.trr.ohttp.uri",
      `https://${domain}:${h2Port}/${path}`
    );
  } else {
    Services.prefs.setCharPref(
      "network.trr.ohttp.uri",
      `https://${TRR_Domain}:${h2Port}/${path}`
    );
  }
}

function setModeAndURI(mode, path, domain) {
  if (runningOHTTPTests) {
    setModeAndURIForOHTTP(mode, path, domain);
  } else {
    Services.prefs.setIntPref("network.trr.mode", mode);
    if (domain) {
      Services.prefs.setCharPref(
        "network.trr.uri",
        `https://${domain}:${h2Port}/${path}`
      );
    } else {
      Services.prefs.setCharPref(
        "network.trr.uri",
        `https://${TRR_Domain}:${h2Port}/${path}`
      );
    }
  }
}

async function test_A_record() {
  info("Verifying a basic A record");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2"); // TRR-first
  await new TRRDNSListener("bar.example.com", "2.2.2.2");

  info("Verifying a basic A record - without bootstrapping");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=3.3.3.3"); // TRR-only

  // Clear bootstrap address and add DoH endpoint hostname to local domains
  Services.prefs.clearUserPref("network.trr.bootstrapAddr");
  Services.prefs.setCharPref("network.dns.localDomains", TRR_Domain);

  await new TRRDNSListener("bar.example.com", "3.3.3.3");

  Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");
  Services.prefs.clearUserPref("network.dns.localDomains");

  info("Verify that the cached record is used when DoH endpoint is down");
  // Don't clear the cache. That is what we're checking.
  setModeAndURI(3, "404");

  await new TRRDNSListener("bar.example.com", "3.3.3.3");
  info("verify working credentials in DOH request");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=4.4.4.4&auth=true");
  Services.prefs.setCharPref("network.trr.credentials", "user:password");

  await new TRRDNSListener("bar.example.com", "4.4.4.4");

  info("Verify failing credentials in DOH request");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=4.4.4.4&auth=true");
  Services.prefs.setCharPref("network.trr.credentials", "evil:person");

  let { inStatus } = await new TRRDNSListener(
    "wrong.example.com",
    undefined,
    false
  );
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );

  Services.prefs.clearUserPref("network.trr.credentials");
}

async function test_AAAA_records() {
  info("Verifying AAAA record");

  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=2020:2020::2020&delayIPv4=100");

  await new TRRDNSListener("aaaa.example.com", "2020:2020::2020");

  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=2020:2020::2020&delayIPv6=100");

  await new TRRDNSListener("aaaa.example.com", "2020:2020::2020");

  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=2020:2020::2020");

  await new TRRDNSListener("aaaa.example.com", "2020:2020::2020");
}

async function test_RFC1918() {
  info("Verifying that RFC1918 address from the server is rejected by default");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=192.168.0.1");

  let { inStatus } = await new TRRDNSListener(
    "rfc1918.example.com",
    undefined,
    false
  );

  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );
  setModeAndURI(3, "doh?responseIP=::ffff:192.168.0.1");
  ({ inStatus } = await new TRRDNSListener(
    "rfc1918-ipv6.example.com",
    undefined,
    false
  ));
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );

  info("Verify RFC1918 address from the server is fine when told so");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=192.168.0.1");
  Services.prefs.setBoolPref("network.trr.allow-rfc1918", true);
  await new TRRDNSListener("rfc1918.example.com", "192.168.0.1");
  setModeAndURI(3, "doh?responseIP=::ffff:192.168.0.1");

  await new TRRDNSListener("rfc1918-ipv6.example.com", "::ffff:192.168.0.1");

  Services.prefs.clearUserPref("network.trr.allow-rfc1918");
}

async function test_GET_ECS() {
  info("Verifying resolution via GET with ECS disabled");
  Services.dns.clearCache(true);
  // The template part should be discarded
  setModeAndURI(3, "doh{?dns}");
  Services.prefs.setBoolPref("network.trr.useGET", true);
  Services.prefs.setBoolPref("network.trr.disable-ECS", true);

  await new TRRDNSListener("ecs.example.com", "5.5.5.5");

  info("Verifying resolution via GET with ECS enabled");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh");
  Services.prefs.setBoolPref("network.trr.disable-ECS", false);

  await new TRRDNSListener("get.example.com", "5.5.5.5");

  Services.prefs.clearUserPref("network.trr.useGET");
  Services.prefs.clearUserPref("network.trr.disable-ECS");
}

async function test_timeout_mode3() {
  info("Verifying that a short timeout causes failure with a slow server");
  Services.dns.clearCache(true);
  // First, mode 3.
  setModeAndURI(3, "doh?noResponse=true");
  Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
  Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);

  let { inStatus } = await new TRRDNSListener(
    "timeout.example.com",
    undefined,
    false
  );
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );

  // Now for mode 2
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?noResponse=true");

  await new TRRDNSListener("timeout.example.com", "127.0.0.1"); // Should fallback

  Services.prefs.clearUserPref("network.trr.request_timeout_ms");
  Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
}

async function test_trr_retry() {
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);

  info("Test fallback to native");
  Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", false);
  setModeAndURI(2, "doh?noResponse=true");
  Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
  Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);

  await new TRRDNSListener("timeout.example.com", {
    expectedAnswer: "127.0.0.1",
  });

  Services.prefs.clearUserPref("network.trr.request_timeout_ms");
  Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");

  info("Test Retry Success");
  Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", true);

  let chan = makeChan(
    `https://foo.example.com:${h2Port}/reset-doh-request-count`,
    Ci.nsIRequest.TRR_DISABLED_MODE
  );
  await new Promise(resolve =>
    chan.asyncOpen(new ChannelListener(resolve, null))
  );

  setModeAndURI(2, "doh?responseIP=2.2.2.2&retryOnDecodeFailure=true");
  await new TRRDNSListener("retry_ok.example.com", "2.2.2.2");

  info("Test Retry Failed");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true");
  await new TRRDNSListener("retry_ng.example.com", "127.0.0.1");
}

async function test_strict_native_fallback() {
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", true);
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);

  info("First a timeout case");
  setModeAndURI(2, "doh?noResponse=true");
  Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
  Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);
  Services.prefs.setIntPref(
    "network.trr.strict_fallback_request_timeout_ms",
    10
  );

  Services.prefs.setBoolPref(
    "network.trr.strict_native_fallback_allow_timeouts",
    false
  );

  let { inStatus } = await new TRRDNSListener(
    "timeout.example.com",
    undefined,
    false
  );
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );
  Services.dns.clearCache(true);
  await new TRRDNSListener("timeout.example.com", undefined, false);

  Services.dns.clearCache(true);
  Services.prefs.setBoolPref(
    "network.trr.strict_native_fallback_allow_timeouts",
    true
  );
  await new TRRDNSListener("timeout.example.com", {
    expectedAnswer: "127.0.0.1",
  });

  Services.prefs.setBoolPref(
    "network.trr.strict_native_fallback_allow_timeouts",
    false
  );

  info("Now a connection error");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");
  Services.prefs.clearUserPref("network.trr.request_timeout_ms");
  Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
  Services.prefs.clearUserPref(
    "network.trr.strict_fallback_request_timeout_ms"
  );
  ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false));
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );

  info("Now a decode error");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true");
  ({ inStatus } = await new TRRDNSListener(
    "bar.example.com",
    undefined,
    false
  ));
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );

  if (!mozinfo.socketprocess_networking) {
    // Confirmation state isn't passed cross-process.
    info("Now with confirmation failed - should fallback");
    Services.dns.clearCache(true);
    setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true");
    Services.prefs.setCharPref("network.trr.confirmationNS", "example.com");
    await TestUtils.waitForCondition(
      // 3 => CONFIRM_FAILED, 4 => CONFIRM_TRYING_FAILED
      () =>
        Services.dns.currentTrrConfirmationState == 3 ||
        Services.dns.currentTrrConfirmationState == 4,
      `Timed out waiting for confirmation failure. Currently ${Services.dns.currentTrrConfirmationState}`,
      1,
      5000
    );
    await new TRRDNSListener("bar.example.com", "127.0.0.1"); // Should fallback
  }

  info("Now a successful case.");
  // Cancel connections to let TRR recover after the connection/decode error cases
  // above. On Android (ADB port forwarding) reconnection is slower.
  Services.obs.notifyObservers(null, "net:cancel-all-connections");
  await new Promise(resolve => do_timeout(500, resolve));
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");
  if (!mozinfo.socketprocess_networking) {
    // Only need to reset confirmation state if we messed with it before.
    Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
    await TestUtils.waitForCondition(
      // 5 => CONFIRM_DISABLED
      () => Services.dns.currentTrrConfirmationState == 5,
      `Timed out waiting for confirmation disabled. Currently ${Services.dns.currentTrrConfirmationState}`,
      1,
      5000
    );
  }
  await new TRRDNSListener("bar.example.com", "2.2.2.2");

  info("Now without strict fallback mode, timeout case");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?noResponse=true");
  Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
  Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);
  Services.prefs.setIntPref(
    "network.trr.strict_fallback_request_timeout_ms",
    10
  );
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);

  await new TRRDNSListener("timeout.example.com", "127.0.0.1"); // Should fallback

  info("Now a connection error");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");
  Services.prefs.clearUserPref("network.trr.request_timeout_ms");
  Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
  Services.prefs.clearUserPref(
    "network.trr.strict_fallback_request_timeout_ms"
  );
  await new TRRDNSListener("closeme.com", "127.0.0.1"); // Should fallback

  info("Now a decode error");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true");
  await new TRRDNSListener("bar.example.com", "127.0.0.1"); // Should fallback

  Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
  Services.prefs.clearUserPref("network.trr.request_timeout_ms");
  Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
  Services.prefs.clearUserPref(
    "network.trr.strict_fallback_request_timeout_ms"
  );
}

async function test_no_answers_fallback() {
  info("Verfiying that we correctly fallback to Do53 when no answers from DoH");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=none"); // TRR-first

  await new TRRDNSListener("confirm.example.com", "127.0.0.1");

  info("Now in strict mode - no fallback");
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
  Services.dns.clearCache(true);
  await new TRRDNSListener("confirm.example.com", "127.0.0.1");
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
}

async function test_404_fallback() {
  info("Verfiying that we correctly fallback to Do53 when DoH sends 404");
  Services.dns.clearCache(true);
  setModeAndURI(2, "404"); // TRR-first

  await new TRRDNSListener("test404.example.com", "127.0.0.1");

  info("Now in strict mode - no fallback");
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
  Services.dns.clearCache(true);
  let { inStatus } = await new TRRDNSListener("test404.example.com", {
    expectedSuccess: false,
  });
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
}

async function test_mode_1_and_4() {
  info("Verifying modes 1 and 4 are treated as TRR-off");
  for (let mode of [1, 4]) {
    Services.dns.clearCache(true);
    setModeAndURI(mode, "doh?responseIP=2.2.2.2");
    Assert.equal(
      Services.dns.currentTrrMode,
      5,
      "Effective TRR mode should be 5"
    );
  }
}

async function test_CNAME() {
  info("Checking that we follow a CNAME correctly");
  // cnameHandler for dns-cname in head_trr doesn't currently suppport GET
  Services.prefs.setBoolPref("network.trr.useGET", false);
  Services.dns.clearCache(true);
  // The dns-cname path alternates between sending us a CNAME pointing to
  // another domain, and an A record. If we follow the cname correctly, doing
  // a lookup with this path as the DoH URI should resolve to that A record.
  setModeAndURI(3, "dns-cname");

  await new TRRDNSListener("cname.example.com", "99.88.77.66");

  info("Verifying that we bail out when we're thrown into a CNAME loop");
  Services.dns.clearCache(true);
  // First mode 3.
  setModeAndURI(3, "doh?responseIP=none&cnameloop=true");

  let { inStatus } = await new TRRDNSListener(
    "test18.example.com",
    undefined,
    false
  );
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );

  // Now mode 2.
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=none&cnameloop=true");

  await new TRRDNSListener("test20.example.com", "127.0.0.1"); // Should fallback

  info("Check that we correctly handle CNAME bundled with an A record");
  Services.dns.clearCache(true);
  // "dns-cname-a" path causes server to send a CNAME as well as an A record
  setModeAndURI(3, "dns-cname-a");

  await new TRRDNSListener("cname-a.example.com", "9.8.7.6");
  Services.prefs.clearUserPref("network.trr.useGET");
}

async function test_name_mismatch() {
  info("Verify that records that don't match the requested name are rejected");
  Services.dns.clearCache(true);
  // Setting hostname param tells server to always send record for bar.example.com
  // regardless of what was requested.
  setModeAndURI(3, "doh?hostname=mismatch.example.com");

  let { inStatus } = await new TRRDNSListener(
    "bar.example.com",
    undefined,
    false
  );
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );
}

async function test_mode_2() {
  info("Checking that TRR result is used in mode 2");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=192.192.192.192");
  Services.prefs.setCharPref("network.trr.excluded-domains", "");
  Services.prefs.setCharPref("network.trr.builtin-excluded-domains", "");

  await new TRRDNSListener("bar.example.com", "192.192.192.192");

  info("Now in strict mode");
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
  Services.dns.clearCache(true);
  await new TRRDNSListener("bar.example.com", "192.192.192.192");
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
}

async function test_excluded_domains() {
  info("Checking that Do53 is used for names in excluded-domains list");
  for (let strictMode of [true, false]) {
    info("Strict mode: " + strictMode);
    Services.prefs.setBoolPref(
      "network.trr.strict_native_fallback",
      strictMode
    );
    Services.dns.clearCache(true);
    setModeAndURI(2, "doh?responseIP=192.192.192.192");
    Services.prefs.setCharPref(
      "network.trr.excluded-domains",
      "bar.example.com"
    );

    await new TRRDNSListener("bar.example.com", "127.0.0.1"); // Do53 result

    Services.dns.clearCache(true);
    Services.prefs.setCharPref("network.trr.excluded-domains", "example.com");

    await new TRRDNSListener("bar.example.com", "127.0.0.1");

    Services.dns.clearCache(true);
    Services.prefs.setCharPref(
      "network.trr.excluded-domains",
      "foo.test.com, bar.example.com"
    );
    await new TRRDNSListener("bar.example.com", "127.0.0.1");

    Services.dns.clearCache(true);
    Services.prefs.setCharPref(
      "network.trr.excluded-domains",
      "bar.example.com, foo.test.com"
    );

    await new TRRDNSListener("bar.example.com", "127.0.0.1");

    Services.prefs.clearUserPref("network.trr.excluded-domains");
  }
}

function topicObserved(topic) {
  return new Promise(resolve => {
    let observer = {
      QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
      observe(aSubject, aTopic, aData) {
        if (aTopic == topic) {
          Services.obs.removeObserver(observer, topic);
          resolve(aData);
        }
      },
    };
    Services.obs.addObserver(observer, topic);
  });
}

async function test_captiveportal_canonicalURL() {
  info("Check that captivedetect.canonicalURL is resolved via native DNS");
  for (let strictMode of [true, false]) {
    info("Strict mode: " + strictMode);
    Services.prefs.setBoolPref(
      "network.trr.strict_native_fallback",
      strictMode
    );
    Services.dns.clearCache(true);
    setModeAndURI(2, "doh?responseIP=2.2.2.2");

    const cpServer = new HttpServer();
    cpServer.registerPathHandler(
      "/cp",
      function handleRawData(request, response) {
        response.setHeader("Content-Type", "text/plain", false);
        response.setHeader("Cache-Control", "no-cache", false);
        response.bodyOutputStream.write("data", 4);
      }
    );
    cpServer.start(-1);
    cpServer.identity.setPrimary(
      "http",
      "detectportal.firefox.com",
      cpServer.identity.primaryPort
    );
    let cpPromise = topicObserved("captive-portal-login");

    Services.prefs.setCharPref(
      "captivedetect.canonicalURL",
      `http://detectportal.firefox.com:${cpServer.identity.primaryPort}/cp`
    );
    Services.prefs.setBoolPref("network.captive-portal-service.testMode", true);
    Services.prefs.setBoolPref("network.captive-portal-service.enabled", true);

    // The captive portal has to have used native DNS, otherwise creating
    // a socket to a non-local IP would trigger a crash.
    await cpPromise;
    // Simply resolving the captive portal domain should still use TRR
    await new TRRDNSListener("detectportal.firefox.com", "2.2.2.2");

    Services.prefs.clearUserPref("network.captive-portal-service.enabled");
    Services.prefs.clearUserPref("network.captive-portal-service.testMode");
    Services.prefs.clearUserPref("captivedetect.canonicalURL");

    await new Promise(resolve => cpServer.stop(resolve));
  }
}

async function test_parentalcontrols() {
  info("Check that DoH isn't used when parental controls are enabled");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");
  await SetParentalControlEnabled(true);
  await new TRRDNSListener("www.example.com", "127.0.0.1");
  await SetParentalControlEnabled(false);

  info("Now in strict mode");
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");
  await SetParentalControlEnabled(true);
  await new TRRDNSListener("www.example.com", "127.0.0.1");
  await SetParentalControlEnabled(false);
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
}

async function test_builtin_excluded_domains() {
  info("Verifying Do53 is used for domains in builtin-excluded-domians list");
  for (let strictMode of [true, false]) {
    info("Strict mode: " + strictMode);
    Services.prefs.setBoolPref(
      "network.trr.strict_native_fallback",
      strictMode
    );
    Services.dns.clearCache(true);
    setModeAndURI(2, "doh?responseIP=2.2.2.2");

    Services.prefs.setCharPref("network.trr.excluded-domains", "");
    Services.prefs.setCharPref(
      "network.trr.builtin-excluded-domains",
      "bar.example.com"
    );
    await new TRRDNSListener("bar.example.com", "127.0.0.1");

    Services.dns.clearCache(true);
    Services.prefs.setCharPref(
      "network.trr.builtin-excluded-domains",
      "example.com"
    );
    await new TRRDNSListener("bar.example.com", "127.0.0.1");

    Services.dns.clearCache(true);
    Services.prefs.setCharPref(
      "network.trr.builtin-excluded-domains",
      "foo.test.com, bar.example.com"
    );
    await new TRRDNSListener("bar.example.com", "127.0.0.1");
    await new TRRDNSListener("foo.test.com", "127.0.0.1");
  }
}

async function test_excluded_domains_mode3() {
  info("Checking  Do53 is used for names in excluded-domains list in mode 3");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=192.192.192.192");
  Services.prefs.setCharPref("network.trr.excluded-domains", "");
  Services.prefs.setCharPref("network.trr.builtin-excluded-domains", "");

  await new TRRDNSListener("excluded", "192.192.192.192", true);

  Services.dns.clearCache(true);
  Services.prefs.setCharPref("network.trr.excluded-domains", "excluded");

  await new TRRDNSListener("excluded", "127.0.0.1");

  // Test .local
  Services.dns.clearCache(true);
  Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local");

  await new TRRDNSListener("test.local", "127.0.0.1");

  // Test .other
  Services.dns.clearCache(true);
  Services.prefs.setCharPref(
    "network.trr.excluded-domains",
    "excluded,local,other"
  );

  await new TRRDNSListener("domain.other", "127.0.0.1");
}

async function test25e() {
  info("Check captivedetect.canonicalURL is resolved via native DNS in mode 3");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=192.192.192.192");

  const cpServer = new HttpServer();
  cpServer.registerPathHandler(
    "/cp",
    function handleRawData(request, response) {
      response.setHeader("Content-Type", "text/plain", false);
      response.setHeader("Cache-Control", "no-cache", false);
      response.bodyOutputStream.write("data", 4);
    }
  );
  cpServer.start(-1);
  cpServer.identity.setPrimary(
    "http",
    "detectportal.firefox.com",
    cpServer.identity.primaryPort
  );
  let cpPromise = topicObserved("captive-portal-login");

  Services.prefs.setCharPref(
    "captivedetect.canonicalURL",
    `http://detectportal.firefox.com:${cpServer.identity.primaryPort}/cp`
  );
  Services.prefs.setBoolPref("network.captive-portal-service.testMode", true);
  Services.prefs.setBoolPref("network.captive-portal-service.enabled", true);

  // The captive portal has to have used native DNS, otherwise creating
  // a socket to a non-local IP would trigger a crash.
  await cpPromise;
  // // Simply resolving the captive portal domain should still use TRR
  await new TRRDNSListener("detectportal.firefox.com", "192.192.192.192");

  Services.prefs.clearUserPref("network.captive-portal-service.enabled");
  Services.prefs.clearUserPref("network.captive-portal-service.testMode");
  Services.prefs.clearUserPref("captivedetect.canonicalURL");

  await new Promise(resolve => cpServer.stop(resolve));
}

async function test_parentalcontrols_mode3() {
  info("Check DoH isn't used when parental controls are enabled in mode 3");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=192.192.192.192");
  await SetParentalControlEnabled(true);
  await new TRRDNSListener("www.example.com", "127.0.0.1");
  await SetParentalControlEnabled(false);
}

async function test_builtin_excluded_domains_mode3() {
  info("Check Do53 used for domains in builtin-excluded-domians list, mode 3");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=192.192.192.192");
  Services.prefs.setCharPref("network.trr.excluded-domains", "");
  Services.prefs.setCharPref(
    "network.trr.builtin-excluded-domains",
    "excluded"
  );

  await new TRRDNSListener("excluded", "127.0.0.1");

  // Test .local
  Services.dns.clearCache(true);
  Services.prefs.setCharPref(
    "network.trr.builtin-excluded-domains",
    "excluded,local"
  );

  await new TRRDNSListener("test.local", "127.0.0.1");

  // Test .other
  Services.dns.clearCache(true);
  Services.prefs.setCharPref(
    "network.trr.builtin-excluded-domains",
    "excluded,local,other"
  );

  await new TRRDNSListener("domain.other", "127.0.0.1");
}

async function count_cookies() {
  info("Check that none of the requests have set any cookies.");
  Assert.equal(Services.cookies.countCookiesFromHost("example.com"), 0);
  Assert.equal(Services.cookies.countCookiesFromHost("foo.example.com."), 0);
}

async function test_connection_closed() {
  info("Check we handle it correctly when the connection is closed");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=2.2.2.2");
  Services.prefs.setCharPref("network.trr.excluded-domains", "");
  // We don't need to wait for 30 seconds for the request to fail
  Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 500);
  // bootstrap
  Services.prefs.clearUserPref("network.dns.localDomains");
  Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");

  await new TRRDNSListener("bar.example.com", "2.2.2.2");

  // makes the TRR connection shut down.
  let { inStatus } = await new TRRDNSListener("closeme.com", undefined, false);
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );
  await new TRRDNSListener("bar2.example.com", "2.2.2.2");

  // No bootstrap this time
  Services.prefs.clearUserPref("network.trr.bootstrapAddr");

  Services.dns.clearCache(true);
  Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local");
  Services.prefs.setCharPref("network.dns.localDomains", TRR_Domain);

  await new TRRDNSListener("bar.example.com", "2.2.2.2");

  // makes the TRR connection shut down.
  ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false));
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );
  await new TRRDNSListener("bar2.example.com", "2.2.2.2");

  // No local domains either
  Services.dns.clearCache(true);
  Services.prefs.setCharPref("network.trr.excluded-domains", "excluded");
  Services.prefs.clearUserPref("network.dns.localDomains");
  Services.prefs.clearUserPref("network.trr.bootstrapAddr");

  await new TRRDNSListener("bar.example.com", "2.2.2.2");

  // makes the TRR connection shut down.
  ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false));
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );
  await new TRRDNSListener("bar2.example.com", "2.2.2.2");

  // Now make sure that even in mode 3 without a bootstrap address
  // we are able to restart the TRR connection if it drops - the TRR service
  // channel will use regular DNS to resolve the TRR address.
  Services.dns.clearCache(true);
  Services.prefs.setCharPref("network.trr.excluded-domains", "");
  Services.prefs.setCharPref("network.trr.builtin-excluded-domains", "");
  Services.prefs.clearUserPref("network.dns.localDomains");
  Services.prefs.clearUserPref("network.trr.bootstrapAddr");

  await new TRRDNSListener("bar.example.com", "2.2.2.2");

  // makes the TRR connection shut down.
  ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false));
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );
  Services.dns.clearCache(true);
  await new TRRDNSListener("bar2.example.com", "2.2.2.2");

  // This test exists to document what happens when we're in TRR only mode
  // and we don't set a bootstrap address. We use DNS to resolve the
  // initial URI, but if the connection fails, we don't fallback to DNS
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=9.9.9.9");
  Services.prefs.setCharPref("network.dns.localDomains", "closeme.com");
  Services.prefs.clearUserPref("network.trr.bootstrapAddr");

  await new TRRDNSListener("bar.example.com", "9.9.9.9");

  // makes the TRR connection shut down. Should fallback to DNS
  await new TRRDNSListener("closeme.com", "127.0.0.1");
  // TRR should be back up again
  await new TRRDNSListener("bar2.example.com", "9.9.9.9");
}

async function test_fetch_time() {
  info("Verifying timing");
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2&delayIPv4=20");

  await new TRRDNSListener("bar_time.example.com", "2.2.2.2", true, 20);

  // gets an error from DoH. It will fall back to regular DNS. The TRR timing should be 0.
  Services.dns.clearCache(true);
  setModeAndURI(2, "404&delayIPv4=20");

  await new TRRDNSListener("bar_time1.example.com", "127.0.0.1", true, 0);

  // check an excluded domain. It should fall back to regular DNS. The TRR timing should be 0.
  Services.prefs.setCharPref(
    "network.trr.excluded-domains",
    "bar_time2.example.com"
  );
  for (let strictMode of [true, false]) {
    info("Strict mode: " + strictMode);
    Services.prefs.setBoolPref(
      "network.trr.strict_native_fallback",
      strictMode
    );
    Services.dns.clearCache(true);
    setModeAndURI(2, "doh?responseIP=2.2.2.2&delayIPv4=20");
    await new TRRDNSListener("bar_time2.example.com", "127.0.0.1", true, 0);
  }

  Services.prefs.setCharPref("network.trr.excluded-domains", "");

  // verify RFC1918 address from the server is rejected and the TRR timing will be not set because the response will be from the native resolver.
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=192.168.0.1&delayIPv4=20");
  await new TRRDNSListener("rfc1918_time.example.com", "127.0.0.1", true, 0);
}

async function test_fqdn() {
  info("Test that we handle FQDN encoding and decoding properly");
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=9.8.7.6");

  await new TRRDNSListener("fqdn.example.org.", "9.8.7.6");

  // GET
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.useGET", true);
  await new TRRDNSListener("fqdn_get.example.org.", "9.8.7.6");

  Services.prefs.clearUserPref("network.trr.useGET");
}

async function test_ipv6_trr_fallback() {
  info("Testing fallback with ipv6");
  Services.dns.clearCache(true);

  setModeAndURI(2, "doh?responseIP=4.4.4.4");
  const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
    Ci.nsINativeDNSResolverOverride
  );
  gOverride.addIPOverride("ipv6.host.com", "1:1::2");

  // Should not fallback to Do53 because A request for ipv6.host.com returns
  // 4.4.4.4
  let { inStatus } = await new TRRDNSListener("ipv6.host.com", {
    flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
    expectedSuccess: false,
  });
  equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);

  // This time both requests fail, so we do fall back
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=none");
  await new TRRDNSListener("ipv6.host.com", "1:1::2");

  info("In strict mode, the lookup should fail when both reqs fail.");
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
  setModeAndURI(2, "doh?responseIP=none");
  await new TRRDNSListener("ipv6.host.com", "1:1::2");
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);

  override.clearOverrides();
}

async function test_ipv4_trr_fallback() {
  info("Testing fallback with ipv4");
  Services.dns.clearCache(true);

  setModeAndURI(2, "doh?responseIP=1:2::3");
  const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
    Ci.nsINativeDNSResolverOverride
  );
  gOverride.addIPOverride("ipv4.host.com", "3.4.5.6");

  // Should not fallback to Do53 because A request for ipv4.host.com returns
  // 1:2::3
  let { inStatus } = await new TRRDNSListener("ipv4.host.com", {
    flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
    expectedSuccess: false,
  });
  equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);

  // This time both requests fail, so we do fall back
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=none");
  await new TRRDNSListener("ipv4.host.com", "3.4.5.6");

  // No fallback with strict mode.
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
  setModeAndURI(2, "doh?responseIP=none");
  await new TRRDNSListener("ipv4.host.com", "3.4.5.6");
  Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);

  override.clearOverrides();
}

async function test_no_retry_without_doh() {
  info("Bug 1648147 - if the TRR returns 0.0.0.0 we should not retry with DNS");
  Services.prefs.setBoolPref("network.trr.fallback-on-zero-response", false);
  Services.prefs.setBoolPref("network.socket.ip_addr_any.disabled", false);
  Services.prefs.setBoolPref("network.trr.allow-rfc1918", true);

  async function test(url, ip) {
    setModeAndURI(2, `doh?responseIP=${ip}`);

    // Requests to 0.0.0.0 are usually directed to localhost, so let's use a port
    // we know isn't being used - 666 (Doom)
    let chan = makeChan(url, Ci.nsIRequest.TRR_DEFAULT_MODE);
    let statusCounter = {
      statusCount: {},
      QueryInterface: ChromeUtils.generateQI([
        "nsIInterfaceRequestor",
        "nsIProgressEventSink",
      ]),
      getInterface(iid) {
        return this.QueryInterface(iid);
      },
      onProgress() {},
      onStatus(request, status) {
        this.statusCount[status] = 1 + (this.statusCount[status] || 0);
      },
    };
    chan.notificationCallbacks = statusCounter;
    await new Promise(resolve =>
      chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE))
    );
    equal(
      statusCounter.statusCount[0x4b000b],
      1,
      "Expecting only one instance of NS_NET_STATUS_RESOLVED_HOST"
    );
    equal(
      statusCounter.statusCount[0x4b0007],
      1,
      "Expecting only one instance of NS_NET_STATUS_CONNECTING_TO"
    );
  }

  for (let strictMode of [true, false]) {
    info("Strict mode: " + strictMode);
    Services.prefs.setBoolPref(
      "network.trr.strict_native_fallback",
      strictMode
    );
    await test(`http://unknown.ipv4.stuff:666/path`, "0.0.0.0");
    await test(`http://unknown.ipv6.stuff:666/path`, "::");
  }

  Services.prefs.clearUserPref("network.trr.allow-rfc1918");
  Services.prefs.clearUserPref("network.socket.ip_addr_any.disabled");
}
