// test HTTP/2

"use strict";

/* import-globals-from head_channels.js */

// Generate a small and a large post with known pre-calculated md5 sums
function generateContent(size) {
  var content = "";
  for (var i = 0; i < size; i++) {
    content += "0";
  }
  return content;
}

var posts = [];
posts.push(generateContent(10));
posts.push(generateContent(250000));
posts.push(generateContent(128000));

// pre-calculated md5sums (in hex) of the above posts
var md5s = [
  "f1b708bba17f1ce948dc979f4d7092bc",
  "2ef8d3b6c8f329318eb1a119b12622b6",
];

var bigListenerData = generateContent(128 * 1024);
var bigListenerMD5 = "8f607cfdd2c87d6a7eedb657dafbd836";

function checkIsHttp2(request) {
  try {
    if (request.getResponseHeader("X-Firefox-Spdy") == "h2") {
      if (request.getResponseHeader("X-Connection-Http2") == "yes") {
        return true;
      }
      return false; // Weird case, but the server disagrees with us
    }
  } catch (e) {
    // Nothing to do here
  }
  return false;
}

var Http2CheckListener = function() {};

Http2CheckListener.prototype = {
  onStartRequestFired: false,
  onDataAvailableFired: false,
  isHttp2Connection: false,
  shouldBeHttp2: true,
  accum: 0,
  expected: -1,
  shouldSucceed: true,

  onStartRequest: function testOnStartRequest(request) {
    this.onStartRequestFired = true;
    if (this.shouldSucceed && !Components.isSuccessCode(request.status)) {
      do_throw("Channel should have a success code! (" + request.status + ")");
    } else if (
      !this.shouldSucceed &&
      Components.isSuccessCode(request.status)
    ) {
      do_throw("Channel succeeded unexpectedly!");
    }

    Assert.ok(request instanceof Ci.nsIHttpChannel);
    Assert.equal(request.requestSucceeded, this.shouldSucceed);
    if (this.shouldSucceed) {
      Assert.equal(request.responseStatus, 200);
    }
  },

  onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
    this.onDataAvailableFired = true;
    this.isHttp2Connection = checkIsHttp2(request);
    this.accum += cnt;
    read_stream(stream, cnt);
  },

  onStopRequest: function testOnStopRequest(request, status) {
    Assert.ok(this.onStartRequestFired);
    if (this.expected != -1) {
      Assert.equal(this.accum, this.expected);
    }

    if (this.shouldSucceed) {
      Assert.ok(Components.isSuccessCode(status));
      Assert.ok(this.onDataAvailableFired);
      Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);
    } else {
      Assert.ok(!Components.isSuccessCode(status));
    }
    request.QueryInterface(Ci.nsIProxiedChannel);
    var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
    this.finish({ httpProxyConnectResponseCode });
  },
};

/*
 * Support for testing valid multiplexing of streams
 */

var multiplexContent = generateContent(30 * 1024);

/* Listener class to control the testing of multiplexing */
var Http2MultiplexListener = function() {};

Http2MultiplexListener.prototype = new Http2CheckListener();

Http2MultiplexListener.prototype.streamID = 0;
Http2MultiplexListener.prototype.buffer = "";

Http2MultiplexListener.prototype.onDataAvailable = function(
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.isHttp2Connection = checkIsHttp2(request);
  this.streamID = parseInt(request.getResponseHeader("X-Http2-StreamID"));
  var data = read_stream(stream, cnt);
  this.buffer = this.buffer.concat(data);
};

Http2MultiplexListener.prototype.onStopRequest = function(request, status) {
  Assert.ok(this.onStartRequestFired);
  Assert.ok(this.onDataAvailableFired);
  Assert.ok(this.isHttp2Connection);
  Assert.ok(this.buffer == multiplexContent);

  request.QueryInterface(Ci.nsIProxiedChannel);
  // This is what does most of the hard work for us
  var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
  var streamID = this.streamID;
  this.finish({ httpProxyConnectResponseCode, streamID });
};

// Does the appropriate checks for header gatewaying
var Http2HeaderListener = function(name, callback) {
  this.name = name;
  this.callback = callback;
};

Http2HeaderListener.prototype = new Http2CheckListener();
Http2HeaderListener.prototype.value = "";

Http2HeaderListener.prototype.onDataAvailable = function(
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.isHttp2Connection = checkIsHttp2(request);
  var hvalue = request.getResponseHeader(this.name);
  Assert.notEqual(hvalue, "");
  this.callback(hvalue);
  read_stream(stream, cnt);
};

var Http2PushListener = function(shouldBePushed) {
  this.shouldBePushed = shouldBePushed;
};

Http2PushListener.prototype = new Http2CheckListener();

Http2PushListener.prototype.onDataAvailable = function(
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.isHttp2Connection = checkIsHttp2(request);
  if (
    request.originalURI.spec ==
      `https://localhost:${this.serverPort}/push.js` ||
    request.originalURI.spec ==
      `https://localhost:${this.serverPort}/push2.js` ||
    request.originalURI.spec == `https://localhost:${this.serverPort}/push5.js`
  ) {
    Assert.equal(
      request.getResponseHeader("pushed"),
      this.shouldBePushed ? "yes" : "no"
    );
  }
  read_stream(stream, cnt);
};

const pushHdrTxt =
  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const pullHdrTxt = pushHdrTxt
  .split("")
  .reverse()
  .join("");

function checkContinuedHeaders(getHeader, headerPrefix, headerText) {
  for (var i = 0; i < 265; i++) {
    Assert.equal(getHeader(headerPrefix + 1), headerText);
  }
}

var Http2ContinuedHeaderListener = function() {};

Http2ContinuedHeaderListener.prototype = new Http2CheckListener();

Http2ContinuedHeaderListener.prototype.onStopsLeft = 2;

Http2ContinuedHeaderListener.prototype.QueryInterface = ChromeUtils.generateQI([
  "nsIHttpPushListener",
  "nsIStreamListener",
]);

Http2ContinuedHeaderListener.prototype.getInterface = function(aIID) {
  return this.QueryInterface(aIID);
};

Http2ContinuedHeaderListener.prototype.onDataAvailable = function(
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.isHttp2Connection = checkIsHttp2(request);
  if (
    request.originalURI.spec ==
    `https://localhost:${this.serverPort}/continuedheaders`
  ) {
    // This is the original request, so the only one where we'll have continued response headers
    checkContinuedHeaders(
      request.getResponseHeader,
      "X-Pull-Test-Header-",
      pullHdrTxt
    );
  }
  read_stream(stream, cnt);
};

Http2ContinuedHeaderListener.prototype.onStopRequest = function(
  request,
  status
) {
  Assert.ok(this.onStartRequestFired);
  Assert.ok(Components.isSuccessCode(status));
  Assert.ok(this.onDataAvailableFired);
  Assert.ok(this.isHttp2Connection);

  --this.onStopsLeft;
  if (this.onStopsLeft === 0) {
    request.QueryInterface(Ci.nsIProxiedChannel);
    var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
    this.finish({ httpProxyConnectResponseCode });
  }
};

Http2ContinuedHeaderListener.prototype.onPush = function(
  associatedChannel,
  pushChannel
) {
  Assert.equal(
    associatedChannel.originalURI.spec,
    "https://localhost:" + this.serverPort + "/continuedheaders"
  );
  Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true");
  checkContinuedHeaders(
    pushChannel.getRequestHeader,
    "X-Push-Test-Header-",
    pushHdrTxt
  );

  pushChannel.asyncOpen(this);
};

// Does the appropriate checks for a large GET response
var Http2BigListener = function() {};

Http2BigListener.prototype = new Http2CheckListener();
Http2BigListener.prototype.buffer = "";

Http2BigListener.prototype.onDataAvailable = function(
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.isHttp2Connection = checkIsHttp2(request);
  this.buffer = this.buffer.concat(read_stream(stream, cnt));
  // We know the server should send us the same data as our big post will be,
  // so the md5 should be the same
  Assert.equal(bigListenerMD5, request.getResponseHeader("X-Expected-MD5"));
};

Http2BigListener.prototype.onStopRequest = function(request, status) {
  Assert.ok(this.onStartRequestFired);
  Assert.ok(this.onDataAvailableFired);
  Assert.ok(this.isHttp2Connection);

  // Don't want to flood output, so don't use do_check_eq
  Assert.ok(this.buffer == bigListenerData);

  request.QueryInterface(Ci.nsIProxiedChannel);
  var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
  this.finish({ httpProxyConnectResponseCode });
};

var Http2HugeSuspendedListener = function() {};

Http2HugeSuspendedListener.prototype = new Http2CheckListener();
Http2HugeSuspendedListener.prototype.count = 0;

Http2HugeSuspendedListener.prototype.onDataAvailable = function(
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.isHttp2Connection = checkIsHttp2(request);
  this.count += cnt;
  read_stream(stream, cnt);
};

Http2HugeSuspendedListener.prototype.onStopRequest = function(request, status) {
  Assert.ok(this.onStartRequestFired);
  Assert.ok(this.onDataAvailableFired);
  Assert.ok(this.isHttp2Connection);
  Assert.equal(this.count, 1024 * 1024 * 1); // 1mb of data expected
  request.QueryInterface(Ci.nsIProxiedChannel);
  var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
  this.finish({ httpProxyConnectResponseCode });
};

// Does the appropriate checks for POSTs
var Http2PostListener = function(expected_md5) {
  this.expected_md5 = expected_md5;
};

Http2PostListener.prototype = new Http2CheckListener();
Http2PostListener.prototype.expected_md5 = "";

Http2PostListener.prototype.onDataAvailable = function(
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.isHttp2Connection = checkIsHttp2(request);
  read_stream(stream, cnt);
  Assert.equal(
    this.expected_md5,
    request.getResponseHeader("X-Calculated-MD5")
  );
};

function createPrincipal(url) {
  var ssm = Services.scriptSecurityManager;
  try {
    return ssm.createContentPrincipal(Services.io.newURI(url), {});
  } catch (e) {
    return null;
  }
}

function makeChan(url, with_proxy) {
  if (with_proxy) {
    return Services.io
      .newChannelFromURIWithProxyFlags(
        Services.io.newURI(url),
        null,
        Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL,
        null,
        createPrincipal(url),
        createPrincipal(url),
        Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
        Ci.nsIContentPolicy.TYPE_OTHER
      )
      .QueryInterface(Ci.nsIHttpChannel);
  }
  return NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);
}

var ResumeStalledChannelListener = function() {};

ResumeStalledChannelListener.prototype = {
  onStartRequestFired: false,
  onDataAvailableFired: false,
  isHttp2Connection: false,
  shouldBeHttp2: true,
  resumable: null,

  onStartRequest: function testOnStartRequest(request) {
    this.onStartRequestFired = true;
    if (!Components.isSuccessCode(request.status)) {
      do_throw("Channel should have a success code! (" + request.status + ")");
    }

    Assert.ok(request instanceof Ci.nsIHttpChannel);
    Assert.equal(request.responseStatus, 200);
    Assert.equal(request.requestSucceeded, true);
  },

  onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
    this.onDataAvailableFired = true;
    this.isHttp2Connection = checkIsHttp2(request);
    read_stream(stream, cnt);
  },

  onStopRequest: function testOnStopRequest(request, status) {
    Assert.ok(this.onStartRequestFired);
    Assert.ok(Components.isSuccessCode(status));
    Assert.ok(this.onDataAvailableFired);
    Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);
    this.resumable.resume();
  },
};

// test a large download that creates stream flow control and
// confirm we can do another independent stream while the download
// stream is stuck
async function test_http2_blocking_download(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/bigdownload");
  var internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
  internalChannel.initialRwin = 500000; // make the stream.suspend push back in h2
  var p = new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    listener.expected = 3 * 1024 * 1024;
    chan.asyncOpen(listener);
    chan.suspend();
  });
  // wait 5 seconds so that stream flow control kicks in and then see if we
  // can do a basic transaction (i.e. session not blocked). afterwards resume
  // channel
  do_timeout(5000, function() {
    var simpleChannel = makeChan("https://localhost:" + serverPort + "/");
    var sl = new ResumeStalledChannelListener();
    sl.resumable = chan;
    simpleChannel.asyncOpen(sl);
  });
  return p;
}

// Make sure we make a HTTP2 connection and both us and the server mark it as such
async function test_http2_basic(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/");
  var p = new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
  return p;
}

async function test_http2_basic_unblocked_dep(serverPort) {
  var chan = makeChan(
    "https://localhost:" + serverPort + "/basic_unblocked_dep"
  );
  var cos = chan.QueryInterface(Ci.nsIClassOfService);
  cos.addClassFlags(Ci.nsIClassOfService.Unblocked);
  return new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
}

// make sure we don't use h2 when disallowed
async function test_http2_nospdy(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/");
  return new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    var internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
    internalChannel.allowSpdy = false;
    listener.shouldBeHttp2 = false;
    chan.asyncOpen(listener);
  });
}

// Support for making sure XHR works over SPDY
function checkXhr(xhr, finish) {
  if (xhr.readyState != 4) {
    return;
  }

  Assert.equal(xhr.status, 200);
  Assert.equal(checkIsHttp2(xhr), true);
  finish();
}

// Fires off an XHR request over h2
async function test_http2_xhr(serverPort) {
  return new Promise(resolve => {
    var req = new XMLHttpRequest();
    req.open("GET", "https://localhost:" + serverPort + "/", true);
    req.addEventListener("readystatechange", function(evt) {
      checkXhr(req, resolve);
    });
    req.send(null);
  });
}

var Http2ConcurrentListener = function() {};

Http2ConcurrentListener.prototype = new Http2CheckListener();
Http2ConcurrentListener.prototype.count = 0;
Http2ConcurrentListener.prototype.target = 0;
Http2ConcurrentListener.prototype.reset = 0;
Http2ConcurrentListener.prototype.recvdHdr = 0;

Http2ConcurrentListener.prototype.onStopRequest = function(request, status) {
  this.count++;
  Assert.ok(this.isHttp2Connection);
  if (this.recvdHdr > 0) {
    Assert.equal(request.getResponseHeader("X-Recvd"), this.recvdHdr);
  }

  if (this.count == this.target) {
    if (this.reset > 0) {
      Services.prefs.setIntPref(
        "network.http.http2.default-concurrent",
        this.reset
      );
    }
    request.QueryInterface(Ci.nsIProxiedChannel);
    var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
    this.finish({ httpProxyConnectResponseCode });
  }
};

async function test_http2_concurrent(concurrent_channels, serverPort) {
  var p = new Promise(resolve => {
    var concurrent_listener = new Http2ConcurrentListener();
    concurrent_listener.finish = resolve;
    concurrent_listener.target = 201;
    concurrent_listener.reset = Services.prefs.getIntPref(
      "network.http.http2.default-concurrent"
    );
    Services.prefs.setIntPref("network.http.http2.default-concurrent", 100);

    for (var i = 0; i < concurrent_listener.target; i++) {
      concurrent_channels[i] = makeChan(
        "https://localhost:" + serverPort + "/750ms"
      );
      concurrent_channels[i].loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
      concurrent_channels[i].asyncOpen(concurrent_listener);
    }
  });
  return p;
}

async function test_http2_concurrent_post(concurrent_channels, serverPort) {
  return new Promise(resolve => {
    var concurrent_listener = new Http2ConcurrentListener();
    concurrent_listener.finish = resolve;
    concurrent_listener.target = 8;
    concurrent_listener.recvdHdr = posts[2].length;
    concurrent_listener.reset = Services.prefs.getIntPref(
      "network.http.http2.default-concurrent"
    );
    Services.prefs.setIntPref("network.http.http2.default-concurrent", 3);

    for (var i = 0; i < concurrent_listener.target; i++) {
      concurrent_channels[i] = makeChan(
        "https://localhost:" + serverPort + "/750msPost"
      );
      concurrent_channels[i].loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
      var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
        Ci.nsIStringInputStream
      );
      stream.data = posts[2];
      var uchan = concurrent_channels[i].QueryInterface(Ci.nsIUploadChannel);
      uchan.setUploadStream(stream, "text/plain", stream.available());
      concurrent_channels[i].requestMethod = "POST";
      concurrent_channels[i].asyncOpen(concurrent_listener);
    }
  });
}

// Test to make sure we get multiplexing right
async function test_http2_multiplex(serverPort) {
  var chan1 = makeChan("https://localhost:" + serverPort + "/multiplex1");
  var chan2 = makeChan("https://localhost:" + serverPort + "/multiplex2");
  var listener1 = new Http2MultiplexListener();
  var listener2 = new Http2MultiplexListener();

  var promises = [];
  var p1 = new Promise(resolve => {
    listener1.finish = resolve;
  });
  promises.push(p1);
  var p2 = new Promise(resolve => {
    listener2.finish = resolve;
  });
  promises.push(p2);

  chan1.asyncOpen(listener1);
  chan2.asyncOpen(listener2);
  return Promise.all(promises);
}

// Test to make sure we gateway non-standard headers properly
async function test_http2_header(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/header");
  var hvalue = "Headers are fun";
  chan.setRequestHeader("X-Test-Header", hvalue, false);
  return new Promise(resolve => {
    var listener = new Http2HeaderListener("X-Received-Test-Header", function(
      received_hvalue
    ) {
      Assert.equal(received_hvalue, hvalue);
    });
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
}

// Test to make sure headers with invalid characters in the name are rejected
async function test_http2_invalid_response_header(serverPort) {
  return new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    listener.shouldSucceed = false;
    var chan = makeChan(
      "https://localhost:" + serverPort + "/invalid_response_header"
    );
    chan.asyncOpen(listener);
  });
}

// Test to make sure cookies are split into separate fields before compression
async function test_http2_cookie_crumbling(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/cookie_crumbling");
  var cookiesSent = ["a=b", "c=d01234567890123456789", "e=f"].sort();
  chan.setRequestHeader("Cookie", cookiesSent.join("; "), false);
  return new Promise(resolve => {
    var listener = new Http2HeaderListener("X-Received-Header-Pairs", function(
      pairsReceived
    ) {
      var cookiesReceived = JSON.parse(pairsReceived)
        .filter(function(pair) {
          return pair[0] == "cookie";
        })
        .map(function(pair) {
          return pair[1];
        })
        .sort();
      Assert.equal(cookiesReceived.length, cookiesSent.length);
      cookiesReceived.forEach(function(cookieReceived, index) {
        Assert.equal(cookiesSent[index], cookieReceived);
      });
    });
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push1(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push");
  chan.loadGroup = loadGroup;
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push2(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push.js");
  chan.loadGroup = loadGroup;
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push3(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push2");
  chan.loadGroup = loadGroup;
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push4(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push2.js");
  chan.loadGroup = loadGroup;
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push5(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push5");
  chan.loadGroup = loadGroup;
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push6(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push5.js");
  chan.loadGroup = loadGroup;
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

// this is a basic test where the server sends a simple document with 2 header
// blocks. bug 1027364
async function test_http2_doubleheader(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/doubleheader");
  return new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
}

// Make sure we handle GETs that cover more than 2 frames properly
async function test_http2_big(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/big");
  return new Promise(resolve => {
    var listener = new Http2BigListener();
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
}

async function test_http2_huge_suspended(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/huge");
  return new Promise(resolve => {
    var listener = new Http2HugeSuspendedListener();
    listener.finish = resolve;
    chan.asyncOpen(listener);
    chan.suspend();
    do_timeout(500, chan.resume);
  });
}

// Support for doing a POST
function do_post(content, chan, listener, method) {
  var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
    Ci.nsIStringInputStream
  );
  stream.data = content;

  var uchan = chan.QueryInterface(Ci.nsIUploadChannel);
  uchan.setUploadStream(stream, "text/plain", stream.available());

  chan.requestMethod = method;

  chan.asyncOpen(listener);
}

// Make sure we can do a simple POST
async function test_http2_post(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/post");
  var p = new Promise(resolve => {
    var listener = new Http2PostListener(md5s[0]);
    listener.finish = resolve;
    do_post(posts[0], chan, listener, "POST");
  });
  return p;
}

async function test_http2_empty_post(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/post");
  var p = new Promise(resolve => {
    var listener = new Http2PostListener("0");
    listener.finish = resolve;
    do_post("", chan, listener, "POST");
  });
  return p;
}

// Make sure we can do a simple PATCH
async function test_http2_patch(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/patch");
  return new Promise(resolve => {
    var listener = new Http2PostListener(md5s[0]);
    listener.finish = resolve;
    do_post(posts[0], chan, listener, "PATCH");
  });
}

// Make sure we can do a POST that covers more than 2 frames
async function test_http2_post_big(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/post");
  return new Promise(resolve => {
    var listener = new Http2PostListener(md5s[1]);
    listener.finish = resolve;
    do_post(posts[1], chan, listener, "POST");
  });
}

// When a http proxy is used alt-svc is disable. Therefore if withProxy is true,
// try numberOfTries times to connect and make sure that alt-svc is not use and we never
// connect to the HTTP/2 server.
var altsvcClientListener = function(
  finish,
  httpserv,
  httpserv2,
  withProxy,
  numberOfTries
) {
  this.finish = finish;
  this.httpserv = httpserv;
  this.httpserv2 = httpserv2;
  this.withProxy = withProxy;
  this.numberOfTries = numberOfTries;
};

altsvcClientListener.prototype = {
  onStartRequest: function test_onStartR(request) {
    Assert.equal(request.status, Cr.NS_OK);
  },

  onDataAvailable: function test_ODA(request, stream, offset, cnt) {
    read_stream(stream, cnt);
  },

  onStopRequest: function test_onStopR(request, status) {
    var isHttp2Connection = checkIsHttp2(
      request.QueryInterface(Ci.nsIHttpChannel)
    );
    if (!isHttp2Connection) {
      dump("/altsvc1 not over h2 yet - retry\n");
      if (this.withProxy && this.numberOfTries == 0) {
        request.QueryInterface(Ci.nsIProxiedChannel);
        var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
        this.finish({ httpProxyConnectResponseCode });
        return;
      }
      let chan = makeChan(
        "http://foo.example.com:" + this.httpserv + "/altsvc1",
        this.withProxy
      ).QueryInterface(Ci.nsIHttpChannel);
      // we use this header to tell the server to issue a altsvc frame for the
      // speficied origin we will use in the next part of the test
      chan.setRequestHeader(
        "x-redirect-origin",
        "http://foo.example.com:" + this.httpserv2,
        false
      );
      chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
      chan.asyncOpen(
        new altsvcClientListener(
          this.finish,
          this.httpserv,
          this.httpserv2,
          this.withProxy,
          this.numberOfTries - 1
        )
      );
    } else {
      Assert.ok(isHttp2Connection);
      let chan = makeChan(
        "http://foo.example.com:" + this.httpserv2 + "/altsvc2"
      ).QueryInterface(Ci.nsIHttpChannel);
      chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
      chan.asyncOpen(
        new altsvcClientListener2(this.finish, this.httpserv, this.httpserv2)
      );
    }
  },
};

var altsvcClientListener2 = function(finish, httpserv, httpserv2) {
  this.finish = finish;
  this.httpserv = httpserv;
  this.httpserv2 = httpserv2;
};

altsvcClientListener2.prototype = {
  onStartRequest: function test_onStartR(request) {
    Assert.equal(request.status, Cr.NS_OK);
  },

  onDataAvailable: function test_ODA(request, stream, offset, cnt) {
    read_stream(stream, cnt);
  },

  onStopRequest: function test_onStopR(request, status) {
    var isHttp2Connection = checkIsHttp2(
      request.QueryInterface(Ci.nsIHttpChannel)
    );
    if (!isHttp2Connection) {
      dump("/altsvc2 not over h2 yet - retry\n");
      var chan = makeChan(
        "http://foo.example.com:" + this.httpserv2 + "/altsvc2"
      ).QueryInterface(Ci.nsIHttpChannel);
      chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
      chan.asyncOpen(
        new altsvcClientListener2(this.finish, this.httpserv, this.httpserv2)
      );
    } else {
      Assert.ok(isHttp2Connection);
      request.QueryInterface(Ci.nsIProxiedChannel);
      var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
      this.finish({ httpProxyConnectResponseCode });
    }
  },
};

async function test_http2_altsvc(httpserv, httpserv2, withProxy) {
  var chan = makeChan(
    "http://foo.example.com:" + httpserv + "/altsvc1",
    withProxy
  ).QueryInterface(Ci.nsIHttpChannel);
  return new Promise(resolve => {
    var numberOfTries = 0;
    if (withProxy) {
      numberOfTries = 20;
    }
    chan.asyncOpen(
      new altsvcClientListener(
        resolve,
        httpserv,
        httpserv2,
        withProxy,
        numberOfTries
      )
    );
  });
}

var Http2PushApiListener = function(finish, serverPort) {
  this.finish = finish;
  this.serverPort = serverPort;
};

Http2PushApiListener.prototype = {
  checksPending: 9, // 4 onDataAvailable and 5 onStop

  getInterface(aIID) {
    return this.QueryInterface(aIID);
  },

  QueryInterface: ChromeUtils.generateQI([
    "nsIHttpPushListener",
    "nsIStreamListener",
  ]),

  // nsIHttpPushListener
  onPush: function onPush(associatedChannel, pushChannel) {
    Assert.equal(
      associatedChannel.originalURI.spec,
      "https://localhost:" + this.serverPort + "/pushapi1"
    );
    Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true");

    pushChannel.asyncOpen(this);
    if (
      pushChannel.originalURI.spec ==
      "https://localhost:" + this.serverPort + "/pushapi1/2"
    ) {
      pushChannel.cancel(Cr.NS_ERROR_ABORT);
    } else if (
      pushChannel.originalURI.spec ==
      "https://localhost:" + this.serverPort + "/pushapi1/3"
    ) {
      Assert.ok(pushChannel.getRequestHeader("Accept-Encoding").includes("br"));
    }
  },

  // normal Channel listeners
  onStartRequest: function pushAPIOnStart(request) {},

  onDataAvailable: function pushAPIOnDataAvailable(
    request,
    stream,
    offset,
    cnt
  ) {
    Assert.notEqual(
      request.originalURI.spec,
      `https://localhost:${this.serverPort}/pushapi1/2`
    );

    var data = read_stream(stream, cnt);

    if (
      request.originalURI.spec ==
      `https://localhost:${this.serverPort}/pushapi1`
    ) {
      Assert.equal(data[0], "0");
      --this.checksPending;
    } else if (
      request.originalURI.spec ==
      `https://localhost:${this.serverPort}/pushapi1/1`
    ) {
      Assert.equal(data[0], "1");
      --this.checksPending; // twice
    } else if (
      request.originalURI.spec ==
      `https://localhost:${this.serverPort}/pushapi1/3`
    ) {
      Assert.equal(data[0], "3");
      --this.checksPending;
    } else {
      Assert.equal(true, false);
    }
  },

  onStopRequest: function test_onStopR(request, status) {
    if (
      request.originalURI.spec ==
      `https://localhost:${this.serverPort}/pushapi1/2`
    ) {
      Assert.equal(request.status, Cr.NS_ERROR_ABORT);
    } else {
      Assert.equal(request.status, Cr.NS_OK);
    }

    --this.checksPending; // 5 times - one for each push plus the pull
    if (!this.checksPending) {
      request.QueryInterface(Ci.nsIProxiedChannel);
      var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
      this.finish({ httpProxyConnectResponseCode });
    }
  },
};

// pushAPI testcase 1 expects
// 1 to pull /pushapi1 with 0
// 2 to see /pushapi1/1 with 1
// 3 to see /pushapi1/1 with 1 (again)
// 4 to see /pushapi1/2 that it will cancel
// 5 to see /pushapi1/3 with 3 with brotli

async function test_http2_pushapi_1(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/pushapi1");
  chan.loadGroup = loadGroup;
  return new Promise(resolve => {
    var listener = new Http2PushApiListener(resolve, serverPort);
    chan.notificationCallbacks = listener;
    chan.asyncOpen(listener);
  });
}

var WrongSuiteListener = function() {};

WrongSuiteListener.prototype = new Http2CheckListener();
WrongSuiteListener.prototype.shouldBeHttp2 = false;
WrongSuiteListener.prototype.onStopRequest = function(request, status) {
  Services.prefs.setBoolPref(
    "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256",
    true
  );
  Services.prefs.clearUserPref("security.tls.version.max");
  Http2CheckListener.prototype.onStopRequest.call(this, request, status);
};

// test that we use h1 without the mandatory cipher suite available when
// offering at most tls1.2
async function test_http2_wrongsuite_tls12(serverPort) {
  Services.prefs.setBoolPref(
    "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256",
    false
  );
  Services.prefs.setIntPref("security.tls.version.max", 3);
  var chan = makeChan("https://localhost:" + serverPort + "/wrongsuite");
  chan.loadFlags =
    Ci.nsIRequest.LOAD_FRESH_CONNECTION |
    Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
  return new Promise(resolve => {
    var listener = new WrongSuiteListener();
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
}

// test that we use h2 when offering tls1.3 or higher regardless of if the
// mandatory cipher suite is available
async function test_http2_wrongsuite_tls13(serverPort) {
  Services.prefs.setBoolPref(
    "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256",
    false
  );
  var chan = makeChan("https://localhost:" + serverPort + "/wrongsuite");
  chan.loadFlags =
    Ci.nsIRequest.LOAD_FRESH_CONNECTION |
    Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
  return new Promise(resolve => {
    var listener = new WrongSuiteListener();
    listener.finish = resolve;
    listener.shouldBeHttp2 = true;
    chan.asyncOpen(listener);
  });
}

async function test_http2_h11required_stream(serverPort) {
  var chan = makeChan(
    "https://localhost:" + serverPort + "/h11required_stream"
  );
  return new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    listener.shouldBeHttp2 = false;
    chan.asyncOpen(listener);
  });
}

function H11RequiredSessionListener() {}

H11RequiredSessionListener.prototype = new Http2CheckListener();

H11RequiredSessionListener.prototype.onStopRequest = function(request, status) {
  var streamReused = request.getResponseHeader("X-H11Required-Stream-Ok");
  Assert.equal(streamReused, "yes");

  Assert.ok(this.onStartRequestFired);
  Assert.ok(this.onDataAvailableFired);
  Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);

  request.QueryInterface(Ci.nsIProxiedChannel);
  var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
  this.finish({ httpProxyConnectResponseCode });
};

async function test_http2_h11required_session(serverPort) {
  var chan = makeChan(
    "https://localhost:" + serverPort + "/h11required_session"
  );
  return new Promise(resolve => {
    var listener = new H11RequiredSessionListener();
    listener.finish = resolve;
    listener.shouldBeHttp2 = false;
    chan.asyncOpen(listener);
  });
}

async function test_http2_retry_rst(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/rstonce");
  return new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
}

async function test_http2_continuations(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/continuedheaders");
  chan.loadGroup = loadGroup;
  return new Promise(resolve => {
    var listener = new Http2ContinuedHeaderListener();
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.notificationCallbacks = listener;
    chan.asyncOpen(listener);
  });
}

function Http2IllegalHpackValidationListener() {}

Http2IllegalHpackValidationListener.prototype = new Http2CheckListener();
Http2IllegalHpackValidationListener.prototype.shouldGoAway = false;

Http2IllegalHpackValidationListener.prototype.onStopRequest = function(
  request,
  status
) {
  var wentAway = request.getResponseHeader("X-Did-Goaway") === "yes";
  Assert.equal(wentAway, this.shouldGoAway);

  Assert.ok(this.onStartRequestFired);
  Assert.ok(this.onDataAvailableFired);
  Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);

  request.QueryInterface(Ci.nsIProxiedChannel);
  var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
  this.finish({ httpProxyConnectResponseCode });
};

function Http2IllegalHpackListener() {}
Http2IllegalHpackListener.prototype = new Http2CheckListener();
Http2IllegalHpackListener.prototype.shouldGoAway = false;

Http2IllegalHpackListener.prototype.onStopRequest = function(request, status) {
  var chan = makeChan(
    "https://localhost:" + this.serverPort + "/illegalhpack_validate"
  );
  var listener = new Http2IllegalHpackValidationListener();
  listener.finish = this.finish;
  listener.shouldGoAway = this.shouldGoAway;
  chan.asyncOpen(listener);
};

async function test_http2_illegalhpacksoft(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/illegalhpacksoft");
  return new Promise(resolve => {
    var listener = new Http2IllegalHpackListener();
    listener.finish = resolve;
    listener.serverPort = serverPort;
    listener.shouldGoAway = false;
    listener.shouldSucceed = false;
    chan.asyncOpen(listener);
  });
}

async function test_http2_illegalhpackhard(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/illegalhpackhard");
  return new Promise(resolve => {
    var listener = new Http2IllegalHpackListener();
    listener.finish = resolve;
    listener.serverPort = serverPort;
    listener.shouldGoAway = true;
    listener.shouldSucceed = false;
    chan.asyncOpen(listener);
  });
}

async function test_http2_folded_header(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/foldedheader");
  chan.loadGroup = loadGroup;
  return new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    listener.shouldSucceed = false;
    chan.asyncOpen(listener);
  });
}

async function test_http2_empty_data(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/emptydata");
  return new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push_firstparty1(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push");
  chan.loadGroup = loadGroup;
  chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" };
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push_firstparty2(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push.js");
  chan.loadGroup = loadGroup;
  chan.loadInfo.originAttributes = { firstPartyDomain: "bar.com" };
  return new Promise(resolve => {
    var listener = new Http2PushListener(false);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push_firstparty3(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push.js");
  chan.loadGroup = loadGroup;
  chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" };
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push_userContext1(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push");
  chan.loadGroup = loadGroup;
  chan.loadInfo.originAttributes = { userContextId: 1 };
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push_userContext2(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push.js");
  chan.loadGroup = loadGroup;
  chan.loadInfo.originAttributes = { userContextId: 2 };
  return new Promise(resolve => {
    var listener = new Http2PushListener(false);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_push_userContext3(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/push.js");
  chan.loadGroup = loadGroup;
  chan.loadInfo.originAttributes = { userContextId: 1 };
  return new Promise(resolve => {
    var listener = new Http2PushListener(true);
    listener.finish = resolve;
    listener.serverPort = serverPort;
    chan.asyncOpen(listener);
  });
}

async function test_http2_status_phrase(serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/statusphrase");
  return new Promise(resolve => {
    var listener = new Http2CheckListener();
    listener.finish = resolve;
    listener.shouldSucceed = false;
    chan.asyncOpen(listener);
  });
}

var PulledDiskCacheListener = function() {};
PulledDiskCacheListener.prototype = new Http2CheckListener();
PulledDiskCacheListener.prototype.EXPECTED_DATA = "this was pulled via h2";
PulledDiskCacheListener.prototype.readData = "";
PulledDiskCacheListener.prototype.onDataAvailable = function testOnDataAvailable(
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.isHttp2Connection = checkIsHttp2(request);
  this.accum += cnt;
  this.readData += read_stream(stream, cnt);
};
PulledDiskCacheListener.prototype.onStopRequest = function testOnStopRequest(
  request,
  status
) {
  Assert.equal(this.EXPECTED_DATA, this.readData);
  Http2CheckListener.prorotype.onStopRequest.call(this, request, status);
};

const DISK_CACHE_DATA = "this is from disk cache";

var FromDiskCacheListener = function(finish, loadGroup, serverPort) {
  this.finish = finish;
  this.loadGroup = loadGroup;
  this.serverPort = serverPort;
};
FromDiskCacheListener.prototype = {
  onStartRequestFired: false,
  onDataAvailableFired: false,
  readData: "",

  onStartRequest: function testOnStartRequest(request) {
    this.onStartRequestFired = true;
    if (!Components.isSuccessCode(request.status)) {
      do_throw("Channel should have a success code! (" + request.status + ")");
    }

    Assert.ok(request instanceof Ci.nsIHttpChannel);
    Assert.ok(request.requestSucceeded);
    Assert.equal(request.responseStatus, 200);
  },

  onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
    this.onDataAvailableFired = true;
    this.readData += read_stream(stream, cnt);
  },

  onStopRequest: function testOnStopRequest(request, status) {
    Assert.ok(this.onStartRequestFired);
    Assert.ok(Components.isSuccessCode(status));
    Assert.ok(this.onDataAvailableFired);
    Assert.equal(this.readData, DISK_CACHE_DATA);

    evict_cache_entries("disk");
    syncWithCacheIOThread(() => {
      // Now that we know the entry is out of the disk cache, check to make sure
      // we don't have this hiding in the push cache somewhere - if we do, it
      // didn't get cancelled, and we have a bug.
      var chan = makeChan(
        "https://localhost:" + this.serverPort + "/diskcache"
      );
      var listener = new PulledDiskCacheListener();
      listener.finish = this.finish;
      chan.loadGroup = this.loadGroup;
      chan.asyncOpen(listener);
    });
  },
};

var Http2DiskCachePushListener = function() {};
Http2DiskCachePushListener.prototype = new Http2CheckListener();

Http2DiskCachePushListener.onStopRequest = function(request, status) {
  Assert.ok(this.onStartRequestFired);
  Assert.ok(Components.isSuccessCode(status));
  Assert.ok(this.onDataAvailableFired);
  Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);

  // Now we need to open a channel to ensure we get data from the disk cache
  // for the pushed item, instead of from the push cache.
  var chan = makeChan("https://localhost:" + this.serverPort + "/diskcache");
  var listener = new FromDiskCacheListener(
    this.finish,
    this.loadGroup,
    this.serverPort
  );
  chan.loadGroup = this.loadGroup;
  chan.asyncOpen(listener);
};

function continue_test_http2_disk_cache_push(
  status,
  entry,
  finish,
  loadGroup,
  serverPort
) {
  // TODO - store stuff in cache entry, then open an h2 channel that will push
  // this, once that completes, open a channel for the cache entry we made and
  // ensure it came from disk cache, not the push cache.
  var outputStream = entry.openOutputStream(0, -1);
  outputStream.write(DISK_CACHE_DATA, DISK_CACHE_DATA.length);

  // Now we open our URL that will push data for the URL above
  var chan = makeChan("https://localhost:" + serverPort + "/pushindisk");
  var listener = new Http2DiskCachePushListener();
  listener.finish = finish;
  listener.loadGroup = loadGroup;
  listener.serverPort = serverPort;
  chan.loadGroup = loadGroup;
  chan.asyncOpen(listener);
}

async function test_http2_disk_cache_push(loadGroup, serverPort) {
  return new Promise(resolve => {
    asyncOpenCacheEntry(
      "https://localhost:" + serverPort + "/diskcache",
      "disk",
      Ci.nsICacheStorage.OPEN_NORMALLY,
      null,
      function(status, entry) {
        continue_test_http2_disk_cache_push(
          status,
          entry,
          resolve,
          loadGroup,
          serverPort
        );
      },
      false
    );
  });
}

var Http2DoublepushListener = function() {};
Http2DoublepushListener.prototype = new Http2CheckListener();
Http2DoublepushListener.prototype.onStopRequest = function(request, status) {
  Assert.ok(this.onStartRequestFired);
  Assert.ok(Components.isSuccessCode(status));
  Assert.ok(this.onDataAvailableFired);
  Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);

  var chan = makeChan("https://localhost:" + this.serverPort + "/doublypushed");
  var listener = new Http2DoublypushedListener();
  listener.finish = this.finish;
  chan.loadGroup = this.loadGroup;
  chan.asyncOpen(listener);
};

var Http2DoublypushedListener = function() {};
Http2DoublypushedListener.prototype = new Http2CheckListener();
Http2DoublypushedListener.prototype.readData = "";
Http2DoublypushedListener.prototype.onDataAvailable = function(
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.accum += cnt;
  this.readData += read_stream(stream, cnt);
};
Http2DoublypushedListener.prototype.onStopRequest = function(request, status) {
  Assert.ok(this.onStartRequestFired);
  Assert.ok(Components.isSuccessCode(status));
  Assert.ok(this.onDataAvailableFired);
  Assert.equal(this.readData, "pushed");

  request.QueryInterface(Ci.nsIProxiedChannel);
  let httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
  this.finish({ httpProxyConnectResponseCode });
};

function test_http2_doublepush(loadGroup, serverPort) {
  var chan = makeChan("https://localhost:" + serverPort + "/doublepush");
  return new Promise(resolve => {
    var listener = new Http2DoublepushListener();
    listener.finish = resolve;
    listener.loadGroup = loadGroup;
    listener.serverPort = serverPort;
    chan.loadGroup = loadGroup;
    chan.asyncOpen(listener);
  });
}