/* 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"; const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); var { setTimeout } = ChromeUtils.importESModule( "resource://gre/modules/Timer.sys.mjs" ); const { NodeHTTPSServer } = ChromeUtils.importESModule( "resource://testing-common/NodeServer.sys.mjs" ); const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( Ci.nsINativeDNSResolverOverride ); const mockController = Cc[ "@mozilla.org/network/mock-network-controller;1" ].getService(Ci.nsIMockNetworkLayerController); let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); add_setup(function () { Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true); Services.prefs.setBoolPref("network.socket.attach_mock_network_layer", true); registerCleanupFunction(() => { Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled"); Services.prefs.clearUserPref("network.socket.attach_mock_network_layer"); Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); override.clearOverrides(); mockController.clearBlockedTCPConnect(); mockController.clearPausedTCPConnect(); }); }); async function openChan(uri) { let chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true, }).QueryInterface(Ci.nsIHttpChannel); chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; let result = await new Promise(resolve => { chan.asyncOpen( new ChannelListener( (r, b) => resolve({ req: r, buffer: b }), null, CL_ALLOW_UNKNOWN_CL ) ); }); return { addr: result.req.QueryInterface(Ci.nsIHttpChannelInternal).remoteAddress, status: result.req.QueryInterface(Ci.nsIHttpChannel).responseStatus, buffer: result.buffer, }; } async function resetConnections() { Services.obs.notifyObservers(null, "net:cancel-all-connections"); let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent); await nssComponent.asyncClearSSLExternalAndInternalSessionCache(); override.clearOverrides(); mockController.clearBlockedTCPConnect(); mockController.clearPausedTCPConnect(); Services.dns.clearCache(true); // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(resolve => setTimeout(resolve, 1000)); } async function do_test_first_attempt_succeeds(host) { await resetConnections(); let server = new NodeHTTPSServer(); await server.start(); await server.registerPathHandler("/test", (_req, resp) => { resp.writeHead(200, { "Content-Type": "text/plain" }); resp.end("ok"); }); let port = server.port(); // Set up DNS to return both IPv6 and IPv4. IPv6 is preferred and should // succeed on the first attempt. override.addIPOverride(host, "::1"); override.addIPOverride(host, "127.0.0.1"); let { status, buffer } = await openChan(`https://${host}:${port}/test`); Assert.equal(status, 200, "Request should succeed"); Assert.equal(buffer, "ok", "Response body should match"); await server.stop(); } // Test first connection attempt succeeds, speculative disabled. add_task(async function test_first_attempt_succeeds_no_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); await do_test_first_attempt_succeeds("foo.example.com"); }); // Test first connection attempt succeeds, speculative enabled. add_task(async function test_first_attempt_succeeds_with_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); await do_test_first_attempt_succeeds("foo.example.com"); }); async function do_test_first_attempt_fails(host) { await resetConnections(); let server = new NodeHTTPSServer(); await server.start(); await server.registerPathHandler("/test", (_req, resp) => { resp.writeHead(200, { "Content-Type": "text/plain" }); resp.end("ok"); }); let port = server.port(); // Set up DNS to return both IPv6 and IPv4. override.addIPOverride(host, "::1"); override.addIPOverride(host, "127.0.0.1"); // Block TCP connect on the IPv6 address to force connection refused. let blockedAddr = mockController.createScriptableNetAddr("::1", port); mockController.blockTCPConnect(blockedAddr); let { addr, status, buffer } = await openChan(`https://${host}:${port}/test`); Assert.equal(status, 200, "Request should succeed"); Assert.equal(buffer, "ok", "Response body should match"); Assert.equal( addr, "127.0.0.1", "Should fall back to IPv4 after IPv6 failure" ); await server.stop(); } // Test fallback with speculative connections disabled. add_task(async function test_first_attempt_fails_no_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); await do_test_first_attempt_fails("alt1.example.com"); }); // Test fallback with speculative connections enabled. add_task(async function test_first_attempt_fails_with_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); await do_test_first_attempt_fails("alt1.example.com"); }); async function do_test_first_attempt_slow(host) { await resetConnections(); let server = new NodeHTTPSServer(); await server.start(); await server.registerPathHandler("/test", (_req, resp) => { resp.writeHead(200, { "Content-Type": "text/plain" }); resp.end("ok"); }); let port = server.port(); // Set up DNS to return both IPv6 and IPv4. override.addIPOverride(host, "::1"); override.addIPOverride(host, "127.0.0.1"); // Pause TCP connect on the IPv6 address so it hangs indefinitely. let pausedAddr = mockController.createScriptableNetAddr("::1", port); mockController.pauseTCPConnect(pausedAddr); let { addr, status, buffer } = await openChan(`https://${host}:${port}/test`); Assert.equal(status, 200, "Request should succeed"); Assert.equal(buffer, "ok", "Response body should match"); Assert.equal(addr, "127.0.0.1", "Should fall back to IPv4 after IPv6 hangs"); await server.stop(); } async function do_test_all_attempts_fail(host) { await resetConnections(); let server = new NodeHTTPSServer(); await server.start(); await server.registerPathHandler("/test", (_req, resp) => { resp.writeHead(200, { "Content-Type": "text/plain" }); resp.end("ok"); }); let port = server.port(); // Set up DNS to return both IPv6 and IPv4. override.addIPOverride(host, "::1"); override.addIPOverride(host, "127.0.0.1"); // Block TCP connect on both addresses so all attempts fail. let blockedAddr6 = mockController.createScriptableNetAddr("::1", port); mockController.blockTCPConnect(blockedAddr6); let blockedAddr4 = mockController.createScriptableNetAddr("127.0.0.1", port); mockController.blockTCPConnect(blockedAddr4); let chan = NetUtil.newChannel({ uri: `https://${host}:${port}/test`, loadUsingSystemPrincipal: true, }).QueryInterface(Ci.nsIHttpChannel); chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; await new Promise(resolve => { chan.asyncOpen( new ChannelListener(() => resolve(), null, CL_EXPECT_FAILURE) ); }); Assert.equal( chan.status, Cr.NS_ERROR_CONNECTION_REFUSED, "Should fail with connection refused" ); await server.stop(); } // Test all attempts fail, speculative disabled. add_task(async function test_all_attempts_fail_no_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); await do_test_all_attempts_fail("alt1.example.com"); }); // Test all attempts fail, speculative enabled. add_task(async function test_all_attempts_fail_with_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); await do_test_all_attempts_fail("alt1.example.com"); }); async function do_test_cancel_during_connection(host) { await resetConnections(); let server = new NodeHTTPSServer(); await server.start(); await server.registerPathHandler("/test", (_req, resp) => { resp.writeHead(200, { "Content-Type": "text/plain" }); resp.end("ok"); }); let port = server.port(); override.addIPOverride(host, "::1"); override.addIPOverride(host, "127.0.0.1"); // Pause TCP connect on both addresses so connections hang. let pausedAddr6 = mockController.createScriptableNetAddr("::1", port); mockController.pauseTCPConnect(pausedAddr6); let pausedAddr4 = mockController.createScriptableNetAddr("127.0.0.1", port); mockController.pauseTCPConnect(pausedAddr4); let chan = NetUtil.newChannel({ uri: `https://${host}:${port}/test`, loadUsingSystemPrincipal: true, }).QueryInterface(Ci.nsIHttpChannel); chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; let openPromise = new Promise(resolve => { chan.asyncOpen( new ChannelListener(() => resolve(), null, CL_EXPECT_FAILURE) ); }); // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(resolve => setTimeout(resolve, 100)); chan.cancel(Cr.NS_BINDING_ABORTED); await openPromise; Assert.equal(chan.status, Cr.NS_BINDING_ABORTED, "Should be cancelled"); await server.stop(); } add_task(async function test_cancel_during_connection_no_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); await do_test_cancel_during_connection("alt2.example.com"); }); add_task(async function test_cancel_during_connection_with_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); await do_test_cancel_during_connection("alt2.example.com"); }); // Test fallback when first attempt hangs, speculative disabled. // pauseTCPConnect does not work on Linux, so skip these tests there. add_task( { skip_if: () => AppConstants.platform == "linux" }, async function test_first_attempt_slow_no_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); await do_test_first_attempt_slow("alt2.example.com"); } ); // Test fallback when first attempt hangs, speculative enabled. add_task( { skip_if: () => AppConstants.platform == "linux" }, async function test_first_attempt_slow_with_speculative() { Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); await do_test_first_attempt_slow("alt2.example.com"); } );