diff options
Diffstat (limited to 'netwerk/test/unit/test_http2.js')
-rw-r--r-- | netwerk/test/unit/test_http2.js | 1119 |
1 files changed, 1119 insertions, 0 deletions
diff --git a/netwerk/test/unit/test_http2.js b/netwerk/test/unit/test_http2.js new file mode 100644 index 000000000..3fefe707e --- /dev/null +++ b/netwerk/test/unit/test_http2.js @@ -0,0 +1,1119 @@ +// test HTTP/2 + +var Ci = Components.interfaces; +var Cc = Components.classes; + +// 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, ctx) { + 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!"); + } + + do_check_true(request instanceof Components.interfaces.nsIHttpChannel); + do_check_eq(request.requestSucceeded, this.shouldSucceed); + if (this.shouldSucceed) { + do_check_eq(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.accum += cnt; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, ctx, status) { + do_check_true(this.onStartRequestFired); + if (this.expected != -1) { + do_check_eq(this.accum, this.expected); + } + if (this.shouldSucceed) { + do_check_true(Components.isSuccessCode(status)); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection == this.shouldBeHttp2); + } else { + do_check_false(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) { + do_check_neq(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, ctx, 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, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection); + do_check_true(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, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + var hvalue = request.getResponseHeader(this.name); + do_check_neq(hvalue, ""); + this.callback(hvalue); + read_stream(stream, cnt); +}; + +var Http2PushListener = function() {}; + +Http2PushListener.prototype = new Http2CheckListener(); + +Http2PushListener.prototype.onDataAvailable = function(request, ctx, 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") { + do_check_eq(request.getResponseHeader("pushed"), "yes"); + } + 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++) { + do_check_eq(getHeader(headerPrefix + 1), headerText); + } +} + +var Http2ContinuedHeaderListener = function() {}; + +Http2ContinuedHeaderListener.prototype = new Http2CheckListener(); + +Http2ContinuedHeaderListener.prototype.onStopsLeft = 2; + +Http2ContinuedHeaderListener.prototype.QueryInterface = function (aIID) { + if (aIID.equals(Ci.nsIHttpPushListener) || + aIID.equals(Ci.nsIStreamListener)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; +}; + +Http2ContinuedHeaderListener.prototype.getInterface = function(aIID) { + return this.QueryInterface(aIID); +}; + +Http2ContinuedHeaderListener.prototype.onDataAvailable = function (request, ctx, 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, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(Components.isSuccessCode(status)); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection); + + --this.onStopsLeft; + if (this.onStopsLeft === 0) { + run_next_test(); + do_test_finished(); + } +}; + +Http2ContinuedHeaderListener.prototype.onPush = function(associatedChannel, pushChannel) { + do_check_eq(associatedChannel.originalURI.spec, "https://localhost:" + serverPort + "/continuedheaders"); + do_check_eq(pushChannel.getRequestHeader("x-pushed-request"), "true"); + checkContinuedHeaders(pushChannel.getRequestHeader, "X-Push-Test-Header-", pushHdrTxt); + + pushChannel.asyncOpen2(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, ctx, 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 + do_check_eq(bigListenerMD5, request.getResponseHeader("X-Expected-MD5")); +}; + +Http2BigListener.prototype.onStopRequest = function(request, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection); + + // Don't want to flood output, so don't use do_check_eq + do_check_true(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, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.count += cnt; + read_stream(stream, cnt); +}; + +Http2HugeSuspendedListener.prototype.onStopRequest = function(request, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection); + do_check_eq(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, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + read_stream(stream, cnt); + do_check_eq(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, ctx) { + this.onStartRequestFired = true; + if (!Components.isSuccessCode(request.status)) + do_throw("Channel should have a success code! (" + request.status + ")"); + + do_check_true(request instanceof Components.interfaces.nsIHttpChannel); + do_check_eq(request.responseStatus, 200); + do_check_eq(request.requestSucceeded, true); + }, + + onDataAvailable: function testOnDataAvailable(request, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(Components.isSuccessCode(status)); + do_check_true(this.onDataAvailableFired); + do_check_true(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(listener); +} + +// Support for making sure XHR works over SPDY +function checkXhr(xhr) { + if (xhr.readyState != 4) { + return; + } + + do_check_eq(xhr.status, 200); + do_check_eq(checkIsHttp2(xhr), true); + run_next_test(); + do_test_finished(); +} + +// Fires off an XHR request over h2 +function test_http2_xhr() { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + req.open("GET", "https://localhost:" + serverPort + "/", true); + req.addEventListener("readystatechange", function (evt) { checkXhr(req); }, + false); + 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, ctx, status) { + this.count++; + do_check_true(this.isHttp2Connection); + if (this.recvdHdr > 0) { + do_check_eq(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].asyncOpen2(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].asyncOpen2(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.asyncOpen2(listener1); + chan2.asyncOpen2(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) { + do_check_eq(received_hvalue, hvalue); + }); + chan.asyncOpen2(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(); + do_check_eq(cookiesReceived.length, cookiesSent.length); + cookiesReceived.forEach(function(cookieReceived, index) { + do_check_eq(cookiesSent[index], cookieReceived) + }); + }); + chan.asyncOpen2(listener); +} + +function test_http2_push1() { + var chan = makeChan("https://localhost:" + serverPort + "/push"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push2() { + var chan = makeChan("https://localhost:" + serverPort + "/push.js"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push3() { + var chan = makeChan("https://localhost:" + serverPort + "/push2"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push4() { + var chan = makeChan("https://localhost:" + serverPort + "/push2.js"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push5() { + var chan = makeChan("https://localhost:" + serverPort + "/push5"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push6() { + var chan = makeChan("https://localhost:" + serverPort + "/push5.js"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(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.asyncOpen2(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.asyncOpen2(listener); +} + +function test_http2_huge_suspended() { + var chan = makeChan("https://localhost:" + serverPort + "/huge"); + var listener = new Http2HugeSuspendedListener(); + chan.asyncOpen2(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.asyncOpen2(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"); +} + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var httpserv = null; +var httpserv2 = null; +var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + +var altsvcClientListener = { + onStartRequest: function test_onStartR(request, ctx) { + do_check_eq(request.status, Components.results.NS_OK); + }, + + onDataAvailable: function test_ODA(request, cx, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + var isHttp2Connection = checkIsHttp2(request.QueryInterface(Components.interfaces.nsIHttpChannel)); + if (!isHttp2Connection) { + dump("/altsvc1 not over h2 yet - retry\n"); + var chan = makeChan("http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1") + .QueryInterface(Components.interfaces.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.asyncOpen2(altsvcClientListener); + } else { + do_check_true(isHttp2Connection); + var chan = makeChan("http://foo.example.com:" + httpserv2.identity.primaryPort + "/altsvc2") + .QueryInterface(Components.interfaces.nsIHttpChannel); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen2(altsvcClientListener2); + } + } +}; + +var altsvcClientListener2 = { + onStartRequest: function test_onStartR(request, ctx) { + do_check_eq(request.status, Components.results.NS_OK); + }, + + onDataAvailable: function test_ODA(request, cx, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + var isHttp2Connection = checkIsHttp2(request.QueryInterface(Components.interfaces.nsIHttpChannel)); + if (!isHttp2Connection) { + dump("/altsvc2 not over h2 yet - retry\n"); + var chan = makeChan("http://foo.example.com:" + httpserv2.identity.primaryPort + "/altsvc2") + .QueryInterface(Components.interfaces.nsIHttpChannel); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen2(altsvcClientListener2); + } else { + do_check_true(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 + '": { "tls-ports": [' + serverPort + '] }}'; + 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 + '": { "tls-ports": [' + serverPort + '] }}'; + response.bodyOutputStream.write(body, body.length); +} +function test_http2_altsvc() { + var chan = makeChan("http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1") + .QueryInterface(Components.interfaces.nsIHttpChannel); + chan.asyncOpen2(altsvcClientListener); +} + +var Http2PushApiListener = function() {}; + +Http2PushApiListener.prototype = { + checksPending: 9, // 4 onDataAvailable and 5 onStop + + getInterface: function(aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIHttpPushListener) || + aIID.equals(Ci.nsIStreamListener)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + // nsIHttpPushListener + onPush: function onPush(associatedChannel, pushChannel) { + do_check_eq(associatedChannel.originalURI.spec, "https://localhost:" + serverPort + "/pushapi1"); + do_check_eq (pushChannel.getRequestHeader("x-pushed-request"), "true"); + + pushChannel.asyncOpen2(this); + if (pushChannel.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/2") { + pushChannel.cancel(Components.results.NS_ERROR_ABORT); + } + }, + + // normal Channel listeners + onStartRequest: function pushAPIOnStart(request, ctx) { + }, + + onDataAvailable: function pushAPIOnDataAvailable(request, ctx, stream, offset, cnt) { + do_check_neq(request.originalURI.spec, "https://localhost:" + serverPort + "/pushapi1/2"); + + var data = read_stream(stream, cnt); + + if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1") { + do_check_eq(data[0], '0'); + --this.checksPending; + } else if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/1") { + do_check_eq(data[0], '1'); + --this.checksPending; // twice + } else if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/3") { + do_check_eq(data[0], '3'); + --this.checksPending; + } else { + do_check_eq(true, false); + } + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/2") { + do_check_eq(request.status, Components.results.NS_ERROR_ABORT); + } else { + do_check_eq(request.status, Components.results.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 + +function test_http2_pushapi_1() { + var chan = makeChan("https://localhost:" + serverPort + "/pushapi1"); + chan.loadGroup = loadGroup; + var listener = new Http2PushApiListener(); + chan.notificationCallbacks = listener; + chan.asyncOpen2(listener); +} + +var WrongSuiteListener = function() {}; +WrongSuiteListener.prototype = new Http2CheckListener(); +WrongSuiteListener.prototype.shouldBeHttp2 = false; +WrongSuiteListener.prototype.onStopRequest = function(request, ctx, status) { + prefs.setBoolPref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", true); + Http2CheckListener.prototype.onStopRequest.call(this); +}; + +// test that we use h1 without the mandatory cipher suite available +function test_http2_wrongsuite() { + 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(); + chan.asyncOpen2(listener); +} + +function test_http2_h11required_stream() { + var chan = makeChan("https://localhost:" + serverPort + "/h11required_stream"); + var listener = new Http2CheckListener(); + listener.shouldBeHttp2 = false; + chan.asyncOpen2(listener); +} + +function H11RequiredSessionListener () { } +H11RequiredSessionListener.prototype = new Http2CheckListener(); + +H11RequiredSessionListener.prototype.onStopRequest = function (request, ctx, status) { + var streamReused = request.getResponseHeader("X-H11Required-Stream-Ok"); + do_check_eq(streamReused, "yes"); + + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(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.asyncOpen2(listener); +} + +function test_http2_retry_rst() { + var chan = makeChan("https://localhost:" + serverPort + "/rstonce"); + var listener = new Http2CheckListener(); + chan.asyncOpen2(listener); +} + +function test_http2_continuations() { + var chan = makeChan("https://localhost:" + serverPort + "/continuedheaders"); + chan.loadGroup = loadGroup; + var listener = new Http2ContinuedHeaderListener(); + chan.notificationCallbacks = listener; + chan.asyncOpen2(listener); +} + +function Http2IllegalHpackValidationListener() { } +Http2IllegalHpackValidationListener.prototype = new Http2CheckListener(); +Http2IllegalHpackValidationListener.prototype.shouldGoAway = false; + +Http2IllegalHpackValidationListener.prototype.onStopRequest = function (request, ctx, status) { + var wentAway = (request.getResponseHeader('X-Did-Goaway') === 'yes'); + do_check_eq(wentAway, this.shouldGoAway); + + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(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, ctx, status) { + var chan = makeChan("https://localhost:" + serverPort + "/illegalhpack_validate"); + var listener = new Http2IllegalHpackValidationListener(); + listener.shouldGoAway = this.shouldGoAway; + chan.asyncOpen2(listener); +}; + +function test_http2_illegalhpacksoft() { + var chan = makeChan("https://localhost:" + serverPort + "/illegalhpacksoft"); + var listener = new Http2IllegalHpackListener(); + listener.shouldGoAway = false; + listener.shouldSucceed = false; + chan.asyncOpen2(listener); +} + +function test_http2_illegalhpackhard() { + var chan = makeChan("https://localhost:" + serverPort + "/illegalhpackhard"); + var listener = new Http2IllegalHpackListener(); + listener.shouldGoAway = true; + listener.shouldSucceed = false; + chan.asyncOpen2(listener); +} + +function test_http2_folded_header() { + var chan = makeChan("https://localhost:" + serverPort + "/foldedheader"); + chan.loadGroup = loadGroup; + var listener = new Http2CheckListener(); + listener.shouldSucceed = false; + chan.asyncOpen2(listener); +} + +function test_http2_empty_data() { + var chan = makeChan("https://localhost:" + serverPort + "/emptydata"); + var listener = new Http2CheckListener(); + chan.asyncOpen2(listener); +} + +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); +} + +// 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 + // 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 + + // 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(); + } +} + +// Support for making sure we can talk to the invalid cert the server presents +var CertOverrideListener = function(host, port, bits) { + this.host = host; + if (port) { + this.port = port; + } + this.bits = bits; +}; + +CertOverrideListener.prototype = { + host: null, + port: -1, + bits: null, + + getInterface: function(aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIBadCertListener2) || + aIID.equals(Ci.nsIInterfaceRequestor) || + aIID.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + notifyCertProblem: function(socketInfo, sslStatus, targetHost) { + var cert = sslStatus.QueryInterface(Ci.nsISSLStatus).serverCert; + var cos = Cc["@mozilla.org/security/certoverride;1"]. + getService(Ci.nsICertOverrideService); + cos.rememberValidityOverride(this.host, this.port, cert, this.bits, false); + dump("Certificate Override in place\n"); + return true; + }, +}; + +function addCertOverride(host, port, bits) { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + try { + var url; + if (port) { + url = "https://" + host + ":" + port + "/"; + } else { + url = "https://" + host + "/"; + } + req.open("GET", url, false); + req.channel.notificationCallbacks = new CertOverrideListener(host, port, bits); + req.send(null); + } catch (e) { + // This will fail since the server is not trusted yet + } +} + +var prefs; +var spdypref; +var spdypush; +var http2pref; +var tlspref; +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.spdy.enforce-tls-profile", tlspref); + prefs.setBoolPref("network.http.altsvc.enabled", altsvcpref1); + prefs.setBoolPref("network.http.altsvc.oe", altsvcpref2); + prefs.clearUserPref("network.dns.localDomains"); +} + +function run_test() { + var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment); + serverPort = env.get("MOZHTTP2_PORT"); + do_check_neq(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 CA.cert.der + // 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, "CA.cert.der", "CTu,u,u"); + + addCertOverride("localhost", serverPort, + Ci.nsICertOverrideService.ERROR_UNTRUSTED | + Ci.nsICertOverrideService.ERROR_MISMATCH | + Ci.nsICertOverrideService.ERROR_TIME); + + // 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"); + tlspref = prefs.getBoolPref("network.http.spdy.enforce-tls-profile"); + 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.enabled.v3-1", true); + prefs.setBoolPref("network.http.spdy.allow-push", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false); + 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"); + + 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(); +} + +function readFile(file) { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + fstream.init(file, -1, 0, 0); + let data = NetUtil.readInputStreamToString(fstream, fstream.available()); + fstream.close(); + return data; +} + +function addCertFromFile(certdb, filename, trustString) { + let certFile = do_get_file(filename, false); + let der = readFile(certFile); + certdb.addCert(der, trustString, null); +} |