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