diff options
Diffstat (limited to 'netwerk/test/unit/test_http3.js')
-rw-r--r-- | netwerk/test/unit/test_http3.js | 571 |
1 files changed, 571 insertions, 0 deletions
diff --git a/netwerk/test/unit/test_http3.js b/netwerk/test/unit/test_http3.js new file mode 100644 index 0000000000..7f3f5b118d --- /dev/null +++ b/netwerk/test/unit/test_http3.js @@ -0,0 +1,571 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +// Generate a post with known pre-calculated md5 sum. +function generateContent(size) { + let content = ""; + for (let i = 0; i < size; i++) { + content += "0"; + } + return content; +} + +let post = generateContent(10); + +// Max concurent stream number in neqo is 100. +// Openning 120 streams will test queuing of streams. +let number_of_parallel_requests = 120; +let h1Server = null; +let h3Route; +let httpsOrigin; +let httpOrigin; +let h3AltSvc; + +let prefs; + +let tests = [ + // This test must be the first because it setsup alt-svc connection, that + // other tests use. + test_https_alt_svc, + test_multiple_requests, + test_request_cancelled_by_server, + test_stream_cancelled_by_necko, + test_multiple_request_one_is_cancelled, + test_multiple_request_one_is_cancelled_by_necko, + test_post, + test_patch, + test_http_alt_svc, + test_slow_receiver, + // This test should be at the end, because it will close http3 + // connection and the transaction will switch to already existing http2 + // connection. + // TODO: Bug 1582667 should try to fix issue with connection being closed. + test_version_fallback, + testsDone, +]; + +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + let h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Services.prefs; + + prefs.setBoolPref("network.http.http3.enable", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // We always resolve elements of localDomains as it's hardcoded without the + // following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + prefs.setBoolPref("network.http.altsvc.oe", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com:" + h2Port + "/"; + + h1Server = new HttpServer(); + h1Server.registerPathHandler("/http3-test", h1Response); + h1Server.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); + h1Server.registerPathHandler("/VersionFallback", h1Response); + h1Server.start(-1); + h1Server.identity.setPrimary( + "http", + "foo.example.com", + h1Server.identity.primaryPort + ); + httpOrigin = "http://foo.example.com:" + h1Server.identity.primaryPort + "/"; + + run_next_test(); +} + +function h1Response(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", 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); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + try { + let hval = "h3-29=" + metadata.getHeader("x-altsvc"); + response.setHeader("Alt-Svc", hval, false); + } catch (e) {} + + let body = "Q: What did 0 say to 8? A: Nice Belt!\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); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + let body = '["http://foo.example.com:' + h1Server.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let Http3CheckListener = function () {}; + +Http3CheckListener.prototype = { + onDataAvailableFired: false, + expectedStatus: Cr.NS_OK, + expectedRoute: "", + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, this.expectedStatus); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + if (Components.isSuccessCode(this.expectedStatus)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + Assert.equal(this.onDataAvailableFired, true); + Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3-29"); + } + run_next_test(); + do_test_finished(); + }, +}; + +let WaitForHttp3Listener = function () {}; + +WaitForHttp3Listener.prototype = new Http3CheckListener(); + +WaitForHttp3Listener.prototype.uri = ""; +WaitForHttp3Listener.prototype.h3AltSvc = ""; + +WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Assert.equal(status, this.expectedStatus); + + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + + if (routed == this.expectedRoute) { + Assert.equal(routed, this.expectedRoute); // always true, but a useful log + Assert.equal(httpVersion, "h3-29"); + run_next_test(); + } else { + dump("poll later for alt svc mapping\n"); + if (httpVersion == "h2") { + request.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.ok(request.supportsHTTP3); + } + do_test_pending(); + do_timeout(500, () => { + doTest(this.uri, this.expectedRoute, this.h3AltSvc); + }); + } + + do_test_finished(); +}; + +function doTest(uri, expectedRoute, altSvc) { + let chan = makeChan(uri); + let listener = new WaitForHttp3Listener(); + listener.uri = uri; + listener.expectedRoute = expectedRoute; + listener.h3AltSvc = altSvc; + chan.setRequestHeader("x-altsvc", altSvc, false); + chan.asyncOpen(listener); +} + +// Test Alt-Svc for http3. +// H2 server returns alt-svc=h3-29=:h3port +function test_https_alt_svc() { + dump("test_https_alt_svc()\n"); + do_test_pending(); + doTest(httpsOrigin + "http3-test", h3Route, h3AltSvc); +} + +// Listener for a number of parallel requests. if with_error is set, one of +// the channels will be cancelled (by the server or in onStartRequest). +let MultipleListener = function () {}; + +MultipleListener.prototype = { + number_of_parallel_requests: 0, + with_error: Cr.NS_OK, + count_of_done_requests: 0, + error_found_onstart: false, + error_found_onstop: false, + need_cancel_found: false, + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + let need_cancel = ""; + try { + need_cancel = request.getRequestHeader("CancelMe"); + } catch (e) {} + if (need_cancel != "") { + this.need_cancel_found = true; + request.cancel(Cr.NS_ERROR_ABORT); + } else if (Components.isSuccessCode(request.status)) { + Assert.equal(request.responseStatus, 200); + } else if (this.error_found_onstart) { + do_throw("We should have only one request failing."); + } else { + Assert.equal(request.status, this.with_error); + this.error_found_onstart = true; + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let routed = ""; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + Assert.equal(routed, this.expectedRoute); + + if (Components.isSuccessCode(request.status)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + } + + if (!Components.isSuccessCode(request.status)) { + if (this.error_found_onstop) { + do_throw("We should have only one request failing."); + } else { + Assert.equal(request.status, this.with_error); + this.error_found_onstop = true; + } + } + this.count_of_done_requests++; + if (this.count_of_done_requests == this.number_of_parallel_requests) { + if (Components.isSuccessCode(this.with_error)) { + Assert.equal(this.error_found_onstart, false); + Assert.equal(this.error_found_onstop, false); + } else { + Assert.ok(this.error_found_onstart || this.need_cancel_found); + Assert.equal(this.error_found_onstop, true); + } + run_next_test(); + } + do_test_finished(); + }, +}; + +// Multiple requests +function test_multiple_requests() { + dump("test_multiple_requests()\n"); + + let listener = new MultipleListener(); + listener.number_of_parallel_requests = number_of_parallel_requests; + listener.expectedRoute = h3Route; + + for (let i = 0; i < number_of_parallel_requests; i++) { + let chan = makeChan(httpsOrigin + "20000"); + chan.asyncOpen(listener); + do_test_pending(); + } +} + +// A request cancelled by a server. +function test_request_cancelled_by_server() { + dump("test_request_cancelled_by_server()\n"); + + let listener = new Http3CheckListener(); + listener.expectedStatus = Cr.NS_ERROR_NET_INTERRUPT; + listener.expectedRoute = h3Route; + let chan = makeChan(httpsOrigin + "RequestCancelled"); + chan.asyncOpen(listener); + do_test_pending(); +} + +let CancelRequestListener = function () {}; + +CancelRequestListener.prototype = new Http3CheckListener(); + +CancelRequestListener.prototype.expectedStatus = Cr.NS_ERROR_ABORT; + +CancelRequestListener.prototype.onStartRequest = function testOnStartRequest( + request +) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + Assert.equal(Components.isSuccessCode(request.status), true); + request.cancel(Cr.NS_ERROR_ABORT); +}; + +// Cancel stream after OnStartRequest. +function test_stream_cancelled_by_necko() { + dump("test_stream_cancelled_by_necko()\n"); + + let listener = new CancelRequestListener(); + listener.expectedRoute = h3Route; + let chan = makeChan(httpsOrigin + "20000"); + chan.asyncOpen(listener); + do_test_pending(); +} + +// Multiple requests, one gets cancelled by the server, the other should finish normally. +function test_multiple_request_one_is_cancelled() { + dump("test_multiple_request_one_is_cancelled()\n"); + + let listener = new MultipleListener(); + listener.number_of_parallel_requests = number_of_parallel_requests; + listener.with_error = Cr.NS_ERROR_NET_INTERRUPT; + listener.expectedRoute = h3Route; + + for (let i = 0; i < number_of_parallel_requests; i++) { + let uri = httpsOrigin + "20000"; + if (i == 4) { + // Add a request that will be cancelled by the server. + uri = httpsOrigin + "RequestCancelled"; + } + let chan = makeChan(uri); + chan.asyncOpen(listener); + do_test_pending(); + } +} + +// Multiple requests, one gets cancelled by us, the other should finish normally. +function test_multiple_request_one_is_cancelled_by_necko() { + dump("test_multiple_request_one_is_cancelled_by_necko()\n"); + + let listener = new MultipleListener(); + listener.number_of_parallel_requests = number_of_parallel_requests; + listener.with_error = Cr.NS_ERROR_ABORT; + listener.expectedRoute = h3Route; + for (let i = 0; i < number_of_parallel_requests; i++) { + let chan = makeChan(httpsOrigin + "20000"); + if (i == 4) { + // MultipleListener will cancel request with this header. + chan.setRequestHeader("CancelMe", "true", false); + } + chan.asyncOpen(listener); + do_test_pending(); + } +} + +let PostListener = function () {}; + +PostListener.prototype = new Http3CheckListener(); + +PostListener.prototype.onDataAvailable = function (request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); +}; + +// Support for doing a POST +function do_post(content, chan, listener, method) { + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = content; + + let uchan = chan.QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + + chan.requestMethod = method; + + chan.asyncOpen(listener); +} + +// Test a simple POST +function test_post() { + dump("test_post()"); + let chan = makeChan(httpsOrigin + "post"); + let listener = new PostListener(); + listener.expectedRoute = h3Route; + do_post(post, chan, listener, "POST"); + do_test_pending(); +} + +// Test a simple PATCH +function test_patch() { + dump("test_patch()"); + let chan = makeChan(httpsOrigin + "patch"); + let listener = new PostListener(); + listener.expectedRoute = h3Route; + do_post(post, chan, listener, "PATCH"); + do_test_pending(); +} + +// Test alt-svc for http (without s) +function test_http_alt_svc() { + dump("test_http_alt_svc()\n"); + + do_test_pending(); + doTest(httpOrigin + "http3-test", h3Route, h3AltSvc); +} + +let SlowReceiverListener = function () {}; + +SlowReceiverListener.prototype = new Http3CheckListener(); +SlowReceiverListener.prototype.count = 0; + +SlowReceiverListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.count += cnt; + read_stream(stream, cnt); +}; + +SlowReceiverListener.prototype.onStopRequest = function (request, status) { + Assert.equal(status, this.expectedStatus); + Assert.equal(this.count, 10000000); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + if (Components.isSuccessCode(this.expectedStatus)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + Assert.equal(this.onDataAvailableFired, true); + } + run_next_test(); + do_test_finished(); +}; + +function test_slow_receiver() { + dump("test_slow_receiver()\n"); + let chan = makeChan(httpsOrigin + "10000000"); + let listener = new SlowReceiverListener(); + listener.expectedRoute = h3Route; + chan.asyncOpen(listener); + do_test_pending(); + chan.suspend(); + do_timeout(1000, chan.resume); +} + +let CheckFallbackListener = function () {}; + +CheckFallbackListener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, "0"); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "http/1.1"); + run_next_test(); + do_test_finished(); + }, +}; + +// Server cancels request with VersionFallback. +function test_version_fallback() { + dump("test_version_fallback()\n"); + + let chan = makeChan(httpsOrigin + "VersionFallback"); + let listener = new CheckFallbackListener(); + chan.asyncOpen(listener); + do_test_pending(); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enable"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + prefs.clearUserPref("network.http.altsvc.oe"); + dump("testDone\n"); + do_test_pending(); + h1Server.stop(do_test_finished); +} |