"use strict";

const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
  Ci.nsINativeDNSResolverOverride
);
const defaultOriginAttributes = {};
const mainThread = Services.tm.currentThread;

class Listener {
  constructor() {
    this.promise = new Promise(resolve => {
      this.resolve = resolve;
    });
  }

  onLookupComplete(inRequest, inRecord, inStatus) {
    this.resolve([inRequest, inRecord, inStatus]);
  }

  async firstAddress() {
    let all = await this.addresses();
    if (all.length) {
      return all[0];
    }

    return undefined;
  }

  async addresses() {
    let [, inRecord] = await this.promise;
    let addresses = [];
    if (!inRecord) {
      return addresses; // returns []
    }
    inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
    while (inRecord.hasMore()) {
      addresses.push(inRecord.getNextAddrAsString());
    }
    return addresses;
  }

  then() {
    return this.promise.then.apply(this.promise, arguments);
  }
}
Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]);

const DOMAIN = "example.org";
const OTHER = "example.com";

add_setup(async function setup() {
  trr_test_setup();
  // For canon-name flags
  Services.prefs.setBoolPref("network.dns.always_ai_canonname", false);

  registerCleanupFunction(async () => {
    trr_clear_prefs();
  });
});

add_task(async function test_bad_IPs() {
  Assert.throws(
    () => override.addIPOverride(DOMAIN, DOMAIN),
    /NS_ERROR_UNEXPECTED/,
    "Should throw if input is not an IP address"
  );
  Assert.throws(
    () => override.addIPOverride(DOMAIN, ""),
    /NS_ERROR_UNEXPECTED/,
    "Should throw if input is not an IP address"
  );
  Assert.throws(
    () => override.addIPOverride(DOMAIN, " "),
    /NS_ERROR_UNEXPECTED/,
    "Should throw if input is not an IP address"
  );
  Assert.throws(
    () => override.addIPOverride(DOMAIN, "1-2-3-4"),
    /NS_ERROR_UNEXPECTED/,
    "Should throw if input is not an IP address"
  );
});

add_task(async function test_ipv4() {
  let listener = new Listener();
  override.addIPOverride(DOMAIN, "1.2.3.4");
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    0,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  Assert.equal(await listener.firstAddress(), "1.2.3.4");

  Services.dns.clearCache(false);
  override.clearOverrides();
});

add_task(async function test_ipv6() {
  let listener = new Listener();
  override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b");
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    0,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  Assert.equal(await listener.firstAddress(), "fe80::6a99:9b2b:6ccc:6e1b");

  Services.dns.clearCache(false);
  override.clearOverrides();
});

add_task(async function test_clearOverrides() {
  let listener = new Listener();
  override.addIPOverride(DOMAIN, "1.2.3.4");
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    0,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  Assert.equal(await listener.firstAddress(), "1.2.3.4");

  Services.dns.clearCache(false);
  override.clearOverrides();

  listener = new Listener();
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    0,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  Assert.notEqual(await listener.firstAddress(), "1.2.3.4");

  await new Promise(resolve => do_timeout(1000, resolve));
  Services.dns.clearCache(false);
  override.clearOverrides();
});

add_task(async function test_clearHostOverride() {
  override.addIPOverride(DOMAIN, "2.2.2.2");
  override.addIPOverride(OTHER, "2.2.2.2");
  override.clearHostOverride(DOMAIN);
  let listener = new Listener();
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    0,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );

  Assert.notEqual(await listener.firstAddress(), "2.2.2.2");

  listener = new Listener();
  Services.dns.asyncResolve(
    OTHER,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    0,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  Assert.equal(await listener.firstAddress(), "2.2.2.2");

  // Note: this test will use the actual system resolver. On windows we do a
  // second async call to the system libraries to get the TTL values, which
  // keeps the record alive after the onLookupComplete()
  // We need to wait for a bit, until the second call is finished before we
  // can clear the cache to make sure we evict everything.
  // If the next task ever starts failing, with an IP that is not in this
  // file, then likely the timeout is too small.
  await new Promise(resolve => do_timeout(1000, resolve));
  Services.dns.clearCache(false);
  override.clearOverrides();
});

add_task(async function test_multiple_IPs() {
  override.addIPOverride(DOMAIN, "2.2.2.2");
  override.addIPOverride(DOMAIN, "1.1.1.1");
  override.addIPOverride(DOMAIN, "::1");
  override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b");
  let listener = new Listener();
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    0,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  Assert.deepEqual(await listener.addresses(), [
    "2.2.2.2",
    "1.1.1.1",
    "::1",
    "fe80::6a99:9b2b:6ccc:6e1b",
  ]);

  Services.dns.clearCache(false);
  override.clearOverrides();
});

add_task(async function test_address_family_flags() {
  override.addIPOverride(DOMAIN, "2.2.2.2");
  override.addIPOverride(DOMAIN, "1.1.1.1");
  override.addIPOverride(DOMAIN, "::1");
  override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b");
  let listener = new Listener();
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  Assert.deepEqual(await listener.addresses(), [
    "::1",
    "fe80::6a99:9b2b:6ccc:6e1b",
  ]);

  listener = new Listener();
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  Assert.deepEqual(await listener.addresses(), ["2.2.2.2", "1.1.1.1"]);

  Services.dns.clearCache(false);
  override.clearOverrides();
});

add_task(async function test_cname_flag() {
  override.addIPOverride(DOMAIN, "2.2.2.2");
  let listener = new Listener();
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    0,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  let [, inRecord] = await listener;
  inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
  Assert.throws(
    () => inRecord.canonicalName,
    /NS_ERROR_NOT_AVAILABLE/,
    "No canonical name flag"
  );
  Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2");

  listener = new Listener();
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  [, inRecord] = await listener;
  inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
  Assert.equal(inRecord.canonicalName, DOMAIN, "No canonical name specified");
  Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2");

  Services.dns.clearCache(false);
  override.clearOverrides();

  override.addIPOverride(DOMAIN, "2.2.2.2");
  override.setCnameOverride(DOMAIN, OTHER);
  listener = new Listener();
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );
  [, inRecord] = await listener;
  inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
  Assert.equal(inRecord.canonicalName, OTHER, "Must have correct CNAME");
  Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2");

  Services.dns.clearCache(false);
  override.clearOverrides();
});

add_task(async function test_nxdomain() {
  override.addIPOverride(DOMAIN, "N/A");
  let listener = new Listener();
  Services.dns.asyncResolve(
    DOMAIN,
    Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
    null,
    listener,
    mainThread,
    defaultOriginAttributes
  );

  let [, , inStatus] = await listener;
  equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
});

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]);
    }
    chan.asyncOpen(new ChannelListener(finish, null, flags));
  });
}

function hexToUint8Array(hex) {
  return new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}

add_task(
  {
    skip_if: () =>
      mozinfo.os == "win" ||
      mozinfo.os == "android" ||
      mozinfo.socketprocess_networking,
  },
  async function test_https_record_override() {
    let trrServer = new TRRServer();
    await trrServer.start();
    registerCleanupFunction(async () => {
      await trrServer.stop();
    });

    await trrServer.registerDoHAnswers("service.com", "HTTPS", {
      answers: [
        {
          name: "service.com",
          ttl: 55,
          type: "HTTPS",
          flush: false,
          data: {
            priority: 1,
            name: ".",
            values: [
              { key: "alpn", value: ["h2", "h3"] },
              { key: "no-default-alpn" },
              { key: "port", value: 8888 },
              { key: "ipv4hint", value: "1.2.3.4" },
              { key: "echconfig", value: "123..." },
              { key: "ipv6hint", value: "::1" },
              { key: "odoh", value: "456..." },
            ],
          },
        },
        {
          name: "service.com",
          ttl: 55,
          type: "HTTPS",
          flush: false,
          data: {
            priority: 2,
            name: "test.com",
            values: [
              { key: "alpn", value: "h2" },
              { key: "ipv4hint", value: ["1.2.3.4", "5.6.7.8"] },
              { key: "echconfig", value: "abc..." },
              { key: "ipv6hint", value: ["::1", "fe80::794f:6d2c:3d5e:7836"] },
              { key: "odoh", value: "def..." },
            ],
          },
        },
      ],
    });

    let chan = makeChan(
      `https://foo.example.com:${trrServer.port()}/dnsAnswer?host=service.com&type=HTTPS`
    );
    let [, buf] = await channelOpenPromise(chan);
    let rawBuffer = hexToUint8Array(buf);

    override.addHTTPSRecordOverride("service.com", rawBuffer, rawBuffer.length);

    Services.prefs.setBoolPref("network.dns.native_https_query", true);
    registerCleanupFunction(async () => {
      Services.prefs.clearUserPref("network.dns.native_https_query");
      Services.prefs.clearUserPref("network.trr.excluded-domains");
    });

    let listener = new Listener();
    Services.dns.asyncResolve(
      "service.com",
      Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
      0,
      null,
      listener,
      mainThread,
      defaultOriginAttributes
    );

    let [, inRecord, inStatus] = await listener;
    equal(inStatus, Cr.NS_OK);
    Assert.ok(
      !inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).IsTRR(),
      "resolved by Native"
    );
    let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
    equal(answer[0].priority, 1);
    equal(answer[0].name, "service.com");
    Assert.deepEqual(
      answer[0].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn,
      ["h2", "h3"],
      "got correct answer"
    );
    Assert.ok(
      answer[0].values[1].QueryInterface(Ci.nsISVCParamNoDefaultAlpn),
      "got correct answer"
    );
    Assert.equal(
      answer[0].values[2].QueryInterface(Ci.nsISVCParamPort).port,
      8888,
      "got correct answer"
    );
    Assert.equal(
      answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0]
        .address,
      "1.2.3.4",
      "got correct answer"
    );
    Assert.equal(
      answer[0].values[4].QueryInterface(Ci.nsISVCParamEchConfig).echconfig,
      "123...",
      "got correct answer"
    );
    Assert.equal(
      answer[0].values[5].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0]
        .address,
      "::1",
      "got correct answer"
    );
    Assert.equal(
      answer[0].values[6].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig,
      "456...",
      "got correct answer"
    );

    Assert.equal(answer[1].priority, 2);
    Assert.equal(answer[1].name, "test.com");
    Assert.equal(answer[1].values.length, 5);
    Assert.deepEqual(
      answer[1].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn,
      ["h2"],
      "got correct answer"
    );
    Assert.equal(
      answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0]
        .address,
      "1.2.3.4",
      "got correct answer"
    );
    Assert.equal(
      answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[1]
        .address,
      "5.6.7.8",
      "got correct answer"
    );
    Assert.equal(
      answer[1].values[2].QueryInterface(Ci.nsISVCParamEchConfig).echconfig,
      "abc...",
      "got correct answer"
    );
    Assert.equal(
      answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0]
        .address,
      "::1",
      "got correct answer"
    );
    Assert.equal(
      answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[1]
        .address,
      "fe80::794f:6d2c:3d5e:7836",
      "got correct answer"
    );
    Assert.equal(
      answer[1].values[4].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig,
      "def...",
      "got correct answer"
    );

    // Adding "service.com" into excluded-domains should fail
    // native HTTPS query.
    Services.prefs.setCharPref("network.trr.excluded-domains", "service.com");
    listener = new Listener();
    try {
      Services.dns.asyncResolve(
        "service.com",
        Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
        0,
        null,
        listener,
        mainThread,
        defaultOriginAttributes
      );
      Assert.ok(false, "asyncResolve should fail");
    } catch (e) {
      Assert.equal(e.result, Cr.NS_ERROR_UNKNOWN_HOST);
    }
  }
);
