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

let h2Port;

const certOverrideService = Cc[
  "@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
const { TestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TestUtils.sys.mjs"
);

let trrServer;
add_setup(async function setup() {
  trr_test_setup();

  trrServer = new TRRServer();
  await trrServer.start();
  h2Port = trrServer.port();

  Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
  Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
  Services.prefs.setBoolPref(
    "network.dns.https_rr.check_record_with_cname",
    false
  );

  registerCleanupFunction(async () => {
    trr_clear_prefs();
    Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
    Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
    Services.prefs.clearUserPref(
      "network.dns.https_rr.check_record_with_cname"
    );
    await trrServer.stop();
  });

  if (mozinfo.socketprocess_networking) {
    Services.dns; // Needed to trigger socket process.
    await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
  }

  Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
});

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

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

// This is for testing when the HTTPSSVC record is not available when
// the transaction is added in connection manager.
add_task(async function testUseHTTPSSVCForHttpsUpgrade() {
  // use the h2 server as DOH provider
  Services.prefs.setCharPref(
    "network.trr.uri",
    "https://foo.example.com:" + h2Port + "/doh?httpssvc_as_altsvc=1"
  );
  Services.dns.clearCache(true);

  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    true
  );

  let chan = makeChan(`https://test.httpssvc.com:8080/`);
  let [req] = await channelOpenPromise(chan);
  Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");

  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    false
  );
});

class EventSinkListener {
  getInterface(iid) {
    if (iid.equals(Ci.nsIChannelEventSink)) {
      return this;
    }
    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
  }
  asyncOnChannelRedirect(oldChan, newChan, flags, callback) {
    Assert.equal(oldChan.URI.hostPort, newChan.URI.hostPort);
    Assert.equal(oldChan.URI.scheme, "http");
    Assert.equal(newChan.URI.scheme, "https");
    callback.onRedirectVerifyCallback(Cr.NS_OK);
  }
}

EventSinkListener.prototype.QueryInterface = ChromeUtils.generateQI([
  "nsIInterfaceRequestor",
  "nsIChannelEventSink",
]);

// Test if the request is upgraded to https with a HTTPSSVC record.
add_task(async function testUseHTTPSSVCAsHSTS() {
  // use the h2 server as DOH provider
  Services.prefs.setCharPref(
    "network.trr.uri",
    "https://foo.example.com:" + h2Port + "/doh?httpssvc_as_altsvc=1"
  );
  Services.dns.clearCache(true);

  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    true
  );

  // At this time, the DataStorage is not ready, so MaybeUseHTTPSRRForUpgrade()
  // is called from the callback of NS_ShouldSecureUpgrade().
  let chan = makeChan(`http://test.httpssvc.com:80/`);
  let listener = new EventSinkListener();
  chan.notificationCallbacks = listener;

  let [req] = await channelOpenPromise(chan);

  req.QueryInterface(Ci.nsIHttpChannel);
  Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");

  // At this time, the DataStorage is ready, so MaybeUseHTTPSRRForUpgrade()
  // is called from nsHttpChannel::OnBeforeConnect().
  chan = makeChan(`http://test.httpssvc.com:80/`);
  listener = new EventSinkListener();
  chan.notificationCallbacks = listener;

  [req] = await channelOpenPromise(chan);

  req.QueryInterface(Ci.nsIHttpChannel);
  Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");

  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    false
  );
});

// This is for testing when the HTTPSSVC record is already available before
// the transaction is added in connection manager.
add_task(async function testUseHTTPSSVC() {
  // use the h2 server as DOH provider
  Services.prefs.setCharPref(
    "network.trr.uri",
    "https://foo.example.com:" + h2Port + "/doh?httpssvc_as_altsvc=1"
  );

  // Do DNS resolution before creating the channel, so the HTTPSSVC record will
  // be resolved from the cache.
  await new TRRDNSListener("test.httpssvc.com", {
    type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
  });

  // We need to skip the security check, since our test cert is signed for
  // foo.example.com, not test.httpssvc.com.
  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    true
  );

  let chan = makeChan(`https://test.httpssvc.com:8888`);
  let [req] = await channelOpenPromise(chan);
  // Test if this request is done by h2.
  Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");

  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    false
  );
});

// Test if we can successfully fallback to the original host and port.
add_task(async function testFallback() {
  Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
  Services.prefs.setCharPref(
    "network.trr.uri",
    `https://foo.example.com:${trrServer.port()}/dns-query`
  );

  await trrServer.registerDoHAnswers("test.fallback.com", "A", {
    answers: [
      {
        name: "test.fallback.com",
        ttl: 55,
        type: "A",
        flush: false,
        data: "127.0.0.1",
      },
    ],
  });
  // Use a wrong port number 8888, so this connection will be refused.
  await trrServer.registerDoHAnswers("test.fallback.com", "HTTPS", {
    answers: [
      {
        name: "test.fallback.com",
        ttl: 55,
        type: "HTTPS",
        flush: false,
        data: {
          priority: 1,
          name: "foo.example.com",
          values: [{ key: "port", value: 8888 }],
        },
      },
    ],
  });

  let { inRecord } = await new TRRDNSListener("test.fallback.com", {
    type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
  });

  let record = inRecord
    .QueryInterface(Ci.nsIDNSHTTPSSVCRecord)
    .GetServiceModeRecord(false, false);
  Assert.equal(record.priority, 1);
  Assert.equal(record.name, "foo.example.com");

  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    true
  );

  // When the connection with port 8888 failed, the correct h2Port will be used
  // to connect again.
  let chan = makeChan(`https://test.fallback.com:${h2Port}`);
  let [req] = await channelOpenPromise(chan);
  // Test if this request is done by h2.
  Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");

  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    false
  );
});

// When the origin's HTTPS record is an AliasMode record whose target has no
// HTTPS record of its own, we must still connect to the target (bug 2056195).
// Only the target has an address record, so a successful connection proves the
// alias was followed and the connection was routed to the target.
add_task(async function testConnectToAliasTargetWithoutRecord() {
  Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
  Services.prefs.setCharPref(
    "network.trr.uri",
    `https://foo.example.com:${trrServer.port()}/dns-query`
  );
  Services.dns.clearCache(true);

  // The HTTPS RR query for a non-default port is port-prefixed.
  let originQuery = `_${h2Port}._https.alias-origin.com`;

  // origin is an HTTPS AliasMode record pointing at the target.
  await trrServer.registerDoHAnswers(originQuery, "HTTPS", {
    answers: [
      {
        name: originQuery,
        ttl: 55,
        type: "HTTPS",
        flush: false,
        data: { priority: 0, name: "alias-target.com", values: [] },
      },
    ],
  });
  // The target has no HTTPS record of its own (NODATA)...
  await trrServer.registerDoHAnswers("alias-target.com", "HTTPS", {
    answers: [],
  });
  // ...but it does resolve to the test server's address. The origin has no
  // address record, so the connection can only succeed via the target.
  await trrServer.registerDoHAnswers("alias-target.com", "A", {
    answers: [
      {
        name: "alias-target.com",
        ttl: 55,
        type: "A",
        flush: false,
        data: "127.0.0.1",
      },
    ],
  });

  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    true
  );

  let chan = makeChan(`https://alias-origin.com:${h2Port}/`);
  let [req] = await channelOpenPromise(chan);
  // The connection succeeded over h2, i.e. we reached the target's address.
  Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");

  certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
    false
  );
});
