/* 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/. */

// This module is the stateful server side of test_http2.js and is meant
// to have node be restarted in between each invocation

/* eslint-env node */

var node_http2_root = "../node-http2";
if (process.env.NODE_HTTP2_ROOT) {
  node_http2_root = process.env.NODE_HTTP2_ROOT;
}
var http2 = require(node_http2_root);
var fs = require("fs");
var url = require("url");
var crypto = require("crypto");
const dnsPacket = require(`${node_http2_root}/../dns-packet`);
const ip = require(`${node_http2_root}/../node_ip`);
const { fork } = require("child_process");
const { spawn } = require("child_process");
const path = require("path");
const zlib = require("zlib");

// Hook into the decompression code to log the decompressed name-value pairs
var compression_module = node_http2_root + "/lib/protocol/compressor";
var http2_compression = require(compression_module);
var HeaderSetDecompressor = http2_compression.HeaderSetDecompressor;
var originalRead = HeaderSetDecompressor.prototype.read;
var lastDecompressor;
var decompressedPairs;
HeaderSetDecompressor.prototype.read = function () {
  if (this != lastDecompressor) {
    lastDecompressor = this;
    decompressedPairs = [];
  }
  var pair = originalRead.apply(this, arguments);
  if (pair) {
    decompressedPairs.push(pair);
  }
  return pair;
};

var connection_module = node_http2_root + "/lib/protocol/connection";
var http2_connection = require(connection_module);
var Connection = http2_connection.Connection;
var originalClose = Connection.prototype.close;
Connection.prototype.close = function (error, lastId) {
  if (lastId !== undefined) {
    this._lastIncomingStream = lastId;
  }

  originalClose.apply(this, arguments);
};

var framer_module = node_http2_root + "/lib/protocol/framer";
var http2_framer = require(framer_module);
var Serializer = http2_framer.Serializer;
var originalTransform = Serializer.prototype._transform;
var newTransform = function (frame) {
  if (frame.type == "DATA") {
    // Insert our empty DATA frame
    const emptyFrame = {};
    emptyFrame.type = "DATA";
    emptyFrame.data = Buffer.alloc(0);
    emptyFrame.flags = [];
    emptyFrame.stream = frame.stream;
    var buffers = [];
    Serializer.DATA(emptyFrame, buffers);
    Serializer.commonHeader(emptyFrame, buffers);
    for (var i = 0; i < buffers.length; i++) {
      this.push(buffers[i]);
    }

    // Reset to the original version for later uses
    Serializer.prototype._transform = originalTransform;
  }
  originalTransform.apply(this, arguments);
};

function getHttpContent(pathName) {
  var content =
    "<!doctype html>" +
    "<html>" +
    "<head><title>HOORAY!</title></head>" +
    // 'You Win!' used in tests to check we reached this server
    "<body>You Win! (by requesting" +
    pathName +
    ")</body>" +
    "</html>";
  return content;
}

function generateContent(size) {
  var content = "";
  for (var i = 0; i < size; i++) {
    content += "0";
  }
  return content;
}

/* This takes care of responding to the multiplexed request for us */
var m = {
  mp1res: null,
  mp2res: null,
  buf: null,
  mp1start: 0,
  mp2start: 0,

  checkReady() {
    if (this.mp1res != null && this.mp2res != null) {
      this.buf = generateContent(30 * 1024);
      this.mp1start = 0;
      this.mp2start = 0;
      this.send(this.mp1res, 0);
      setTimeout(this.send.bind(this, this.mp2res, 0), 5);
    }
  },

  send(res, start) {
    var end = Math.min(start + 1024, this.buf.length);
    var content = this.buf.substring(start, end);
    res.write(content);
    if (end < this.buf.length) {
      setTimeout(this.send.bind(this, res, end), 10);
    } else {
      // Clear these variables so we can run the test again with --verify
      if (res == this.mp1res) {
        this.mp1res = null;
      } else {
        this.mp2res = null;
      }
      res.end();
    }
  },
};

var runlater = function () {};
runlater.prototype = {
  req: null,
  resp: null,
  fin: true,

  onTimeout: function onTimeout() {
    this.resp.writeHead(200);
    if (this.fin) {
      this.resp.end("It's all good 750ms.");
    }
  },
};

var runConnectLater = function () {};
runConnectLater.prototype = {
  req: null,
  resp: null,
  connect: false,

  onTimeout: function onTimeout() {
    if (this.connect) {
      this.resp.writeHead(200);
      this.connect = true;
      setTimeout(executeRunLaterCatchError, 50, this);
    } else {
      this.resp.end("HTTP/1.1 200\n\r\n\r");
    }
  },
};

var moreData = function () {};
moreData.prototype = {
  req: null,
  resp: null,
  iter: 3,

  onTimeout: function onTimeout() {
    // 1mb of data
    const content = generateContent(1024 * 1024);
    this.resp.write(content); // 1mb chunk
    this.iter--;
    if (!this.iter) {
      this.resp.end();
    } else {
      setTimeout(executeRunLater, 1, this);
    }
  },
};

function executeRunLater(arg) {
  arg.onTimeout();
}

function executeRunLaterCatchError(arg) {
  arg.onTimeout();
}

var h11required_conn = null;
var h11required_header = "yes";
var didRst = false;
var rstConnection = null;
var illegalheader_conn = null;

var gDoHPortsLog = [];
var gDoHNewConnLog = {};
var gDoHRequestCount = 0;

// eslint-disable-next-line complexity
function handleRequest(req, res) {
  var u = "";
  if (req.url != undefined) {
    u = url.parse(req.url, true);
  }
  var content = getHttpContent(u.pathname);
  var push, push1, push1a, push2, push3;

  // PushService tests.
  var pushPushServer1, pushPushServer2, pushPushServer3, pushPushServer4;

  function createCNameContent(payload) {
    let packet = dnsPacket.decode(payload);
    if (
      packet.questions[0].name == "cname.example.com" &&
      packet.questions[0].type == "A"
    ) {
      return dnsPacket.encode({
        id: 0,
        type: "response",
        flags: dnsPacket.RECURSION_DESIRED,
        questions: [{ name: packet.questions[0].name, type: "A", class: "IN" }],
        answers: [
          {
            name: packet.questions[0].name,
            ttl: 55,
            type: "CNAME",
            flush: false,
            data: "pointing-elsewhere.example.com",
          },
        ],
      });
    }
    if (
      packet.questions[0].name == "pointing-elsewhere.example.com" &&
      packet.questions[0].type == "A"
    ) {
      return dnsPacket.encode({
        id: 0,
        type: "response",
        flags: dnsPacket.RECURSION_DESIRED,
        questions: [{ name: packet.questions[0].name, type: "A", class: "IN" }],
        answers: [
          {
            name: packet.questions[0].name,
            ttl: 55,
            type: "A",
            flush: false,
            data: "99.88.77.66",
          },
        ],
      });
    }

    return dnsPacket.encode({
      id: 0,
      type: "response",
      flags: dnsPacket.RECURSION_DESIRED | dnsPacket.rcodes.toRcode("NXDOMAIN"),
      questions: [
        {
          name: packet.questions[0].name,
          type: packet.questions[0].type,
          class: "IN",
        },
      ],
      answers: [],
    });
  }

  function createCNameARecord() {
    // test23 asks for cname-a.example.com
    // this responds with a CNAME to here.example.com *and* an A record
    // for here.example.com
    let rContent;

    rContent = Buffer.from(
      "0000" +
        "0100" +
        "0001" + // QDCOUNT
        "0002" + // ANCOUNT
        "00000000" + // NSCOUNT + ARCOUNT
        "07636E616D652d61" + // cname-a
        "076578616D706C6503636F6D00" + // .example.com
        "00010001" + // question type (A) + question class (IN)
        // answer record 1
        "C00C" + // name pointer to cname-a.example.com
        "0005" + // type (CNAME)
        "0001" + // class
        "00000037" + // TTL
        "0012" + // RDLENGTH
        "0468657265" + // here
        "076578616D706C6503636F6D00" + // .example.com
        // answer record 2, the A entry for the CNAME above
        "0468657265" + // here
        "076578616D706C6503636F6D00" + // .example.com
        "0001" + // type (A)
        "0001" + // class
        "00000037" + // TTL
        "0004" + // RDLENGTH
        "09080706", // IPv4 address
      "hex"
    );

    return rContent;
  }

  function responseType(packet, responseIP) {
    if (
      !!packet.questions.length &&
      packet.questions[0].name == "confirm.example.com" &&
      packet.questions[0].type == "NS"
    ) {
      return "NS";
    }

    return ip.isV4Format(responseIP) ? "A" : "AAAA";
  }

  function handleAuth() {
    // There's a Set-Cookie: header in the response for "/dns" , which this
    // request subsequently would include if the http channel wasn't
    // anonymous. Thus, if there's a cookie in this request, we know Firefox
    // mishaved. If there's not, we're fine.
    if (req.headers.cookie) {
      res.writeHead(403);
      res.end("cookie for me, not for you");
      return false;
    }
    if (req.headers.authorization != "user:password") {
      res.writeHead(401);
      res.end("bad boy!");
      return false;
    }

    return true;
  }

  function createDNSAnswer(response, packet, responseIP, requestPayload) {
    // This shuts down the connection so we can test if the client reconnects
    if (packet.questions.length && packet.questions[0].name == "closeme.com") {
      response.stream.connection.close("INTERNAL_ERROR", response.stream.id);
      return null;
    }

    let answers = [];
    if (packet.questions.length && packet.questions[0].name.endsWith(".pd")) {
      // Bug 1543811: test edns padding extension. Return whether padding was
      // included via the first half of the ip address (1.1 vs 2.2) and the
      // size of the request in the second half of the ip address allowing to
      // verify that the correct amount of padding was added.
      if (
        !!packet.additionals.length &&
        packet.additionals[0].type == "OPT" &&
        packet.additionals[0].options.some(o => o.type === "PADDING")
      ) {
        // add padding to the response, because the client must be able ignore it
        answers.push({
          name: ".",
          type: "PADDING",
          data: Buffer.from(
            // PADDING_PADDING_PADDING
            "50414444494e475f50414444494e475f50414444494e47",
            "hex"
          ),
        });
        responseIP =
          "1.1." +
          ((requestPayload.length >> 8) & 0xff) +
          "." +
          (requestPayload.length & 0xff);
      } else {
        responseIP =
          "2.2." +
          ((requestPayload.length >> 8) & 0xff) +
          "." +
          (requestPayload.length & 0xff);
      }
    }

    if (u.query.corruptedAnswer) {
      // DNS response header is 12 bytes, we check for this minimum length
      // at the start of decoding so this is the simplest way to force
      // a decode error.
      return "\xFF\xFF\xFF\xFF";
    }

    // Because we send two TRR requests (A and AAAA), skip the first two
    // requests when testing retry.
    if (u.query.retryOnDecodeFailure && gDoHRequestCount < 2) {
      gDoHRequestCount++;
      return "\xFF\xFF\xFF\xFF";
    }

    function responseData() {
      if (
        !!packet.questions.length &&
        packet.questions[0].name == "confirm.example.com" &&
        packet.questions[0].type == "NS"
      ) {
        return "ns.example.com";
      }

      return responseIP;
    }

    if (
      responseIP != "none" &&
      responseType(packet, responseIP) == packet.questions[0].type
    ) {
      answers.push({
        name: u.query.hostname ? u.query.hostname : packet.questions[0].name,
        ttl: 55,
        type: responseType(packet, responseIP),
        flush: false,
        data: responseData(),
      });
    }

    // for use with test_dns_by_type_resolve.js
    if (packet.questions[0].type == "TXT") {
      answers.push({
        name: packet.questions[0].name,
        type: packet.questions[0].type,
        ttl: 55,
        class: "IN",
        flush: false,
        data: Buffer.from(
          "62586B67646D39705932556761584D6762586B676347467A63336476636D513D",
          "hex"
        ),
      });
    }

    if (u.query.cnameloop) {
      answers.push({
        name: "cname.example.com",
        type: "CNAME",
        ttl: 55,
        class: "IN",
        flush: false,
        data: "pointing-elsewhere.example.com",
      });
    }

    if (req.headers["accept-language"] || req.headers["user-agent"]) {
      // If we get this header, don't send back any response. This should
      // cause the tests to fail. This is easier then actually sending back
      // the header value into test_trr.js
      answers = [];
    }

    let buf = dnsPacket.encode({
      type: "response",
      id: packet.id,
      flags: dnsPacket.RECURSION_DESIRED,
      questions: packet.questions,
      answers,
    });

    return buf;
  }

  function getDelayFromPacket(packet, type) {
    let delay = 0;
    if (packet.questions[0].type == "A") {
      delay = parseInt(u.query.delayIPv4);
    } else if (packet.questions[0].type == "AAAA") {
      delay = parseInt(u.query.delayIPv6);
    }

    if (u.query.slowConfirm && type == "NS") {
      delay += 1000;
    }

    return delay;
  }

  function writeDNSResponse(response, buf, delay, contentType) {
    function writeResponse(resp, buffer) {
      resp.setHeader("Set-Cookie", "trackyou=yes; path=/; max-age=100000;");
      resp.setHeader("Content-Type", contentType);
      if (req.headers["accept-encoding"].includes("gzip")) {
        zlib.gzip(buffer, function (err, result) {
          resp.setHeader("Content-Encoding", "gzip");
          resp.setHeader("Content-Length", result.length);
          try {
            resp.writeHead(200);
            resp.end(result);
          } catch (e) {
            // connection was closed by the time we started writing.
          }
        });
      } else {
        const output = Buffer.from(buffer, "utf-8");
        resp.setHeader("Content-Length", output.length);
        try {
          resp.writeHead(200);
          resp.write(output);
          resp.end("");
        } catch (e) {
          // connection was closed by the time we started writing.
        }
      }
    }

    if (delay) {
      setTimeout(
        arg => {
          writeResponse(arg[0], arg[1]);
        },
        delay,
        [response, buf]
      );
      return;
    }

    writeResponse(response, buf);
  }

  if (req.httpVersionMajor === 2) {
    res.setHeader("X-Connection-Http2", "yes");
    res.setHeader("X-Http2-StreamId", "" + req.stream.id);
  } else {
    res.setHeader("X-Connection-Http2", "no");
  }

  if (u.pathname === "/exit") {
    res.setHeader("Content-Type", "text/plain");
    res.setHeader("Connection", "close");
    res.writeHead(200);
    res.end("ok");
    process.exit();
  }

  if (req.method == "CONNECT") {
    if (req.headers.host == "illegalhpacksoft.example.com:80") {
      illegalheader_conn = req.stream.connection;
      res.setHeader("Content-Type", "text/html");
      res.setHeader("x-softillegalhpack", "true");
      res.writeHead(200);
      res.end(content);
      return;
    } else if (req.headers.host == "illegalhpackhard.example.com:80") {
      res.setHeader("Content-Type", "text/html");
      res.setHeader("x-hardillegalhpack", "true");
      res.writeHead(200);
      res.end(content);
      return;
    } else if (req.headers.host == "750.example.com:80") {
      // This response will mock a response through a proxy to a HTTP server.
      // After 750ms , a 200 response for the proxy will be sent then
      // after additional 50ms a 200 response for the HTTP GET request.
      let rl = new runConnectLater();
      rl.req = req;
      rl.resp = res;
      setTimeout(executeRunLaterCatchError, 750, rl);
      return;
    } else if (req.headers.host == "h11required.com:80") {
      if (req.httpVersionMajor === 2) {
        res.stream.reset("HTTP_1_1_REQUIRED");
      }
      return;
    }
  } else if (u.pathname === "/750ms") {
    let rl = new runlater();
    rl.req = req;
    rl.resp = res;
    setTimeout(executeRunLater, 750, rl);
    return;
  } else if (u.pathname === "/750msNoData") {
    let rl = new runlater();
    rl.req = req;
    rl.resp = res;
    rl.fin = false;
    setTimeout(executeRunLater, 750, rl);
    return;
  } else if (u.pathname === "/multiplex1" && req.httpVersionMajor === 2) {
    res.setHeader("Content-Type", "text/plain");
    res.writeHead(200);
    m.mp1res = res;
    m.checkReady();
    return;
  } else if (u.pathname === "/multiplex2" && req.httpVersionMajor === 2) {
    res.setHeader("Content-Type", "text/plain");
    res.writeHead(200);
    m.mp2res = res;
    m.checkReady();
    return;
  } else if (u.pathname === "/header") {
    var val = req.headers["x-test-header"];
    if (val) {
      res.setHeader("X-Received-Test-Header", val);
    }
  } else if (u.pathname === "/doubleheader") {
    res.setHeader("Content-Type", "text/html");
    res.writeHead(200);
    res.write(content);
    res.writeHead(200);
    res.end();
    return;
  } else if (u.pathname === "/cookie_crumbling") {
    res.setHeader("X-Received-Header-Pairs", JSON.stringify(decompressedPairs));
  } else if (u.pathname === "/push") {
    push = res.push("/push.js");
    push.writeHead(200, {
      "content-type": "application/javascript",
      pushed: "yes",
      "content-length": 11,
      "X-Connection-Http2": "yes",
    });
    push.end("// comments");
    content = '<head> <script src="push.js"/></head>body text';
  } else if (u.pathname === "/push.js") {
    content = "// comments";
    res.setHeader("pushed", "no");
  } else if (u.pathname === "/push2") {
    push = res.push("/push2.js");
    push.writeHead(200, {
      "content-type": "application/javascript",
      pushed: "yes",
      // no content-length
      "X-Connection-Http2": "yes",
    });
    push.end("// comments");
    content = '<head> <script src="push2.js"/></head>body text';
  } else if (u.pathname === "/push5") {
    push = res.push("/push5.js");
    push.writeHead(200, {
      "content-type": "application/javascript",
      pushed: "yes",
      // no content-length
      "X-Connection-Http2": "yes",
    });
    content = generateContent(1024 * 150);
    push.write(content);
    push.end();
    content = '<head> <script src="push5.js"/></head>body text';
  } else if (u.pathname === "/pushapi1") {
    push1 = res.push({
      hostname: "localhost:" + serverPort,
      port: serverPort,
      path: "/pushapi1/1",
      method: "GET",
      headers: { "x-pushed-request": "true", "x-foo": "bar" },
    });
    push1.writeHead(200, {
      pushed: "yes",
      "content-length": 1,
      subresource: "1",
      "X-Connection-Http2": "yes",
    });
    push1.end("1");

    push1a = res.push({
      hostname: "localhost:" + serverPort,
      port: serverPort,
      path: "/pushapi1/1",
      method: "GET",
      headers: { "x-foo": "bar", "x-pushed-request": "true" },
    });
    push1a.writeHead(200, {
      pushed: "yes",
      "content-length": 1,
      subresource: "1a",
      "X-Connection-Http2": "yes",
    });
    push1a.end("1");

    push2 = res.push({
      hostname: "localhost:" + serverPort,
      port: serverPort,
      path: "/pushapi1/2",
      method: "GET",
      headers: { "x-pushed-request": "true" },
    });
    push2.writeHead(200, {
      pushed: "yes",
      subresource: "2",
      "content-length": 1,
      "X-Connection-Http2": "yes",
    });
    push2.end("2");

    push3 = res.push({
      hostname: "localhost:" + serverPort,
      port: serverPort,
      path: "/pushapi1/3",
      method: "GET",
      headers: { "x-pushed-request": "true", "Accept-Encoding": "br" },
    });
    push3.writeHead(200, {
      pushed: "yes",
      "content-length": 6,
      subresource: "3",
      "content-encoding": "br",
      "X-Connection-Http2": "yes",
    });
    push3.end(Buffer.from([0x8b, 0x00, 0x80, 0x33, 0x0a, 0x03])); // '3\n'

    content = "0";
  } else if (u.pathname === "/big") {
    content = generateContent(128 * 1024);
    var hash = crypto.createHash("md5");
    hash.update(content);
    let md5 = hash.digest("hex");
    res.setHeader("X-Expected-MD5", md5);
  } else if (u.pathname === "/huge") {
    content = generateContent(1024);
    res.setHeader("Content-Type", "text/plain");
    res.writeHead(200);
    // 1mb of data
    for (let i = 0; i < 1024 * 1; i++) {
      res.write(content); // 1kb chunk
    }
    res.end();
    return;
  } else if (u.pathname === "/post" || u.pathname === "/patch") {
    if (req.method != "POST" && req.method != "PATCH") {
      res.writeHead(405);
      res.end("Unexpected method: " + req.method);
      return;
    }

    var post_hash = crypto.createHash("md5");
    var received_data = false;
    req.on("data", function receivePostData(chunk) {
      received_data = true;
      post_hash.update(chunk.toString());
    });
    req.on("end", function finishPost() {
      let md5 = received_data ? post_hash.digest("hex") : "0";
      res.setHeader("X-Calculated-MD5", md5);
      res.writeHead(200);
      res.end(content);
    });

    return;
  } else if (u.pathname === "/750msPost") {
    if (req.method != "POST") {
      res.writeHead(405);
      res.end("Unexpected method: " + req.method);
      return;
    }

    var accum = 0;
    req.on("data", function receivePostData(chunk) {
      accum += chunk.length;
    });
    req.on("end", function finishPost() {
      res.setHeader("X-Recvd", accum);
      let rl = new runlater();
      rl.req = req;
      rl.resp = res;
      setTimeout(executeRunLater, 750, rl);
    });

    return;
  } else if (u.pathname === "/h11required_stream") {
    if (req.httpVersionMajor === 2) {
      h11required_conn = req.stream.connection;
      res.stream.reset("HTTP_1_1_REQUIRED");
      return;
    }
  } else if (u.pathname === "/bigdownload") {
    res.setHeader("Content-Type", "text/html");
    res.writeHead(200);

    let rl = new moreData();
    rl.req = req;
    rl.resp = res;
    setTimeout(executeRunLater, 1, rl);
    return;
  } else if (u.pathname === "/h11required_session") {
    if (req.httpVersionMajor === 2) {
      if (h11required_conn !== req.stream.connection) {
        h11required_header = "no";
      }
      res.stream.connection.close("HTTP_1_1_REQUIRED", res.stream.id - 2);
      return;
    }
    res.setHeader("X-H11Required-Stream-Ok", h11required_header);
  } else if (u.pathname === "/rstonce") {
    if (!didRst && req.httpVersionMajor === 2) {
      didRst = true;
      rstConnection = req.stream.connection;
      req.stream.reset("REFUSED_STREAM");
      return;
    }

    if (rstConnection === null || rstConnection !== req.stream.connection) {
      if (req.httpVersionMajor != 2) {
        res.setHeader("Connection", "close");
      }
      res.writeHead(400);
      res.end("WRONG CONNECTION, HOMIE!");
      return;
    }

    // Clear these variables so we can run the test again with --verify
    didRst = false;
    rstConnection = null;

    if (req.httpVersionMajor != 2) {
      res.setHeader("Connection", "close");
    }
    res.writeHead(200);
    res.end("It's all good.");
    return;
  } else if (u.pathname === "/continuedheaders") {
    var pushRequestHeaders = { "x-pushed-request": "true" };
    var pushResponseHeaders = {
      "content-type": "text/plain",
      "content-length": "2",
      "X-Connection-Http2": "yes",
    };
    var pushHdrTxt =
      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    var pullHdrTxt = pushHdrTxt.split("").reverse().join("");
    for (let i = 0; i < 265; i++) {
      pushRequestHeaders["X-Push-Test-Header-" + i] = pushHdrTxt;
      res.setHeader("X-Pull-Test-Header-" + i, pullHdrTxt);
    }
    push = res.push({
      hostname: "localhost:" + serverPort,
      port: serverPort,
      path: "/continuedheaders/push",
      method: "GET",
      headers: pushRequestHeaders,
    });
    push.writeHead(200, pushResponseHeaders);
    push.end("ok");
  } else if (u.pathname === "/hugecontinuedheaders") {
    for (let i = 0; i < u.query.size; i++) {
      res.setHeader(
        "X-Test-Header-" + i,
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".repeat(1024)
      );
    }
    res.writeHead(200);
    res.end(content);
    return;
  } else if (u.pathname === "/altsvc1") {
    if (
      req.httpVersionMajor != 2 ||
      req.scheme != "http" ||
      req.headers["alt-used"] != "foo.example.com:" + serverPort
    ) {
      res.writeHead(400);
      res.end("WHAT?");
      return;
    }
    // test the alt svc frame for use with altsvc2
    res.altsvc(
      "foo.example.com",
      serverPort,
      "h2",
      3600,
      req.headers["x-redirect-origin"]
    );
  } else if (u.pathname === "/altsvc2") {
    if (
      req.httpVersionMajor != 2 ||
      req.scheme != "http" ||
      req.headers["alt-used"] != "foo.example.com:" + serverPort
    ) {
      res.writeHead(400);
      res.end("WHAT?");
      return;
    }
  }

  // for use with test_altsvc.js
  else if (u.pathname === "/altsvc-test") {
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Alt-Svc", "h2=" + req.headers["x-altsvc"]);
  }
  // for use with test_http3.js
  else if (u.pathname === "/http3-test") {
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Alt-Svc", "h3-29=" + req.headers["x-altsvc"]);
  }
  // for use with test_http3.js
  else if (u.pathname === "/http3-test2") {
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader(
      "Alt-Svc",
      "h2=foo2.example.com:8000,h3-29=" + req.headers["x-altsvc"]
    );
  } else if (u.pathname === "/http3-test3") {
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader(
      "Alt-Svc",
      "h3-29=" + req.headers["x-altsvc"] + ",h3=" + req.headers["x-altsvc"]
    );
  }
  // for use with test_trr.js
  else if (u.pathname === "/dns-cname") {
    // asking for cname.example.com

    function emitResponse(response, payload) {
      let pcontent = createCNameContent(payload);
      response.setHeader("Content-Type", "application/dns-message");
      response.setHeader("Content-Length", pcontent.length);
      response.writeHead(200);
      response.write(pcontent);
      response.end("");
    }

    let payload = Buffer.from("");
    req.on("data", function receiveData(chunk) {
      payload = Buffer.concat([payload, chunk]);
    });
    req.on("end", function finishedData() {
      emitResponse(res, payload);
    });
    return;
  } else if (u.pathname == "/get-doh-req-port-log") {
    let rContent = JSON.stringify(gDoHPortsLog);
    res.setHeader("Content-Type", "text/plain");
    res.setHeader("Content-Length", rContent.length);
    res.writeHead(400);
    res.end(rContent);
    return;
  } else if (u.pathname == "/reset-doh-request-count") {
    gDoHRequestCount = 0;
    res.setHeader("Content-Type", "text/plain");
    res.setHeader("Content-Length", "ok".length);
    res.writeHead(200);
    res.write("ok");
    res.end("");
    return;
  } else if (u.pathname == "/doh") {
    let responseIP = u.query.responseIP;
    if (!responseIP) {
      responseIP = "5.5.5.5";
    }

    let redirect = u.query.redirect;
    if (redirect) {
      responseIP = redirect;
      if (u.query.dns) {
        res.setHeader(
          "Location",
          "https://localhost:" +
            serverPort +
            "/doh?responseIP=" +
            responseIP +
            "&dns=" +
            u.query.dns
        );
      } else {
        res.setHeader(
          "Location",
          "https://localhost:" + serverPort + "/doh?responseIP=" + responseIP
        );
      }
      res.writeHead(307);
      res.end("");
      return;
    }

    if (u.query.auth) {
      if (!handleAuth()) {
        return;
      }
    }

    if (u.query.noResponse) {
      return;
    }

    if (u.query.push) {
      // push.example.org has AAAA entry 2018::2018
      let pcontent = dnsPacket.encode({
        id: 0,
        type: "response",
        flags: dnsPacket.RECURSION_DESIRED,
        questions: [{ name: "push.example.org", type: "AAAA", class: "IN" }],
        answers: [
          {
            name: "push.example.org",
            type: "AAAA",
            ttl: 55,
            class: "IN",
            flush: false,
            data: "2018::2018",
          },
        ],
      });
      push = res.push({
        hostname: "foo.example.com:" + serverPort,
        port: serverPort,
        path: "/dns-pushed-response?dns=AAAAAAABAAAAAAAABHB1c2gHZXhhbXBsZQNvcmcAABwAAQ",
        method: "GET",
        headers: {
          accept: "application/dns-message",
        },
      });
      push.writeHead(200, {
        "content-type": "application/dns-message",
        pushed: "yes",
        "content-length": pcontent.length,
        "X-Connection-Http2": "yes",
      });
      push.end(pcontent);
    }

    let payload = Buffer.from("");

    function emitResponse(response, requestPayload, decodedPacket, delay) {
      let packet = decodedPacket || dnsPacket.decode(requestPayload);
      let answer = createDNSAnswer(
        response,
        packet,
        responseIP,
        requestPayload
      );
      if (!answer) {
        return;
      }
      writeDNSResponse(
        response,
        answer,
        delay || getDelayFromPacket(packet, responseType(packet, responseIP)),
        "application/dns-message"
      );
    }

    if (u.query.dns) {
      payload = Buffer.from(u.query.dns, "base64");
      emitResponse(res, payload);
      return;
    }

    req.on("data", function receiveData(chunk) {
      payload = Buffer.concat([payload, chunk]);
    });
    req.on("end", function finishedData() {
      // parload is empty when we send redirect response.
      if (payload.length) {
        let packet = dnsPacket.decode(payload);
        let delay;
        if (u.query.conncycle) {
          let name = packet.questions[0].name;
          if (name.startsWith("newconn")) {
            // If we haven't seen a req for this newconn name before,
            // or if we've seen one for the same name on the same port,
            // synthesize a timeout.
            if (
              !gDoHNewConnLog[name] ||
              gDoHNewConnLog[name] == req.remotePort
            ) {
              delay = 1000;
            }
            if (!gDoHNewConnLog[name]) {
              gDoHNewConnLog[name] = req.remotePort;
            }
          }
          gDoHPortsLog.push([packet.questions[0].name, req.remotePort]);
        } else {
          gDoHPortsLog = [];
          gDoHNewConnLog = {};
        }
        emitResponse(res, payload, packet, delay);
      }
    });
    return;
  } else if (u.pathname === "/httpssvc") {
    let payload = Buffer.from("");
    req.on("data", function receiveData(chunk) {
      payload = Buffer.concat([payload, chunk]);
    });
    req.on("end", function finishedData() {
      let packet = dnsPacket.decode(payload);
      let answers = [];
      answers.push({
        name: packet.questions[0].name,
        type: packet.questions[0].type,
        ttl: 55,
        class: "IN",
        flush: false,
        data: {
          priority: 1,
          name: "h3pool",
          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: 30, value: "somelargestring" },
            { key: "odoh", value: "456..." },
          ],
        },
      });
      answers.push({
        name: packet.questions[0].name,
        type: packet.questions[0].type,
        ttl: 55,
        class: "IN",
        flush: false,
        data: {
          priority: 2,
          name: ".",
          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..." },
          ],
        },
      });
      answers.push({
        name: packet.questions[0].name,
        type: packet.questions[0].type,
        ttl: 55,
        class: "IN",
        flush: false,
        data: {
          priority: 3,
          name: "hello",
          values: [],
        },
      });
      let buf = dnsPacket.encode({
        type: "response",
        id: packet.id,
        flags: dnsPacket.RECURSION_DESIRED,
        questions: packet.questions,
        answers,
      });

      res.setHeader("Content-Type", "application/dns-message");
      res.setHeader("Content-Length", buf.length);
      res.writeHead(200);
      res.write(buf);
      res.end("");
    });
    return;
  } else if (u.pathname === "/httpssvc_as_altsvc") {
    let payload = Buffer.from("");
    req.on("data", function receiveData(chunk) {
      payload = Buffer.concat([payload, chunk]);
    });
    req.on("end", function finishedData() {
      let packet = dnsPacket.decode(payload);
      let answers = [];
      if (packet.questions[0].type == "HTTPS") {
        let priority = 1;
        // Set an invalid priority to test the case when receiving a corrupted
        // response.
        if (packet.questions[0].name === "foo.notexisted.com") {
          priority = 0;
        }
        answers.push({
          name: packet.questions[0].name,
          type: packet.questions[0].type,
          ttl: 55,
          class: "IN",
          flush: false,
          data: {
            priority,
            name: "foo.example.com",
            values: [
              { key: "alpn", value: "h2" },
              { key: "port", value: serverPort },
              { key: 30, value: "somelargestring" },
            ],
          },
        });
      } else {
        answers.push({
          name: packet.questions[0].name,
          type: "A",
          ttl: 55,
          flush: false,
          data: "127.0.0.1",
        });
      }

      let buf = dnsPacket.encode({
        type: "response",
        id: packet.id,
        flags: dnsPacket.RECURSION_DESIRED,
        questions: packet.questions,
        answers,
      });

      res.setHeader("Content-Type", "application/dns-message");
      res.setHeader("Content-Length", buf.length);
      res.writeHead(200);
      res.write(buf);
      res.end("");
    });
    return;
  } else if (u.pathname === "/httpssvc_use_iphint") {
    let payload = Buffer.from("");
    req.on("data", function receiveData(chunk) {
      payload = Buffer.concat([payload, chunk]);
    });
    req.on("end", function finishedData() {
      let packet = dnsPacket.decode(payload);
      let answers = [];
      answers.push({
        name: packet.questions[0].name,
        type: "HTTPS",
        ttl: 55,
        class: "IN",
        flush: false,
        data: {
          priority: 1,
          name: ".",
          values: [
            { key: "alpn", value: "h2" },
            { key: "port", value: serverPort },
            { key: "ipv4hint", value: "127.0.0.1" },
          ],
        },
      });

      let buf = dnsPacket.encode({
        type: "response",
        id: packet.id,
        flags: dnsPacket.RECURSION_DESIRED,
        questions: packet.questions,
        answers,
      });

      res.setHeader("Content-Type", "application/dns-message");
      res.setHeader("Content-Length", buf.length);
      res.writeHead(200);
      res.write(buf);
      res.end("");
    });
    return;
  } else if (u.pathname === "/dns-cname-a") {
    let rContent = createCNameARecord();
    res.setHeader("Content-Type", "application/dns-message");
    res.setHeader("Content-Length", rContent.length);
    res.writeHead(200);
    res.write(rContent);
    res.end("");
    return;
  } else if (u.pathname === "/websocket") {
    res.setHeader("Upgrade", "websocket");
    res.setHeader("Connection", "Upgrade");
    var wshash = crypto.createHash("sha1");
    wshash.update(req.headers["sec-websocket-key"]);
    wshash.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
    let key = wshash.digest("base64");
    res.setHeader("Sec-WebSocket-Accept", key);
    res.writeHead(101);
    res.end("something....");
    return;
  }
  // for use with test_dns_by_type_resolve.js
  else if (u.pathname === "/txt-dns-push") {
    // _esni_push.example.com has A entry 127.0.0.1
    let rContent = Buffer.from(
      "0000010000010001000000000A5F65736E695F70757368076578616D706C6503636F6D0000010001C00C000100010000003700047F000001",
      "hex"
    );

    // _esni_push.example.com has TXT entry 2062586B67646D39705932556761584D6762586B676347467A63336476636D513D
    var pcontent = Buffer.from(
      "0000818000010001000000000A5F65736E695F70757368076578616D706C6503636F6D0000100001C00C001000010000003700212062586B67646D39705932556761584D6762586B676347467A63336476636D513D",
      "hex"
    );

    push = res.push({
      hostname: "foo.example.com:" + serverPort,
      port: serverPort,
      path: "/dns-pushed-response?dns=AAABAAABAAAAAAABCl9lc25pX3B1c2gHZXhhbXBsZQNjb20AABAAAQAAKRAAAAAAAAAIAAgABAABAAA",
      method: "GET",
      headers: {
        accept: "application/dns-message",
      },
    });
    push.writeHead(200, {
      "content-type": "application/dns-message",
      pushed: "yes",
      "content-length": pcontent.length,
      "X-Connection-Http2": "yes",
    });
    push.end(pcontent);
    res.setHeader("Content-Type", "application/dns-message");
    res.setHeader("Content-Length", rContent.length);
    res.writeHead(200);
    res.write(rContent);
    res.end("");
    return;
  } else if (u.pathname === "/.well-known/http-opportunistic") {
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Content-Type", "application/json");
    res.writeHead(200, "OK");
    res.end('["http://' + req.headers.host + '"]');
    return;
  } else if (u.pathname === "/stale-while-revalidate-loop-test") {
    res.setHeader(
      "Cache-Control",
      "s-maxage=86400, stale-while-revalidate=86400, immutable"
    );
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.setHeader("X-Content-Type-Options", "nosniff");
    res.setHeader("Content-Length", "1");
    res.writeHead(200, "OK");
    res.end("1");
    return;
  }

  // for PushService tests.
  else if (u.pathname === "/pushSubscriptionSuccess/subscribe") {
    res.setHeader(
      "Location",
      "https://localhost:" + serverPort + "/pushSubscriptionSuccesss"
    );
    res.setHeader(
      "Link",
      '</pushEndpointSuccess>; rel="urn:ietf:params:push", ' +
        '</receiptPushEndpointSuccess>; rel="urn:ietf:params:push:receipt"'
    );
    res.writeHead(201, "OK");
    res.end("");
    return;
  } else if (u.pathname === "/pushSubscriptionSuccesss") {
    // do nothing.
    return;
  } else if (u.pathname === "/pushSubscriptionMissingLocation/subscribe") {
    res.setHeader(
      "Link",
      '</pushEndpointMissingLocation>; rel="urn:ietf:params:push", ' +
        '</receiptPushEndpointMissingLocation>; rel="urn:ietf:params:push:receipt"'
    );
    res.writeHead(201, "OK");
    res.end("");
    return;
  } else if (u.pathname === "/pushSubscriptionMissingLink/subscribe") {
    res.setHeader(
      "Location",
      "https://localhost:" + serverPort + "/subscriptionMissingLink"
    );
    res.writeHead(201, "OK");
    res.end("");
    return;
  } else if (u.pathname === "/pushSubscriptionLocationBogus/subscribe") {
    res.setHeader("Location", "1234");
    res.setHeader(
      "Link",
      '</pushEndpointLocationBogus; rel="urn:ietf:params:push", ' +
        '</receiptPushEndpointLocationBogus>; rel="urn:ietf:params:push:receipt"'
    );
    res.writeHead(201, "OK");
    res.end("");
    return;
  } else if (u.pathname === "/pushSubscriptionMissingLink1/subscribe") {
    res.setHeader(
      "Location",
      "https://localhost:" + serverPort + "/subscriptionMissingLink1"
    );
    res.setHeader(
      "Link",
      '</receiptPushEndpointMissingLink1>; rel="urn:ietf:params:push:receipt"'
    );
    res.writeHead(201, "OK");
    res.end("");
    return;
  } else if (u.pathname === "/pushSubscriptionMissingLink2/subscribe") {
    res.setHeader(
      "Location",
      "https://localhost:" + serverPort + "/subscriptionMissingLink2"
    );
    res.setHeader(
      "Link",
      '</pushEndpointMissingLink2>; rel="urn:ietf:params:push"'
    );
    res.writeHead(201, "OK");
    res.end("");
    return;
  } else if (u.pathname === "/subscriptionMissingLink2") {
    // do nothing.
    return;
  } else if (u.pathname === "/pushSubscriptionNot201Code/subscribe") {
    res.setHeader(
      "Location",
      "https://localhost:" + serverPort + "/subscriptionNot2xxCode"
    );
    res.setHeader(
      "Link",
      '</pushEndpointNot201Code>; rel="urn:ietf:params:push", ' +
        '</receiptPushEndpointNot201Code>; rel="urn:ietf:params:push:receipt"'
    );
    res.writeHead(200, "OK");
    res.end("");
    return;
  } else if (u.pathname === "/pushNotifications/subscription1") {
    pushPushServer1 = res.push({
      hostname: "localhost:" + serverPort,
      port: serverPort,
      path: "/pushNotificationsDeliver1",
      method: "GET",
      headers: {
        "Encryption-Key":
          'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"',
        Encryption: 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"',
        "Content-Encoding": "aesgcm128",
      },
    });
    pushPushServer1.writeHead(200, {
      subresource: "1",
    });

    pushPushServer1.end(
      "370aeb3963f12c4f12bf946bd0a7a9ee7d3eaff8f7aec62b530fc25cfa",
      "hex"
    );
    return;
  } else if (u.pathname === "/pushNotifications/subscription2") {
    pushPushServer2 = res.push({
      hostname: "localhost:" + serverPort,
      port: serverPort,
      path: "/pushNotificationsDeliver3",
      method: "GET",
      headers: {
        "Encryption-Key":
          'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"',
        Encryption: 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"',
        "Content-Encoding": "aesgcm128",
      },
    });
    pushPushServer2.writeHead(200, {
      subresource: "1",
    });

    pushPushServer2.end(
      "66df5d11daa01e5c802ff97cdf7f39684b5bf7c6418a5cf9b609c6826c04b25e403823607ac514278a7da945",
      "hex"
    );
    return;
  } else if (u.pathname === "/pushNotifications/subscription3") {
    pushPushServer3 = res.push({
      hostname: "localhost:" + serverPort,
      port: serverPort,
      path: "/pushNotificationsDeliver3",
      method: "GET",
      headers: {
        "Encryption-Key":
          'keyid="notification3";dh="BD3xV_ACT8r6hdIYES3BJj1qhz9wyv7MBrG9vM2UCnjPzwE_YFVpkD-SGqE-BR2--0M-Yf31wctwNsO1qjBUeMg"',
        Encryption:
          'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24',
        "Content-Encoding": "aesgcm128",
      },
    });
    pushPushServer3.writeHead(200, {
      subresource: "1",
    });

    pushPushServer3.end(
      "2caaeedd9cf1059b80c58b6c6827da8ff7de864ac8bea6d5775892c27c005209cbf9c4de0c3fbcddb9711d74eaeebd33f7275374cb42dd48c07168bc2cc9df63e045ce2d2a2408c66088a40c",
      "hex"
    );
    return;
  } else if (u.pathname == "/pushNotifications/subscription4") {
    pushPushServer4 = res.push({
      hostname: "localhost:" + serverPort,
      port: serverPort,
      path: "/pushNotificationsDeliver4",
      method: "GET",
      headers: {
        "Crypto-Key":
          'keyid="notification4";dh="BJScXUUTcs7D8jJWI1AOxSgAKkF7e56ay4Lek52TqDlWo1yGd5czaxFWfsuP4j7XNWgGYm60-LKpSUMlptxPFVQ"',
        Encryption: 'keyid="notification4"; salt="sn9p2QqF3V6KBclda8vx7w"',
        "Content-Encoding": "aesgcm",
      },
    });
    pushPushServer4.writeHead(200, {
      subresource: "1",
    });

    pushPushServer4.end(
      "9eba7ba6192544a39bd9e9b58e702d0748f1776b27f6616cdc55d29ed5a015a6db8f2dd82cd5751a14315546194ff1c18458ab91eb36c9760ccb042670001fd9964557a079553c3591ee131ceb259389cfffab3ab873f873caa6a72e87d262b8684c3260e5940b992234deebf57a9ff3a8775742f3cbcb152d249725a28326717e19cce8506813a155eff5df9bdba9e3ae8801d3cc2b7e7f2f1b6896e63d1fdda6f85df704b1a34db7b2dd63eba11ede154300a318c6f83c41a3d32356a196e36bc905b99195fd91ae4ff3f545c42d17f1fdc1d5bd2bf7516d0765e3a859fffac84f46160b79cedda589f74c25357cf6988cd8ba83867ebd86e4579c9d3b00a712c77fcea3b663007076e21f9819423faa830c2176ff1001c1690f34be26229a191a938517",
      "hex"
    );
    return;
  } else if (
    u.pathname === "/pushNotificationsDeliver1" ||
    u.pathname === "/pushNotificationsDeliver2" ||
    u.pathname === "/pushNotificationsDeliver3"
  ) {
    res.writeHead(410, "GONE");
    res.end("");
    return;
  } else if (u.pathname === "/illegalhpacksoft") {
    // This will cause the compressor to compress a header that is not legal,
    // but only affects the stream, not the session.
    illegalheader_conn = req.stream.connection;
    res.setHeader("Content-Type", "text/html");
    res.setHeader("x-softillegalhpack", "true");
    res.writeHead(200);
    res.end(content);
    return;
  } else if (u.pathname === "/illegalhpackhard") {
    // This will cause the compressor to insert an HPACK instruction that will
    // cause a session failure.
    res.setHeader("Content-Type", "text/html");
    res.setHeader("x-hardillegalhpack", "true");
    res.writeHead(200);
    res.end(content);
    return;
  } else if (u.pathname === "/illegalhpack_validate") {
    if (req.stream.connection === illegalheader_conn) {
      res.setHeader("X-Did-Goaway", "no");
    } else {
      res.setHeader("X-Did-Goaway", "yes");
    }
    // Fall through to the default response behavior
  } else if (u.pathname === "/foldedheader") {
    res.setHeader("X-Folded-Header", "this is\n folded");
    // Fall through to the default response behavior
  } else if (u.pathname === "/emptydata") {
    // Overwrite the original transform with our version that will insert an
    // empty DATA frame at the beginning of the stream response, then fall
    // through to the default response behavior.
    Serializer.prototype._transform = newTransform;
  }

  // for use with test_immutable.js
  else if (u.pathname === "/immutable-test-without-attribute") {
    res.setHeader("Cache-Control", "max-age=100000");
    res.setHeader("Etag", "1");
    if (req.headers["if-none-match"]) {
      res.setHeader("x-conditional", "true");
    }
    // default response from here
  } else if (u.pathname === "/immutable-test-with-attribute") {
    res.setHeader("Cache-Control", "max-age=100000, immutable");
    res.setHeader("Etag", "2");
    if (req.headers["if-none-match"]) {
      res.setHeader("x-conditional", "true");
    }
    // default response from here
  } else if (u.pathname === "/immutable-test-expired-with-Expires-header") {
    res.setHeader("Cache-Control", "immutable");
    res.setHeader("Expires", "Mon, 01 Jan 1990 00:00:00 GMT");
    res.setHeader("Etag", "3");

    if (req.headers["if-none-match"]) {
      res.setHeader("x-conditional", "true");
    }
  } else if (
    u.pathname === "/immutable-test-expired-with-last-modified-header"
  ) {
    res.setHeader("Cache-Control", "public, max-age=3600, immutable");
    res.setHeader("Date", "Mon, 01 Jan 1990 00:00:00 GMT");
    res.setHeader("Last-modified", "Mon, 01 Jan 1990 00:00:00 GMT");
    res.setHeader("Etag", "4");

    if (req.headers["if-none-match"]) {
      res.setHeader("x-conditional", "true");
    }
  } else if (u.pathname === "/origin-4") {
    let originList = [];
    req.stream.connection.originFrame(originList);
    res.setHeader("x-client-port", req.remotePort);
  } else if (u.pathname === "/origin-6") {
    let originList = [
      "https://alt1.example.com:" + serverPort,
      "https://alt2.example.com:" + serverPort,
      "https://bar.example.com:" + serverPort,
    ];
    req.stream.connection.originFrame(originList);
    res.setHeader("x-client-port", req.remotePort);
  } else if (u.pathname === "/origin-11-a") {
    res.setHeader("x-client-port", req.remotePort);

    const pushb = res.push({
      hostname: "foo.example.com:" + serverPort,
      port: serverPort,
      path: "/origin-11-b",
      method: "GET",
      headers: { "x-pushed-request": "true", "x-foo": "bar" },
    });
    pushb.writeHead(200, {
      pushed: "yes",
      "content-length": 1,
    });
    pushb.end("1");

    const pushc = res.push({
      hostname: "bar.example.com:" + serverPort,
      port: serverPort,
      path: "/origin-11-c",
      method: "GET",
      headers: { "x-pushed-request": "true", "x-foo": "bar" },
    });
    pushc.writeHead(200, {
      pushed: "yes",
      "content-length": 1,
    });
    pushc.end("1");

    const pushd = res.push({
      hostname: "madeup.example.com:" + serverPort,
      port: serverPort,
      path: "/origin-11-d",
      method: "GET",
      headers: { "x-pushed-request": "true", "x-foo": "bar" },
    });
    pushd.writeHead(200, {
      pushed: "yes",
      "content-length": 1,
    });
    pushd.end("1");

    const pushe = res.push({
      hostname: "alt1.example.com:" + serverPort,
      port: serverPort,
      path: "/origin-11-e",
      method: "GET",
      headers: { "x-pushed-request": "true", "x-foo": "bar" },
    });
    pushe.writeHead(200, {
      pushed: "yes",
      "content-length": 1,
    });
    pushe.end("1");
  } else if (u.pathname.substring(0, 8) === "/origin-") {
    // test_origin.js coalescing
    res.setHeader("x-client-port", req.remotePort);
  } else if (u.pathname === "/statusphrase") {
    // Fortunately, the node-http2 API is dumb enough to allow this right on
    // through, so we can easily test rejecting this on gecko's end.
    res.writeHead("200 OK");
    res.end(content);
    return;
  } else if (u.pathname === "/doublepush") {
    push1 = res.push("/doublypushed");
    push1.writeHead(200, {
      "content-type": "text/plain",
      pushed: "yes",
      "content-length": 6,
      "X-Connection-Http2": "yes",
    });
    push1.end("pushed");

    push2 = res.push("/doublypushed");
    push2.writeHead(200, {
      "content-type": "text/plain",
      pushed: "yes",
      "content-length": 6,
      "X-Connection-Http2": "yes",
    });
    push2.end("pushed");
  } else if (u.pathname === "/doublypushed") {
    content = "not pushed";
  } else if (u.pathname === "/diskcache") {
    content = "this was pulled via h2";
  } else if (u.pathname === "/pushindisk") {
    var pushedContent = "this was pushed via h2";
    push = res.push("/diskcache");
    push.writeHead(200, {
      "content-type": "text/html",
      pushed: "yes",
      "content-length": pushedContent.length,
      "X-Connection-Http2": "yes",
    });
    push.end(pushedContent);
  }

  // For test_header_Server_Timing.js
  else if (u.pathname === "/server-timing") {
    res.setHeader("Content-Type", "text/plain");
    res.setHeader("Content-Length", "12");
    res.setHeader("Trailer", "Server-Timing");
    res.setHeader(
      "Server-Timing",
      "metric; dur=123.4; desc=description, metric2; dur=456.78; desc=description1"
    );
    res.write("data reached");
    res.addTrailers({
      "Server-Timing":
        "metric3; dur=789.11; desc=description2, metric4; dur=1112.13; desc=description3",
    });
    res.end();
    return;
  } else if (u.pathname === "/redirect_to_http") {
    res.setHeader(
      "Location",
      `http://test.httpsrr.redirect.com:${u.query.port}/redirect_to_http`
    );
    res.writeHead(307);
    res.end("");
    return;
  } else if (u.pathname === "/103_response") {
    let link_val = req.headers["link-to-set"];
    if (link_val) {
      res.setHeader("link", link_val);
    }
    res.setHeader("something", "something");
    res.writeHead(103);

    res.setHeader("Content-Type", "text/plain");
    res.setHeader("Content-Length", "12");
    res.writeHead(200);
    res.write("data reached");
    res.end();
    return;
  } else if (u.pathname.startsWith("/invalid_response_header/")) {
    // response headers with invalid characters in the name / value (RFC7540 Sec 10.3)
    let kind = u.pathname.slice("/invalid_response_header/".length);
    if (kind === "name_spaces") {
      res.setHeader("With Spaces", "Hello");
    } else if (kind === "value_line_feed") {
      res.setHeader("invalid-header", "line\nfeed");
    } else if (kind === "value_carriage_return") {
      res.setHeader("invalid-header", "carriage\rreturn");
    } else if (kind === "value_null") {
      res.setHeader("invalid-header", "null\0");
    }

    res.writeHead(200);
    res.end("");
    return;
  } else if (u.pathname === "/origin_header") {
    let originHeader = req.headers.origin;
    res.setHeader("Content-Length", originHeader.length);
    res.setHeader("Content-Type", "text/plain");
    res.writeHead(200);
    res.write(originHeader);
    res.end();
    return;
  }

  res.setHeader("Content-Type", "text/html");
  if (req.httpVersionMajor != 2) {
    res.setHeader("Connection", "close");
  }
  res.writeHead(200);
  res.end(content);
}

// Set up the SSL certs for our server - this server has a cert for foo.example.com
// signed by netwerk/tests/unit/http2-ca.pem
var options = {
  key: fs.readFileSync(__dirname + "/http2-cert.key"),
  cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
};

if (process.env.HTTP2_LOG !== undefined) {
  var log_module = node_http2_root + "/test/util";
  options.log = require(log_module).createLogger("server");
}

var server = http2.createServer(options, handleRequest);

server.on("connection", function (socket) {
  socket.on("error", function () {
    // Ignoring SSL socket errors, since they usually represent a connection that was tore down
    // by the browser because of an untrusted certificate. And this happens at least once, when
    // the first test case if done.
  });
});

server.on("connect", function (req, clientSocket) {
  clientSocket.write(
    "HTTP/1.1 404 Not Found\r\nProxy-agent: Node.js-Proxy\r\n\r\n"
  );
  clientSocket.destroy();
});

function makeid(length) {
  var result = "";
  var characters =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  var charactersLength = characters.length;
  for (var i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
}

let globalObjects = {};
var serverPort;

const listen = (serv, envport) => {
  if (!serv) {
    return Promise.resolve(0);
  }

  let portSelection = 0;
  if (envport !== undefined) {
    try {
      portSelection = parseInt(envport, 10);
    } catch (e) {
      portSelection = -1;
    }
  }
  return new Promise(resolve => {
    serv.listen(portSelection, "0.0.0.0", 2000, () => {
      resolve(serv.address().port);
    });
  });
};

const http = require("http");
let httpServer = http.createServer((req, res) => {
  if (req.method != "POST") {
    let u = url.parse(req.url, true);
    if (u.pathname == "/test") {
      // This path is used to test that the server is working properly
      res.writeHead(200);
      res.end("OK");
      return;
    }
    res.writeHead(405);
    res.end("Unexpected method: " + req.method);
    return;
  }

  let code = "";
  req.on("data", function receivePostData(chunk) {
    code += chunk;
  });
  req.on("end", function finishPost() {
    let u = url.parse(req.url, true);
    if (u.pathname == "/fork") {
      let id = forkProcess();
      computeAndSendBackResponse(id);
      return;
    }

    if (u.pathname == "/forkH3Server") {
      forkH3Server(u.query.path, u.query.dbPath)
        .then(result => {
          computeAndSendBackResponse(result);
        })
        .catch(error => {
          computeAndSendBackResponse(error);
        });
      return;
    }

    if (u.pathname.startsWith("/kill/")) {
      let id = u.pathname.slice(6);
      let forked = globalObjects[id];
      if (!forked) {
        computeAndSendBackResponse(undefined, new Error("could not find id"));
        return;
      }

      new Promise((resolve, reject) => {
        forked.resolve = resolve;
        forked.reject = reject;
        forked.kill();
      })
        .then(x =>
          computeAndSendBackResponse(
            undefined,
            new Error(`incorrectly resolved ${x}`)
          )
        )
        .catch(e => {
          // We indicate a proper shutdown by resolving with undefined.
          if (e && e.toString().match(/child process exit closing code/)) {
            e = undefined;
          }
          computeAndSendBackResponse(undefined, e);
        });
      return;
    }

    if (u.pathname.startsWith("/execute/")) {
      let id = u.pathname.slice(9);
      let forked = globalObjects[id];
      if (!forked) {
        computeAndSendBackResponse(undefined, new Error("could not find id"));
        return;
      }

      new Promise((resolve, reject) => {
        forked.resolve = resolve;
        forked.reject = reject;
        forked.send({ code });
      })
        .then(x => sendBackResponse(x))
        .catch(e => computeAndSendBackResponse(undefined, e));
    }

    function computeAndSendBackResponse(evalResult, e) {
      let output = { result: evalResult, error: "", errorStack: "" };
      if (e) {
        output.error = e.toString();
        output.errorStack = e.stack;
      }
      sendBackResponse(output);
    }

    function sendBackResponse(output) {
      output = JSON.stringify(output);

      res.setHeader("Content-Length", output.length);
      res.setHeader("Content-Type", "application/json");
      res.writeHead(200);
      res.write(output);
      res.end("");
    }
  });
});

function forkH3Server(serverPath, dbPath) {
  const args = [dbPath];
  let process = spawn(serverPath, args);
  let id = forkProcessInternal(process);
  // Return a promise that resolves when we receive data from stdout
  return new Promise((resolve, _) => {
    process.stdout.on("data", data => {
      console.log(data.toString());
      resolve({ id, output: data.toString().trim() });
    });
  });
}

function forkProcess() {
  let scriptPath = path.resolve(__dirname, "moz-http2-child.js");
  let forked = fork(scriptPath);
  return forkProcessInternal(forked);
}

function forkProcessInternal(forked) {
  let id = makeid(6);
  forked.errors = "";
  globalObjects[id] = forked;
  forked.on("message", msg => {
    if (forked.resolve) {
      forked.resolve(msg);
      forked.resolve = null;
    } else {
      console.log(
        `forked process without handler sent: ${JSON.stringify(msg)}`
      );
      forked.errors += `forked process without handler sent: ${JSON.stringify(
        msg
      )}\n`;
    }
  });

  let exitFunction = (code, signal) => {
    if (globalObjects[id]) {
      delete globalObjects[id];
    } else {
      // already called
      return;
    }

    if (!forked.reject) {
      console.log(
        `child process ${id} closing code: ${code} signal: ${signal}`
      );
      return;
    }

    if (forked.errors != "") {
      forked.reject(forked.errors);
      forked.errors = "";
      forked.reject = null;
      return;
    }

    forked.reject(`child process exit closing code: ${code} signal: ${signal}`);
    forked.reject = null;
  };

  forked.on("error", exitFunction);
  forked.on("close", exitFunction);
  forked.on("exit", exitFunction);

  return id;
}

Promise.all([
  listen(server, process.env.MOZHTTP2_PORT).then(port => (serverPort = port)),
  listen(httpServer, process.env.MOZNODE_EXEC_PORT),
]).then(([sPort, nodeExecPort]) => {
  console.log(`HTTP2 server listening on ports ${sPort},${nodeExecPort}`);
});
