// 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) {
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) {
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) {
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")
);
};
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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(
`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 = makeHTTPChannel(`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 () {
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) {
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] = makeHTTPChannel(
`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] = makeHTTPChannel(
`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.setByteStringData(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) {
let chan1 = makeHTTPChannel(`https:
//localhost:${serverPort}/multiplex1`);
let chan2 = makeHTTPChannel(`https:
//localhost:${serverPort}/multiplex2`);
let listener1 =
new Http2MultiplexListener();
let listener2 =
new Http2MultiplexListener();
let promises = [];
let p1 =
new Promise(resolve => {
listener1.finish = resolve;
});
promises.push(p1);
let 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) {
let chan = makeHTTPChannel(`https:
//localhost:${serverPort}/header`);
let hvalue =
"Headers are fun";
chan.setRequestHeader(
"X-Test-Header", hvalue,
false);
return new Promise(resolve => {
let 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, invalid_kind) {
return new Promise(resolve => {
var listener =
new Http2CheckListener();
listener.finish = resolve;
listener.shouldSucceed =
false;
var chan = makeHTTPChannel(
`https:
//localhost:${serverPort}/invalid_response_header/${invalid_kind}`
);
chan.asyncOpen(listener);
});
}
// Test to make sure cookies are split into separate fields before compression
async
function test_http2_cookie_crumbling(serverPort) {
var chan = makeHTTPChannel(
`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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.setByteStringData(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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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) {
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 = makeHTTPChannel(
`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 = makeHTTPChannel(
`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) {
var isHttp2Connection = checkIsHttp2(
request.QueryInterface(Ci.nsIHttpChannel)
);
if (!isHttp2Connection) {
dump(
"/altsvc2 not over h2 yet - retry\n");
var chan = makeHTTPChannel(
`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 = makeHTTPChannel(
`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() {},
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) {
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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(
`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) {
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 = makeHTTPChannel(
`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 = makeHTTPChannel(`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 = makeHTTPChannel(
`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);
});
}
async
function test_http2_continuations_over_max_response_limit(
loadGroup,
serverPort
) {
var chan = makeHTTPChannel(
`https:
//localhost:${serverPort}/hugecontinuedheaders?size=385`
);
chan.loadGroup = loadGroup;
return new Promise(resolve => {
var listener =
new Http2CheckListener();
listener.finish = resolve;
listener.shouldSucceed =
false;
chan.asyncOpen(listener);
});
}
function Http2IllegalHpackValidationListener() {}
Http2IllegalHpackValidationListener.prototype =
new Http2CheckListener();
Http2IllegalHpackValidationListener.prototype.shouldGoAway =
false;
Http2IllegalHpackValidationListener.prototype.onStopRequest =
function (
request
) {
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 () {
var chan = makeHTTPChannel(
`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 = makeHTTPChannel(
`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 = makeHTTPChannel(
`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(
`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 = makeHTTPChannel(`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 = makeHTTPChannel(`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 = makeHTTPChannel(
`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 = makeHTTPChannel(`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);
});
}