tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit d037b11c9b5a45658b1c9337eebf823fa64b2d83
parent 0298cb464f679e722e81cc8a34ebe2fde44ff008
Author: Kershaw Chang <kershaw@mozilla.com>
Date:   Mon, 13 Oct 2025 15:35:10 +0000

Bug 1993438 - Modernize test_http3.js and extract tests into a separate file, r=necko-reviewers,valentin

Differential Revision: https://phabricator.services.mozilla.com/D268120

Diffstat:
Anetwerk/test/unit/http3_common.js | 719+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/test/unit/test_http3.js | 638++++++++++++-------------------------------------------------------------------
Mnetwerk/test/unit/xpcshell.toml | 2++
3 files changed, 814 insertions(+), 545 deletions(-)

diff --git a/netwerk/test/unit/http3_common.js b/netwerk/test/unit/http3_common.js @@ -0,0 +1,719 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_http3.js */ + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +// Promise-backed Http3CheckListener +class Http3CheckListener { + constructor( + { expectedStatus = Cr.NS_OK, expectedRoute = "" } = {}, + resolve, + reject + ) { + this.onDataAvailableFired = false; + this.expectedStatus = expectedStatus; + this.expectedRoute = expectedRoute; + this._resolve = resolve; + this._reject = reject; + } + + onStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + } + + onDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); + } + + onStopRequest(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"); + Assert.equal(this.onDataAvailableFired, true); + Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3"); + } + + this._resolve?.(request); + } +} + +class WaitForHttp3Listener extends Http3CheckListener { + constructor( + { + expectedStatus = Cr.NS_OK, + expectedRoute = "", + uri = "", + h3AltSvc = "", + retry, + delayMs = 500, + } = {}, + resolve, + reject + ) { + super({ expectedStatus, expectedRoute }, resolve, reject); + this.uri = uri; + this.h3AltSvc = h3AltSvc; + this._retry = retry; // function to re-open the request + this._delayMs = delayMs; // poll interval + } + + onStopRequest(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) { + // (This is where run_next_test() used to be.) + Assert.equal(routed, this.expectedRoute); // useful log/assert + Assert.equal(httpVersion, "h3"); + this._resolve?.(request); + return; + } + + // Not routed yet: mirror old behavior (log + supportsHTTP3 check + retry) + dump("poll later for alt-svc mapping\n"); + if (httpVersion === "h2") { + request.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.ok(request.supportsHTTP3); + } + + if (typeof this._retry === "function") { + // schedule another attempt (replaces do_test_pending/do_timeout recursion) + do_timeout(this._delayMs, () => + this._retry(this.uri, this.expectedRoute, this.h3AltSvc) + ); + } + // Promise remains pending until a later attempt matches expectedRoute. + } +} + +// Factory to create { listener, promise } +function createHttp3CheckListener(options = {}) { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const listener = new Http3CheckListener(options, resolve, reject); + return { listener, promise }; +} + +function createWaitForHttp3Listener(options = {}) { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const listener = new WaitForHttp3Listener(options, resolve, reject); + return { listener, promise }; +} + +// --- Async wrapper that does the polling by re-issuing the request --- +async function waitForHttp3Route( + uri, + expectedRoute, + altSvc, + { delayMs = 500 } = {} +) { + let listenerRef; + + // Function to (re)open the channel using the same listener instance. + const retry = () => { + const chan = makeChan(uri); + if (altSvc) { + chan.setRequestHeader("x-altsvc", altSvc, false); + } + chan.asyncOpen(listenerRef); + }; + + const { listener, promise } = createWaitForHttp3Listener({ + expectedStatus: Cr.NS_OK, + expectedRoute, + uri, + h3AltSvc: altSvc, + retry, + delayMs, + }); + listenerRef = listener; + + // Kick off first attempt; subsequent attempts are scheduled by the listener. + retry(); + + // Resolves only when routed === expectedRoute + return promise; +} + +// Promise-backed MultipleListener +class MultipleListener { + constructor( + { + number_of_parallel_requests = 0, + expectedRoute = "", + with_error = Cr.NS_OK, // NS_OK means we expect success for all + } = {}, + resolve, + reject + ) { + this.number_of_parallel_requests = number_of_parallel_requests; + this.expectedRoute = expectedRoute; + this.with_error = with_error; + + this.count_of_done_requests = 0; + this.error_found_onstart = false; + this.error_found_onstop = false; + this.need_cancel_found = false; + + this._resolve = resolve; + this._reject = reject; + } + + onStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + // Optional cancel behavior via header "CancelMe" + let need_cancel = ""; + try { + need_cancel = request.getRequestHeader("CancelMe"); + } catch (_) {} + if (need_cancel !== "") { + this.need_cancel_found = true; + request.cancel(Cr.NS_ERROR_ABORT); + return; + } + + // Original logic: either 200 OK for success, or exactly one failure + if (Components.isSuccessCode(request.status)) { + Assert.equal(request.responseStatus, 200); + } else if (this.error_found_onstart) { + // Fail fast: more than one failing request on start + this._reject?.( + new Error("We should have only one request failing (onStart).") + ); + } else { + Assert.equal(request.status, this.with_error); + this.error_found_onstart = true; + } + } + + onDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + } + + onStopRequest(request) { + // Check Alt-Used routing matches expectation + let routed = ""; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (_) {} + Assert.equal(routed, this.expectedRoute); + + // If success, ensure HTTP/3 + if (Components.isSuccessCode(request.status)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (_) {} + Assert.equal(httpVersion, "h3"); + } + + // Track/validate failures (at most one) + if (!Components.isSuccessCode(request.status)) { + if (this.error_found_onstop) { + this._reject?.( + new Error("We should have only one request failing (onStop).") + ); + return; + } + Assert.equal(request.status, this.with_error); + this.error_found_onstop = true; + } + + // Count completion and maybe resolve + this.count_of_done_requests++; + if (this.count_of_done_requests === this.number_of_parallel_requests) { + if (Components.isSuccessCode(this.with_error)) { + // All were expected to succeed + Assert.equal(this.error_found_onstart, false); + Assert.equal(this.error_found_onstop, false); + } else { + // One failure was expected OR a cancel path was exercised + Assert.ok(this.error_found_onstart || this.need_cancel_found); + Assert.equal(this.error_found_onstop, true); + } + this._resolve?.(); + } + } +} + +// Factory to create { listener, promise } +function createMultipleListener(options = {}) { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const listener = new MultipleListener(options, resolve, reject); + return { listener, promise }; +} + +async function do_test_multiple_requests( + number_of_parallel_requests, + h3Route, + httpsOrigin +) { + dump("test_multiple_requests()\n"); + + const { listener, promise } = createMultipleListener({ + number_of_parallel_requests, + expectedRoute: h3Route, + with_error: Cr.NS_OK, + }); + + for (let i = 0; i < number_of_parallel_requests; i++) { + const chan = makeChan(httpsOrigin + "20000"); + chan.asyncOpen(listener); + } + + await promise; +} + +async function do_test_request_cancelled_by_server(h3Route, httpsOrigin) { + dump("do_test_request_cancelled_by_server()\n"); + + const { listener, promise } = createHttp3CheckListener({ + expectedStatus: Cr.NS_ERROR_NET_INTERRUPT, + expectedRoute: h3Route, + }); + + const chan = makeChan(httpsOrigin + "RequestCancelled"); + chan.asyncOpen(listener); + + // Resolves at the point where run_next_test() used to be called + await promise; +} + +// Promise-backed Http3CheckListener must already exist: +// createHttp3CheckListener({ expectedStatus, expectedRoute }) + +class CancelRequestListener extends Http3CheckListener { + constructor({ expectedRoute = "" } = {}, resolve, reject) { + super( + { expectedStatus: Cr.NS_ERROR_ABORT, expectedRoute }, + resolve, + reject + ); + } + + onStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(Components.isSuccessCode(request.status), true); + // Cancel the request immediately (simulate Necko cancelling) + request.cancel(Cr.NS_ERROR_ABORT); + } +} + +function createCancelRequestListener(options = {}) { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const listener = new CancelRequestListener(options, resolve, reject); + return { listener, promise }; +} + +// Cancel stream after OnStartRequest. +async function do_test_stream_cancelled_by_necko(h3Route, httpsOrigin) { + dump("do_test_stream_cancelled_by_necko()\n"); + + const { listener, promise } = createCancelRequestListener({ + expectedRoute: h3Route, + }); + + const chan = makeChan(httpsOrigin + "20000"); + chan.asyncOpen(listener); + + // Resolves at the end of onStopRequest (where run_next_test() used to be) + await promise; +} + +async function do_test_multiple_request_one_is_cancelled( + number_of_parallel_requests, + h3Route, + httpsOrigin +) { + dump("do_test_multiple_request_one_is_cancelled()\n"); + + const { listener, promise } = createMultipleListener({ + number_of_parallel_requests, + expectedRoute: h3Route, + with_error: Cr.NS_ERROR_NET_INTERRUPT, // one request is expected to fail (server-cancelled) + }); + + 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"; + } + const chan = makeChan(uri); + chan.asyncOpen(listener); + } + + // Resolves when all parallel requests complete and invariants are checked + await promise; +} + +async function do_test_multiple_request_one_is_cancelled_by_necko( + number_of_parallel_requests, + h3Route, + httpsOrigin +) { + dump("do_test_multiple_request_one_is_cancelled_by_necko()\n"); + + const { listener, promise } = createMultipleListener({ + number_of_parallel_requests, + expectedRoute: h3Route, + with_error: Cr.NS_ERROR_ABORT, + }); + + 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); + } + + // Resolves when all parallel requests complete and invariants are checked + await promise; +} + +// Promise-backed Http3CheckListener assumed available: +// function createHttp3CheckListener({ expectedStatus, expectedRoute }) + +class PostListener extends Http3CheckListener { + constructor(opts = {}, resolve, reject) { + super(opts, resolve, reject); + } + onDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); + } +} + +// Factory for PostListener +function createPostListener(options = {}) { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const listener = new PostListener(options, resolve, reject); + return { listener, promise }; +} + +// Helper to perform a POST (or any method with a body) +function openWithBody( + content, + chan, + method = "POST", + contentType = "text/plain" +) { + const stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setByteStringData(content); + + const uchan = chan.QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, contentType, stream.available()); + chan.requestMethod = method; + return chan; +} + +// 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); + +// Test a simple POST (async) +async function do_test_post(httpsOrigin, h3Route) { + dump("do_test_post()\n"); + + const chan = makeChan(httpsOrigin + "post"); + openWithBody(post, chan, "POST"); + + const { listener, promise } = createPostListener({ + expectedStatus: Cr.NS_OK, + expectedRoute: h3Route, + }); + + chan.asyncOpen(listener); + await promise; // resolves at end of onStopRequest in Http3CheckListener +} + +// Test a simple PATCH +async function do_test_patch(httpsOrigin, h3Route) { + dump("do_test_post()\n"); + + const chan = makeChan(httpsOrigin + "patch"); + openWithBody(post, chan, "PATCH"); + + const { listener, promise } = createPostListener({ + expectedStatus: Cr.NS_OK, + expectedRoute: h3Route, + }); + + chan.asyncOpen(listener); + await promise; +} + +let h1Server = null; +let altsvcHost = ""; +let httpOrigin = ""; + +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=" + 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://${altsvcHost}:${h1Server.identity.primaryPort}"]`; + response.bodyOutputStream.write(body, body.length); +} + +function setup_h1_server(host) { + altsvcHost = host; + 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", + altsvcHost, + h1Server.identity.primaryPort + ); + httpOrigin = `http://${altsvcHost}:${h1Server.identity.primaryPort}/`; + registerCleanupFunction(() => { + h1Server.stop(); + }); +} + +// Promise-backed base assumed available: +// class Http3CheckListener { ... } +// function createHttp3CheckListener(opts) { return { listener, promise }; } + +class SlowReceiverListener extends Http3CheckListener { + constructor( + { + expectedStatus = Cr.NS_OK, + expectedRoute = "", + expectedBytes = 10_000_000, + } = {}, + resolve, + reject + ) { + super({ expectedStatus, expectedRoute }, resolve, reject); + this.count = 0; + this.expectedBytes = expectedBytes; + } + + onDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.count += cnt; + read_stream(stream, cnt); + } + + onStopRequest(request, status) { + Assert.equal(status, this.expectedStatus); + Assert.equal(this.count, this.expectedBytes); + + 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"); + Assert.equal(this.onDataAvailableFired, true); + } + + // Resolve where run_next_test() used to be + this._resolve?.(request); + } +} + +function createSlowReceiverListener(options = {}) { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const listener = new SlowReceiverListener(options, resolve, reject); + return { listener, promise }; +} + +// Test: slow receiver (suspend, then resume) +async function do_test_slow_receiver(httpsOrigin, h3Route) { + dump("do_test_slow_receiver()\n"); + + const chan = makeChan(httpsOrigin + "10000000"); + + const { listener, promise } = createSlowReceiverListener({ + expectedStatus: Cr.NS_OK, + expectedRoute: h3Route, + expectedBytes: 10_000_000, + }); + + chan.asyncOpen(listener); + + // Suspend immediately, then resume after 1s (replaces do_test_pending/do_timeout) + chan.suspend(); + await new Promise(r => do_timeout(1000, r)); + chan.resume(); + + // Wait for completion (used to be run_next_test/do_test_finished) + await promise; +} + +// Promise-backed listener for version fallback checks +class CheckFallbackListener { + constructor(resolve, reject) { + this._resolve = resolve; + this._reject = reject; + } + + onStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + } + + onDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + } + + onStopRequest(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"); + + // Resolve where run_next_test() used to be called + this._resolve?.(request); + } +} + +// Factory to create { listener, promise } +function createCheckFallbackListener() { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const listener = new CheckFallbackListener(resolve, reject); + return { listener, promise }; +} + +// Server cancels request with VersionFallback. +async function do_test_version_fallback(httpsOrigin) { + dump("do_test_version_fallback()\n"); + + const chan = makeChan(httpsOrigin + "VersionFallback"); + const { listener, promise } = createCheckFallbackListener(); + + chan.asyncOpen(listener); + + await promise; +} diff --git a/netwerk/test/unit/test_http3.js b/netwerk/test/unit/test_http3.js @@ -1,64 +1,18 @@ "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); +/* import-globals-from http3_common.js */ // 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 h3Port; - +let h3ServerDomain; 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() { +add_setup(async function () { let h2Port = Services.env.get("MOZHTTP2_PORT"); Assert.notEqual(h2Port, null); Assert.notEqual(h2Port, ""); @@ -66,185 +20,21 @@ function run_test() { Assert.notEqual(h3Port, null); Assert.notEqual(h3Port, ""); h3AltSvc = ":" + h3Port; - - h3Route = "foo.example.com:" + h3Port; + h3ServerDomain = "foo.example.com"; + h3Route = `${h3ServerDomain}:${h3Port}`; do_get_profile(); prefs = Services.prefs; prefs.setBoolPref("network.http.http3.enable", true); - prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, alt1.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=" + 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"); - Assert.equal(this.onDataAvailableFired, true); - Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3"); - } - 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"); - 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=:h3port -function test_https_alt_svc() { - dump("test_https_alt_svc()\n"); - do_test_pending(); if (mozinfo.os == "android") { // Set necessary prefs to make Firefox connect to the http3Server on the // host machine. @@ -252,341 +42,99 @@ function test_https_alt_svc() { const overrideService = Cc[ "@mozilla.org/network/native-dns-override;1" ].getService(Ci.nsINativeDNSResolverOverride); - overrideService.addIPOverride("foo.example.com", "10.0.2.2"); + overrideService.addIPOverride(h3ServerDomain, "10.0.2.2"); prefs.setCharPref( "network.http.http3.alt-svc-mapping-for-testing", - `foo.example.com;h3=:${h3Port}` + `${h3ServerDomain};h3=:${h3Port}` ); } - 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) { - 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"); - } - - 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 + // 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 ); - stream.setByteStringData(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"); - // Skip this test on Android because the httpOrigin (http://foo.example.com) - // is on 127.0.0.1, while the http3Server (https://foo.example.com) is - // on 10.0.2.2. Currently, we can't change the IP mapping dynamically. - if (mozinfo.os == "android") { - current_test++; - run_next_test(); - return; - } - 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"); - 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); - }, + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = `https://${h3ServerDomain}:${h2Port}/`; + + registerCleanupFunction(() => { + prefs.clearUserPref("network.http.http3.enable"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + prefs.clearUserPref("network.http.altsvc.oe"); + }); +}); + +add_task(async function test_https_alt_svc() { + await waitForHttp3Route(httpsOrigin + "http3-test", h3Route, h3AltSvc, { + delayMs: 500, + }); +}); + +add_task(async function test_multiple_requests() { + await do_test_multiple_requests( + number_of_parallel_requests, + h3Route, + httpsOrigin + ); +}); - onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { - read_stream(stream, cnt); - }, +add_task(async function test_request_cancelled_by_server() { + await do_test_request_cancelled_by_server(h3Route, httpsOrigin); +}); - 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"); +add_task(async function test_stream_cancelled_by_necko() { + await do_test_stream_cancelled_by_necko(h3Route, httpsOrigin); +}); - Assert.equal(routed, "0"); +add_task(async function test_multiple_request_one_is_cancelled() { + await do_test_multiple_request_one_is_cancelled( + number_of_parallel_requests, + h3Route, + httpsOrigin + ); +}); - let httpVersion = ""; - try { - httpVersion = request.protocolVersion; - } catch (e) {} - Assert.equal(httpVersion, "http/1.1"); - run_next_test(); - do_test_finished(); +add_task(async function test_multiple_request_one_is_cancelled_by_necko() { + await do_test_multiple_request_one_is_cancelled_by_necko( + number_of_parallel_requests, + h3Route, + httpsOrigin + ); +}); + +add_task(async function test_post() { + await do_test_post(httpsOrigin, h3Route); +}); + +add_task(async function test_patch() { + await do_test_patch(httpsOrigin, h3Route); +}); + +add_task( + { + // Skip this test on Android because the httpOrigin (http://foo.example.com) + // is on 127.0.0.1, while the http3Server (https://foo.example.com) is + // on 10.0.2.2. Currently, we can't change the IP mapping dynamically. + skip_if: () => mozinfo.os == "android", }, -}; - -// 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(); -} + async function test_http_alt_svc() { + setup_h1_server(h3ServerDomain); + await waitForHttp3Route(httpOrigin + "http3-test", h3Route, h3AltSvc, { + delayMs: 500, + }); + } +); -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); -} +add_task(async function test_slow_receiver() { + await do_test_slow_receiver(httpsOrigin, h3Route); +}); + +// 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. +add_task(async function test_version_fallback() { + await do_test_version_fallback(httpsOrigin); +}); diff --git a/netwerk/test/unit/xpcshell.toml b/netwerk/test/unit/xpcshell.toml @@ -27,6 +27,7 @@ support-files = [ "test_http3_prio_helpers.js", "http2_test_common.js", "http3_proxy_common.js", + "http3_common.js", ] # dom.serviceWorkers.enabled is currently set to false in StaticPrefList.yaml @@ -709,6 +710,7 @@ run-sequentially = ["true"] # node server exceptions dont replay well head = "head_channels.js head_cache.js head_cookies.js head_trr.js head_http3.js http2_test_common.js" ["test_http3.js"] +head = "head_cookies.js head_channels.js head_cache.js head_http3.js http3_common.js" run-sequentially = ["true"] # http3server skip-if = [ "os == 'android' && os_version == '14' && processor == 'x86_64'", # Bug 1982955