// test HTTP/2

Cu.importGlobalProperties(["XMLHttpRequest"]);

// 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));
    }

    run_next_test();
    do_test_finished();
  }
};

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

var multiplexContent = generateContent(30*1024);
var completed_channels = [];
function register_completed_channel(listener) {
  completed_channels.push(listener);
  if (completed_channels.length == 2) {
    Assert.notEqual(completed_channels[0].streamID, completed_channels[1].streamID);
    run_next_test();
    do_test_finished();
  }
}

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

  // This is what does most of the hard work for us
  register_completed_channel(this);
};

// 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:" + serverPort + "/push.js"  ||
      request.originalURI.spec == "https://localhost:" + serverPort + "/push2.js" ||
      request.originalURI.spec == "https://localhost:" + 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:" + 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) {
    run_next_test();
    do_test_finished();
  }
};

Http2ContinuedHeaderListener.prototype.onPush = function(associatedChannel, pushChannel) {
  Assert.equal(associatedChannel.originalURI.spec, "https://localhost:" + 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);

  run_next_test();
  do_test_finished();
};

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
  run_next_test();
  do_test_finished();
};

// 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 makeChan(url) {
  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
function test_http2_blocking_download() {
  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 listener = new Http2CheckListener();
  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);
  });
}

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

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

// make sure we don't use h2 when disallowed
function test_http2_nospdy() {
  var chan = makeChan("https://localhost:" + serverPort + "/");
  var listener = new Http2CheckListener();
  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) {
  if (xhr.readyState != 4) {
    return;
  }

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

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

var concurrent_channels = [];

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) {
      prefs.setIntPref("network.http.spdy.default-concurrent", this.reset);
    }
    run_next_test();
    do_test_finished();
  }
};

function test_http2_concurrent() {
  var concurrent_listener = new Http2ConcurrentListener();
  concurrent_listener.target = 201;
  concurrent_listener.reset = prefs.getIntPref("network.http.spdy.default-concurrent");
  prefs.setIntPref("network.http.spdy.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);
  }
}

function test_http2_concurrent_post() {
  var concurrent_listener = new Http2ConcurrentListener();
  concurrent_listener.target = 8;
  concurrent_listener.recvdHdr = posts[2].length;
  concurrent_listener.reset = prefs.getIntPref("network.http.spdy.default-concurrent");
  prefs.setIntPref("network.http.spdy.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
function test_http2_multiplex() {
  var chan1 = makeChan("https://localhost:" + serverPort + "/multiplex1");
  var chan2 = makeChan("https://localhost:" + serverPort + "/multiplex2");
  var listener1 = new Http2MultiplexListener();
  var listener2 = new Http2MultiplexListener();
  chan1.asyncOpen(listener1);
  chan2.asyncOpen(listener2);
}

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

// Test to make sure cookies are split into separate fields before compression
function test_http2_cookie_crumbling() {
  var chan = makeChan("https://localhost:" + serverPort + "/cookie_crumbling");
  var cookiesSent = ['a=b', 'c=d01234567890123456789', 'e=f'].sort();
  chan.setRequestHeader("Cookie", cookiesSent.join('; '), false);
  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)
    });
  });
  chan.asyncOpen(listener);
}

function test_http2_push1() {
  var chan = makeChan("https://localhost:" + serverPort + "/push");
  chan.loadGroup = loadGroup;
  var listener = new Http2PushListener(true);
  chan.asyncOpen(listener);
}

function test_http2_push2() {
  var chan = makeChan("https://localhost:" + serverPort + "/push.js");
  chan.loadGroup = loadGroup;
  var listener = new Http2PushListener(true);
  chan.asyncOpen(listener);
}

function test_http2_push3() {
  var chan = makeChan("https://localhost:" + serverPort + "/push2");
  chan.loadGroup = loadGroup;
  var listener = new Http2PushListener(true);
  chan.asyncOpen(listener);
}

function test_http2_push4() {
  var chan = makeChan("https://localhost:" + serverPort + "/push2.js");
  chan.loadGroup = loadGroup;
  var listener = new Http2PushListener(true);
  chan.asyncOpen(listener);
}

function test_http2_push5() {
  var chan = makeChan("https://localhost:" + serverPort + "/push5");
  chan.loadGroup = loadGroup;
  var listener = new Http2PushListener(true);
  chan.asyncOpen(listener);
}

function test_http2_push6() {
  var chan = makeChan("https://localhost:" + serverPort + "/push5.js");
  chan.loadGroup = loadGroup;
  var listener = new Http2PushListener(true);
  chan.asyncOpen(listener);
}

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

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

function test_http2_huge_suspended() {
  var chan = makeChan("https://localhost:" + serverPort + "/huge");
  var listener = new Http2HugeSuspendedListener();
  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
function test_http2_post() {
  var chan = makeChan("https://localhost:" + serverPort + "/post");
  var listener = new Http2PostListener(md5s[0]);
  do_post(posts[0], chan, listener, "POST");
}

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

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

const {HttpServer} = ChromeUtils.import("resource://testing-common/httpd.js");

var httpserv = null;
var httpserv2 = null;
var ios = Cc["@mozilla.org/network/io-service;1"]
            .getService(Ci.nsIIOService);

var altsvcClientListener = {
  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");
      var chan = makeChan("http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1")
                .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:" + httpserv2.identity.primaryPort, false);
      chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
      chan.asyncOpen(altsvcClientListener);
    } else {
      Assert.ok(isHttp2Connection);
      var chan = makeChan("http://foo.example.com:" + httpserv2.identity.primaryPort + "/altsvc2")
                .QueryInterface(Ci.nsIHttpChannel);
      chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
      chan.asyncOpen(altsvcClientListener2);
    }
  }
};

var altsvcClientListener2 = {
  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:" + httpserv2.identity.primaryPort + "/altsvc2")
                .QueryInterface(Ci.nsIHttpChannel);
      chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
      chan.asyncOpen(altsvcClientListener2);
    } else {
      Assert.ok(isHttp2Connection);
      run_next_test();
      do_test_finished();
    }
  }
};

function altsvcHttp1Server(metadata, response) {
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.setHeader("Content-Type", "text/plain", false);
  response.setHeader("Connection", "close", false);
  response.setHeader("Alt-Svc", 'h2=":' + serverPort + '"', false);
  var body = "this is where a cool kid would write something neat.\n";
  response.bodyOutputStream.write(body, body.length);
}

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

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

function altsvcHttp1Server2(metadata, response) {
// this server should never be used thanks to an alt svc frame from the
// h2 server.. but in case of some async lag in setting the alt svc route
// up we have it.
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.setHeader("Content-Type", "text/plain", false);
  response.setHeader("Connection", "close", false);
  var body = "hanging.\n";
  response.bodyOutputStream.write(body, body.length);
}

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

  var body = '["http://foo.example.com:' + httpserv2.identity.primaryPort + '"]';
  response.bodyOutputStream.write(body, body.length);
}
function test_http2_altsvc() {
  var chan = makeChan("http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1")
           .QueryInterface(Ci.nsIHttpChannel);
  chan.asyncOpen(altsvcClientListener);
}

var Http2PushApiListener = function() {};

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:" + serverPort + "/pushapi1");
    Assert.equal (pushChannel.getRequestHeader("x-pushed-request"), "true");

    pushChannel.asyncOpen(this);
    if (pushChannel.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/2") {
      pushChannel.cancel(Cr.NS_ERROR_ABORT);
    } else if (pushChannel.originalURI.spec == "https://localhost:" + 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:" + serverPort + "/pushapi1/2");

    var data = read_stream(stream, cnt);

    if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1") {
      Assert.equal(data[0], '0');
      --this.checksPending;
    } else if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/1") {
      Assert.equal(data[0], '1');
      --this.checksPending; // twice
    } else if (request.originalURI.spec == "https://localhost:" + 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:" + 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) {
      run_next_test();
      do_test_finished();
    }
  }
};

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

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

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

// test that we use h1 without the mandatory cipher suite available when
// offering at most tls1.2
function test_http2_wrongsuite_tls12() {
  prefs.setBoolPref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", false);
  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;
  var listener = new WrongSuiteListener();
  chan.asyncOpen(listener);
}

// test that we use h2 when offering tls1.3 or higher regardless of if the
// mandatory cipher suite is available
function test_http2_wrongsuite_tls13() {
  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;
  var listener = new WrongSuiteListener();
  listener.shouldBeHttp2 = true;
  chan.asyncOpen(listener);
}

function test_http2_h11required_stream() {
  var chan = makeChan("https://localhost:" + serverPort + "/h11required_stream");
  var listener = new Http2CheckListener();
  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);

  run_next_test();
  do_test_finished();
};

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

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

function test_http2_continuations() {
  var chan = makeChan("https://localhost:" + serverPort + "/continuedheaders");
  chan.loadGroup = loadGroup;
  var listener = new Http2ContinuedHeaderListener();
  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);

  run_next_test();
  do_test_finished();
};

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

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

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

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

function test_http2_folded_header() {
  var chan = makeChan("https://localhost:" + serverPort + "/foldedheader");
  chan.loadGroup = loadGroup;
  var listener = new Http2CheckListener();
  listener.shouldSucceed = false;
  chan.asyncOpen(listener);
}

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

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

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

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

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

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

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

function test_http2_status_phrase() {
  var chan = makeChan("https://localhost:" + serverPort + "/statusphrase");
  var listener = new Http2CheckListener();
  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() {};
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:" + serverPort + "/diskcache");
      chan.listener = new PulledDiskCacheListener();
      chan.loadGroup = 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:" + serverPort + "/diskcache");
    chan.listener = new FromDiskCacheListener();
    chan.loadGroup = loadGroup;
    chan.asyncOpen(listener);
};

function continue_test_http2_disk_cache_push(status, entry, appCache) {
  // 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();
  chan.loadGroup = loadGroup;
  chan.asyncOpen(listener);
}

function test_http2_disk_cache_push() {
  asyncOpenCacheEntry("https://localhost:" + serverPort + "/diskcache",
                      "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null,
                      continue_test_http2_disk_cache_push, false);
}

function test_complete() {
  resetPrefs();
  do_test_pending();
  httpserv.stop(do_test_finished);
  do_test_pending();
  httpserv2.stop(do_test_finished);

  do_test_finished();
  do_timeout(0,run_next_test);
}

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:" + serverPort + "/doublypushed");
  var listener = new Http2DoublypushedListener();
  chan.loadGroup = 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");

  run_next_test();
  do_test_finished();
};

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

// hack - the header test resets the multiplex object on the server,
// so make sure header is always run before the multiplex test.
//
// make sure post_big runs first to test race condition in restarting
// a stalled stream when a SETTINGS frame arrives
var tests = [ test_http2_post_big
            , test_http2_basic
            , test_http2_concurrent
            , test_http2_concurrent_post
            , test_http2_basic_unblocked_dep
            , test_http2_nospdy
            , test_http2_push1
            , test_http2_push2
            , test_http2_push3
            , test_http2_push4
            , test_http2_push5
            , test_http2_push6
            , test_http2_altsvc
            , test_http2_doubleheader
            , test_http2_xhr
            , test_http2_header
            , test_http2_cookie_crumbling
            , test_http2_multiplex
            , test_http2_big
            , test_http2_huge_suspended
            , test_http2_post
            , test_http2_patch
            , test_http2_pushapi_1
            , test_http2_continuations
            , test_http2_blocking_download
            , test_http2_illegalhpacksoft
            , test_http2_illegalhpackhard
            , test_http2_folded_header
            , test_http2_empty_data
            , test_http2_status_phrase
            , test_http2_doublepush
            , test_http2_disk_cache_push
            // Add new tests above here - best to add new tests before h1
            // streams get too involved
            // These next two must always come in this order
            , test_http2_h11required_stream
            , test_http2_h11required_session
            , test_http2_retry_rst
            , test_http2_wrongsuite_tls12
            , test_http2_wrongsuite_tls13
            , test_http2_push_firstparty1
            , test_http2_push_firstparty2
            , test_http2_push_firstparty3
            , test_http2_push_userContext1
            , test_http2_push_userContext2
            , test_http2_push_userContext3

            // cleanup
            , test_complete
            ];
var current_test = 0;

function run_next_test() {
  if (current_test < tests.length) {
    dump("starting test number " + current_test + "\n");
    tests[current_test]();
    current_test++;
    do_test_pending();
  }
}

var prefs;
var spdypref;
var spdypush;
var http2pref;
var altsvcpref1;
var altsvcpref2;
var loadGroup;
var serverPort;
var speculativeLimit;

function resetPrefs() {
  prefs.setIntPref("network.http.speculative-parallel-limit", speculativeLimit);
  prefs.setBoolPref("network.http.spdy.enabled", spdypref);
  prefs.setBoolPref("network.http.spdy.allow-push", spdypush);
  prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref);
  prefs.setBoolPref("network.http.altsvc.enabled", altsvcpref1);
  prefs.setBoolPref("network.http.altsvc.oe", altsvcpref2);
  prefs.clearUserPref("network.dns.localDomains");
  prefs.clearUserPref("network.cookieSettings.unblocked_for_testing");
}

function run_test() {
  var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
  serverPort = env.get("MOZHTTP2_PORT");
  Assert.notEqual(serverPort, null);
  dump("using port " + serverPort + "\n");

  // Set to allow the cert presented by our H2 server
  do_get_profile();
  prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
  speculativeLimit = prefs.getIntPref("network.http.speculative-parallel-limit");
  prefs.setIntPref("network.http.speculative-parallel-limit", 0);

  // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
  // so add that cert to the trust list as a signing cert. Some older tests in
  // this suite use localhost with a TOFU exception, but new ones should use
  // foo.example.com
  let certdb = Cc["@mozilla.org/security/x509certdb;1"]
                  .getService(Ci.nsIX509CertDB);
  addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");

  // Enable all versions of spdy to see that we auto negotiate http/2
  spdypref = prefs.getBoolPref("network.http.spdy.enabled");
  spdypush = prefs.getBoolPref("network.http.spdy.allow-push");
  http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2");
  altsvcpref1 = prefs.getBoolPref("network.http.altsvc.enabled");
  altsvcpref2 = prefs.getBoolPref("network.http.altsvc.oe", true);

  prefs.setBoolPref("network.http.spdy.enabled", true);
  prefs.setBoolPref("network.http.spdy.allow-push", true);
  prefs.setBoolPref("network.http.spdy.enabled.http2", true);
  prefs.setBoolPref("network.http.altsvc.enabled", true);
  prefs.setBoolPref("network.http.altsvc.oe", true);
  prefs.setCharPref("network.dns.localDomains", "foo.example.com, bar.example.com");
  prefs.setBoolPref("network.cookieSettings.unblocked_for_testing", true);

  loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance(Ci.nsILoadGroup);

  httpserv = new HttpServer();
  httpserv.registerPathHandler("/altsvc1", altsvcHttp1Server);
  httpserv.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
  httpserv.start(-1);
  httpserv.identity.setPrimary("http", "foo.example.com", httpserv.identity.primaryPort);

  httpserv2 = new HttpServer();
  httpserv2.registerPathHandler("/altsvc2", altsvcHttp1Server2);
  httpserv2.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK2);
  httpserv2.start(-1);
  httpserv2.identity.setPrimary("http", "foo.example.com", httpserv2.identity.primaryPort);

  // And make go!
  run_next_test();
}
