"use strict";

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

var h2Port;
var h3Port;

// https://foo.example.com:(h3Port)
// https://bar.example.com:(h3Port) <- invalid for bar, but ok for foo
var h1Foo; // server http://foo.example.com:(h1Foo.identity.primaryPort)
var h1Bar; // server http://bar.example.com:(h1bar.identity.primaryPort)

var otherServer; // server socket listening for other connection.

var h2FooRoute; // foo.example.com:H2PORT

var h3FooRoute; // foo.example.com:H3PORT
var h3BarRoute; // bar.example.com:H3PORT
var h3Route; // :H3PORT
var httpFooOrigin; // http://foo.exmaple.com:PORT/
var httpsFooOrigin; // https://foo.exmaple.com:PORT/
var httpBarOrigin; // http://bar.example.com:PORT/
var httpsBarOrigin; // https://bar.example.com:PORT/

function run_test() {
  h2Port = Services.env.get("MOZHTTP2_PORT");
  Assert.notEqual(h2Port, null);
  Assert.notEqual(h2Port, "");

  h3Port = Services.env.get("MOZHTTP3_PORT");
  Assert.notEqual(h3Port, null);
  Assert.notEqual(h3Port, "");

  // Set to allow the cert presented by our H3 server
  do_get_profile();

  Services.prefs.setBoolPref("network.http.http2.enabled", true);
  Services.prefs.setBoolPref("network.http.http3.enable", true);
  Services.prefs.setBoolPref("network.http.altsvc.enabled", true);
  Services.prefs.setBoolPref("network.http.altsvc.oe", true);
  Services.prefs.setCharPref(
    "network.dns.localDomains",
    "foo.example.com, bar.example.com"
  );

  // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
  // so add that cert to the trust list as a signing cert. The same cert is used
  // for both h3FooRoute and h3BarRoute though it is only valid for
  // the foo.example.com domain name.
  let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    Ci.nsIX509CertDB
  );
  addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");

  h1Foo = new HttpServer();
  h1Foo.registerPathHandler("/altsvc-test", h1Server);
  h1Foo.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
  h1Foo.start(-1);
  h1Foo.identity.setPrimary(
    "http",
    "foo.example.com",
    h1Foo.identity.primaryPort
  );

  h1Bar = new HttpServer();
  h1Bar.registerPathHandler("/altsvc-test", h1Server);
  h1Bar.start(-1);
  h1Bar.identity.setPrimary(
    "http",
    "bar.example.com",
    h1Bar.identity.primaryPort
  );

  h2FooRoute = "foo.example.com:" + h2Port;

  h3FooRoute = "foo.example.com:" + h3Port;
  h3BarRoute = "bar.example.com:" + h3Port;
  h3Route = ":" + h3Port;

  httpFooOrigin = "http://foo.example.com:" + h1Foo.identity.primaryPort + "/";
  httpsFooOrigin = "https://" + h3FooRoute + "/";
  httpBarOrigin = "http://bar.example.com:" + h1Bar.identity.primaryPort + "/";
  httpsBarOrigin = "https://" + h3BarRoute + "/";
  dump(
    "http foo - " +
      httpFooOrigin +
      "\n" +
      "https foo - " +
      httpsFooOrigin +
      "\n" +
      "http bar - " +
      httpBarOrigin +
      "\n" +
      "https bar - " +
      httpsBarOrigin +
      "\n"
  );

  doTest1();
}

function h1Server(metadata, response) {
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.setHeader("Content-Type", "text/plain", false);
  response.setHeader("Connection", "close", false);
  response.setHeader("Cache-Control", "no-cache", false);
  response.setHeader("Access-Control-Allow-Origin", "*", false);
  response.setHeader("Access-Control-Allow-Method", "GET", false);
  response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);

  try {
    // If needed, prefix Alt-Svc with "h3=".
    if (metadata.getHeader("x-altsvc").includes("=")) {
      response.setHeader("Alt-Svc", metadata.getHeader("x-altsvc"), false);
    } else {
      response.setHeader(
        "Alt-Svc",
        "h3=" + metadata.getHeader("x-altsvc"),
        false
      );
    }
  } catch (e) {}

  var body = "Q: What did 0 say to 8? A: Nice Belt!\n";
  response.bodyOutputStream.write(body, body.length);
}

function h1ServerWK(metadata, response) {
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.setHeader("Content-Type", "application/json", false);
  response.setHeader("Connection", "close", false);
  response.setHeader("Cache-Control", "no-cache", false);
  response.setHeader("Access-Control-Allow-Origin", "*", false);
  response.setHeader("Access-Control-Allow-Method", "GET", false);
  response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);

  var body = '["http://foo.example.com:' + h1Foo.identity.primaryPort + '"]';
  response.bodyOutputStream.write(body, body.length);
}

function resetPrefs() {
  Services.prefs.clearUserPref("network.http.http2.enabled");
  Services.prefs.clearUserPref("network.http.http3.enable");
  Services.prefs.clearUserPref("network.dns.localDomains");
  Services.prefs.clearUserPref("network.http.altsvc.enabled");
  Services.prefs.clearUserPref("network.http.altsvc.oe");
  Services.prefs.clearUserPref("network.dns.localDomains");
  Services.prefs.clearUserPref("network.security.ports.banned");
}

function makeChan(origin) {
  return NetUtil.newChannel({
    uri: origin + "altsvc-test",
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);
}

var origin;
var xaltsvc;
var loadWithoutClearingMappings = false;
var disallowH3 = false;
var disallowH2 = false;
var testKeepAliveNotSet = false;
var nextTest;
var expectPass = true;
var waitFor = 0;
var originAttributes = {};

var Listener = function (expectedHttpVersion, expectedRoute) {
  this._expectedRoute = expectedRoute;
  this._expectedHttpVersion = expectedHttpVersion;
};

Listener.prototype = {
  onStartRequest: function testOnStartRequest(request) {
    Assert.ok(request instanceof Ci.nsIHttpChannel);

    if (expectPass) {
      if (!Components.isSuccessCode(request.status)) {
        do_throw(
          "Channel should have a success code! (" + request.status + ")"
        );
      }
      Assert.equal(request.responseStatus, 200);
    } else {
      Assert.equal(Components.isSuccessCode(request.status), false);
    }
  },

  onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
    read_stream(stream, cnt);
  },

  onStopRequest: function testOnStopRequest(request, status) {
    var routed = "";
    try {
      routed = request.getRequestHeader("Alt-Used");
    } catch (e) {}
    dump("routed is " + routed + "\n");
    Assert.equal(Components.isSuccessCode(status), expectPass);

    function assertHttpVersion(request, expectedHttpVersion) {
      if (expectedHttpVersion) {
        const httpVersion = request?.protocolVersion || "";
        Assert.equal(httpVersion, httpVersion);
      }
    }

    if (waitFor != 0) {
      Assert.equal(routed, "");
      do_test_pending();
      loadWithoutClearingMappings = true;
      do_timeout(waitFor, () => {
        doTest(this._expectedHttpVersion, this._expectedRoute);
      });
      waitFor = 0;
      xaltsvc = "NA";
    } else if (xaltsvc == "NA") {
      Assert.equal(routed, "");
      nextTest();
    } else if (this._expectedRoute && this._expectedRoute == routed) {
      assertHttpVersion(request, this._expectedHttpVersion);
      nextTest();
    } else if (routed == xaltsvc) {
      Assert.equal(routed, xaltsvc); // always true, but a useful log
      assertHttpVersion(request, this._expectedHttpVersion);
      nextTest();
    } else {
      dump("poll later for alt svc mapping\n");
      do_test_pending();
      loadWithoutClearingMappings = true;
      do_timeout(500, () => {
        doTest(this._expectedHttpVersion, this._expectedRoute);
      });
    }

    do_test_finished();
  },
};

function testsDone() {
  dump("testDone\n");
  resetPrefs();
  do_test_pending();
  otherServer.close();
  do_test_pending();
  h1Foo.stop(do_test_finished);
  do_test_pending();
  h1Bar.stop(do_test_finished);
}

function doTest(expectedHttpVersion, expectedRoute) {
  dump("execute doTest " + origin + "\n");
  var chan = makeChan(origin);
  var listener = new Listener(expectedHttpVersion, expectedRoute);
  if (xaltsvc != "NA") {
    chan.setRequestHeader("x-altsvc", xaltsvc, false);
  }
  if (testKeepAliveNotSet) {
    chan.setRequestHeader("Connection", "close", false);
    testKeepAliveNotSet = false;
  }
  if (loadWithoutClearingMappings) {
    chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
  } else {
    chan.loadFlags =
      Ci.nsIRequest.LOAD_FRESH_CONNECTION |
      Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
  }
  if (disallowH3) {
    let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
    internalChannel.allowHttp3 = false;
    disallowH3 = false;
  }
  if (disallowH2) {
    let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
    internalChannel.allowSpdy = false;
    disallowH2 = false;
  }
  loadWithoutClearingMappings = false;
  chan.loadInfo.originAttributes = originAttributes;
  chan.asyncOpen(listener);
}

// xaltsvc is overloaded to do two things..
// 1] it is sent in the x-altsvc request header, and the response uses the value in the Alt-Svc response header
// 2] the test polls until necko sets Alt-Used to that value (i.e. it uses that route)
//
// When xaltsvc is set to h3Route (i.e. :port with the implied hostname) it doesn't match the alt-used,
// which is always explicit, so it needs to be changed after the channel is created but before the
// listener is invoked

// http://foo served from h3=:port
function doTest1() {
  dump("doTest1()\n");
  origin = httpFooOrigin;
  xaltsvc = h3Route;
  nextTest = doTest2;
  do_test_pending();
  doTest("h3");
  xaltsvc = h3FooRoute;
}

// http://foo served from h3=foo:port
function doTest2() {
  dump("doTest2()\n");
  origin = httpFooOrigin;
  xaltsvc = h3FooRoute;
  nextTest = doTest3;
  do_test_pending();
  doTest("h3");
}

// http://foo served from h3=bar:port
// requires cert for foo
function doTest3() {
  dump("doTest3()\n");
  origin = httpFooOrigin;
  xaltsvc = h3BarRoute;
  nextTest = doTest4;
  do_test_pending();
  doTest("h3");
}

// https://bar should fail because host bar has cert for foo
function doTest4() {
  dump("doTest4()\n");
  origin = httpsBarOrigin;
  xaltsvc = "";
  expectPass = false;
  nextTest = doTest5;
  do_test_pending();
  doTest();
}

// http://bar via h3 on bar
// should not use TLS/h3 because h3BarRoute is not auth'd for bar
// however the test ought to PASS (i.e. get a 200) because fallback
// to plaintext happens.. thus the timeout
function doTest5() {
  dump("doTest5()\n");
  origin = httpBarOrigin;
  xaltsvc = h3BarRoute;
  expectPass = true;
  waitFor = 500;
  nextTest = doTest6;
  do_test_pending();
  doTest("h3");
}

// http://bar served from h3=:port, which is like the bar route in 8
function doTest6() {
  dump("doTest6()\n");
  origin = httpBarOrigin;
  xaltsvc = h3Route;
  expectPass = true;
  waitFor = 500;
  nextTest = doTest7;
  do_test_pending();
  doTest();
  xaltsvc = h3BarRoute;
}

// check again https://bar should fail because host bar has cert for foo
function doTest7() {
  dump("doTest7()\n");
  origin = httpsBarOrigin;
  xaltsvc = "";
  expectPass = false;
  nextTest = doTest8;
  do_test_pending();
  doTest();
}

// http://bar served from h3=foo, should fail because host foo only has
// cert for foo. Fail in this case means alt-svc is not used, but content
// is served
function doTest8() {
  dump("doTest8()\n");
  origin = httpBarOrigin;
  xaltsvc = h3FooRoute;
  expectPass = true;
  waitFor = 500;
  nextTest = doTest9;
  do_test_pending();
  doTest("h3");
}

// Test 9-12:
// Insert a cache of http://foo served from h3=:port with origin attributes.
function doTest9() {
  dump("doTest9()\n");
  origin = httpFooOrigin;
  xaltsvc = h3Route;
  originAttributes = {
    userContextId: 1,
    firstPartyDomain: "a.com",
  };
  nextTest = doTest10;
  do_test_pending();
  doTest("h3");
  xaltsvc = h3FooRoute;
}

// Make sure we get a cache miss with a different userContextId.
function doTest10() {
  dump("doTest10()\n");
  origin = httpFooOrigin;
  xaltsvc = "NA";
  originAttributes = {
    userContextId: 2,
    firstPartyDomain: "a.com",
  };
  loadWithoutClearingMappings = true;
  nextTest = doTest11;
  do_test_pending();
  doTest("h3");
}

// Make sure we get a cache miss with a different firstPartyDomain.
function doTest11() {
  dump("doTest11()\n");
  origin = httpFooOrigin;
  xaltsvc = "NA";
  originAttributes = {
    userContextId: 1,
    firstPartyDomain: "b.com",
  };
  loadWithoutClearingMappings = true;
  nextTest = doTest12;
  do_test_pending();
  doTest("h3");
}
//
// Make sure we get a cache hit with the same origin attributes.
function doTest12() {
  dump("doTest12()\n");
  origin = httpFooOrigin;
  xaltsvc = "NA";
  originAttributes = {
    userContextId: 1,
    firstPartyDomain: "a.com",
  };
  loadWithoutClearingMappings = true;
  nextTest = doTest13;
  do_test_pending();
  doTest("h3");
  // This ensures a cache hit.
  xaltsvc = h3FooRoute;
}

// Make sure we do not use H3 if it is disabled on a channel.
function doTest13() {
  dump("doTest13()\n");
  origin = httpFooOrigin;
  xaltsvc = "NA";
  disallowH3 = true;
  originAttributes = {
    userContextId: 1,
    firstPartyDomain: "a.com",
  };
  loadWithoutClearingMappings = true;
  nextTest = doTest14;
  do_test_pending();
  doTest("h3");
}

// Make sure we use H3 if only Http2 is disabled on a channel.
function doTest14() {
  dump("doTest14()\n");
  origin = httpFooOrigin;
  xaltsvc = "NA";
  disallowH2 = true;
  originAttributes = {
    userContextId: 1,
    firstPartyDomain: "a.com",
  };
  loadWithoutClearingMappings = true;
  nextTest = doTest15;
  do_test_pending();
  doTest("h3");
  // This should ensures a cache hit.
  xaltsvc = h3FooRoute;
}

// Make sure we do not use H3 if NS_HTTP_ALLOW_KEEPALIVE is not set.
function doTest15() {
  dump("doTest15()\n");
  origin = httpFooOrigin;
  xaltsvc = "NA";
  testKeepAliveNotSet = true;
  originAttributes = {
    userContextId: 1,
    firstPartyDomain: "a.com",
  };
  loadWithoutClearingMappings = true;
  nextTest = doTest16;
  do_test_pending();
  doTest("h3");
}

// Check we don't connect to blocked ports
function doTest16() {
  dump("doTest16()\n");
  origin = httpFooOrigin;
  otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance(
    Ci.nsIServerSocket
  );
  otherServer.init(-1, true, -1);
  xaltsvc = "localhost:" + otherServer.port;
  Services.prefs.setCharPref(
    "network.security.ports.banned",
    "" + otherServer.port
  );
  dump("Blocked port: " + otherServer.port);
  waitFor = 500;
  otherServer.asyncListen({
    onSocketAccepted() {
      Assert.ok(false, "Got connection to socket when we didn't expect it!");
    },
    onStopListening() {
      // We get closed when the entire file is done, which guarantees we get the socket accept
      // if we do connect to the alt-svc header
      do_test_finished();
    },
  });
  nextTest = doTest17;
  do_test_pending();
  doTest("h3");
}

// Make sure we do not use a draft QUIC version.
function doTest17() {
  dump("doTest17()\n");
  origin = httpFooOrigin;
  nextTest = testsDone;
  xaltsvc = "h3-29=" + h3FooRoute + ", h2=" + h2FooRoute;
  disallowH2 = false;
  originAttributes = {
    userContextId: 1,
    firstPartyDomain: "a.com",
  };
  loadWithoutClearingMappings = true;
  do_test_pending();
  doTest("h2", h2FooRoute);
}
