tor-browser

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

commit d99f80eefef95212f825c381e13540754b97d860
parent 5d480818f9232e01b0fc25db779df314f717e2a4
Author: Kershaw Chang <kershaw@mozilla.com>
Date:   Thu,  2 Oct 2025 13:59:39 +0000

Bug 1991426 - Run tests with speculative connections enabled and disabled, r=necko-reviewers,valentin

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

Diffstat:
Anetwerk/test/unit/http3_proxy_common.js | 485+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/test/unit/test_http3_proxy.js | 479+++----------------------------------------------------------------------------
Anetwerk/test/unit/test_http3_proxy_no_speculative.js | 25+++++++++++++++++++++++++
Mnetwerk/test/unit/xpcshell.toml | 10++++++++++
4 files changed, 535 insertions(+), 464 deletions(-)

diff --git a/netwerk/test/unit/http3_proxy_common.js b/netwerk/test/unit/http3_proxy_common.js @@ -0,0 +1,485 @@ +/* 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 { + Http3ProxyFilter, + with_node_servers, + NodeHTTPServer, + NodeHTTPSServer, + NodeHTTP2Server, + NodeHTTP2ProxyServer, +} = ChromeUtils.importESModule("resource://testing-common/NodeServer.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; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); +let proxyHost; +let proxyPort; +let noResponsePort; +let proxyAuth; +let proxyFilter; + +/** + * Sets up proxy filter to MASQUE H3 proxy + */ +async function setup_http3_proxy() { + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + Services.prefs.setIntPref("network.webtransport.datagram_size", 1500); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setIntPref("network.http.http3.max_gso_segments", 1); // TODO: fix underflow + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + proxyHost = "foo.example.com"; + ({ masqueProxyPort: proxyPort, noResponsePort } = + await create_masque_proxy_server()); + proxyAuth = ""; + + Assert.notEqual(proxyPort, null); + Assert.notEqual(proxyPort, ""); + + // A dummy request to make sure AltSvcCache::mStorage is ready. + let chan = makeChan(`https://localhost`); + await channelOpenPromise(chan, CL_EXPECT_FAILURE); + + proxyFilter = new Http3ProxyFilter( + proxyHost, + proxyPort, + 0, + "/.well-known/masque/udp/{target_host}/{target_port}/", + proxyAuth + ); + pps.registerFilter(proxyFilter, 10); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + }); +} + +/** + * Tests HTTP connect through H3 proxy to HTTP, HTTPS and H2 servers + * Makes multiple requests. Expects success. + */ +async function test_http_connect() { + info("Running test_http_connect"); + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + info(`Proxying to ${server.constructor.name} server`); + await server.registerPathHandler("/first", (req, resp) => { + resp.writeHead(200); + resp.end("first"); + }); + await server.registerPathHandler("/second", (req, resp) => { + resp.writeHead(200); + resp.end("second"); + }); + await server.registerPathHandler("/third", (req, resp) => { + resp.writeHead(200); + resp.end("third"); + }); + let chan = makeChan( + `${server.protocol()}://alt1.example.com:${server.port()}/first` + ); + let [req, buf] = await channelOpenPromise( + chan, + CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL + ); + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(buf, "first"); + chan = makeChan( + `${server.protocol()}://alt1.example.com:${server.port()}/second` + ); + [req, buf] = await channelOpenPromise( + chan, + CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL + ); + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(buf, "second"); + + chan = makeChan( + `${server.protocol()}://alt1.example.com:${server.port()}/third` + ); + [req, buf] = await channelOpenPromise( + chan, + CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL + ); + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(buf, "third"); + } + ); +} + +/** + * Test HTTP CONNECT authentication failure - tests behavior when proxy + * authentication is required but not provided or incorrect + */ +async function test_http_connect_auth_failure() { + info("Running test_http_connect_auth_failure"); + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + info(`Testing auth failure with ${server.constructor.name} server`); + // Register a handler that requires authentication + await server.registerPathHandler("/auth-required", (req, resp) => { + const auth = req.headers.authorization; + if (!auth || auth !== "Basic dGVzdDp0ZXN0") { + resp.writeHead(401, { + "WWW-Authenticate": 'Basic realm="Test Realm"', + "Content-Type": "text/plain", + }); + resp.end(""); + } else { + resp.writeHead(200); + resp.end("Authenticated"); + } + }); + + let chan = makeChan( + `${server.protocol()}://alt1.example.com:${server.port()}/auth-required` + ); + let [req] = await channelOpenPromise( + chan, + CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL + ); + + // Should receive 401 Unauthorized through the tunnel + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 401); + } + ); +} + +/** + * Test HTTP CONNECT with large request/response data - ensures the tunnel + * can handle substantial data transfer without corruption or truncation + */ +async function test_http_connect_large_data() { + info("Running test_http_connect_large_data"); + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + info( + `Testing large data transfer with ${server.constructor.name} server` + ); + // Create a large response payload (1MB of data) + const largeData = "x".repeat(1024 * 1024); + await server.registerPathHandler("/large", (req, resp) => { + const largeData = "x".repeat(1024 * 1024); + resp.writeHead(200, { "Content-Type": "text/plain" }); + resp.end(largeData); + }); + + let chan = makeChan( + `${server.protocol()}://alt1.example.com:${server.port()}/large` + ); + let [req, buf] = await channelOpenPromise( + chan, + CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL + ); + + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(buf.length, largeData.length); + Assert.equal(buf, largeData); + } + ); +} + +/** + * Test HTTP CONNECT tunnel connection refused - simulates target server + * being unreachable or refusing connections + */ +async function test_http_connect_connection_refused() { + info("Running test_http_connect_connection_refused"); + // Test connecting to a port that's definitely not in use + let chan = makeChan(`http://alt1.example.com:667/refused`); + let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); + + // Should fail to establish tunnel connection + Assert.notEqual(req.status, Cr.NS_OK); + info(`Connection refused status: ${req.status}`); +} + +/** + * Test HTTP CONNECT with invalid target host - verifies proper error handling + * when trying to tunnel to a non-existent hostname + */ +async function test_http_connect_invalid_host() { + info("Running test_http_connect_invalid_host"); + let chan = makeChan(`http://nonexistent.invalid.example/test`); + let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); + + // Should fail DNS resolution for invalid hostname + Assert.notEqual(req.status, Cr.NS_OK); + info(`Invalid host status: ${req.status}`); +} + +/** + * Test concurrent HTTP CONNECT tunnels - ensures multiple simultaneous + * requests can be established and used independently through the same H3 proxy + */ +async function test_concurrent_http_connect_tunnels() { + info("Running test_concurrent_http_connect_tunnels"); + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + info(`Testing concurrent tunnels with ${server.constructor.name} server`); + + // Register multiple endpoints + await server.registerPathHandler("/concurrent1", (req, resp) => { + resp.writeHead(200); + resp.end("response1"); + }); + await server.registerPathHandler("/concurrent2", (req, resp) => { + resp.writeHead(200); + resp.end("response2"); + }); + await server.registerPathHandler("/concurrent3", (req, resp) => { + resp.writeHead(200); + resp.end("response3"); + }); + + // Create multiple concurrent requests through the tunnel + const promises = []; + for (let i = 1; i <= 3; i++) { + let chan = makeChan( + `${server.protocol()}://alt1.example.com:${server.port()}/concurrent${i}` + ); + promises.push( + channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL) + ); + } + + const results = await Promise.all(promises); + + // Verify all requests succeeded with correct responses + for (let i = 0; i < 3; i++) { + const [req, buf] = results[i]; + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(buf, `response${i + 1}`); + } + info("All concurrent tunnels completed successfully"); + } + ); +} + +/** + * Test HTTP CONNECT tunnel stream closure handling - verifies proper cleanup + * when the tunnel connection is closed unexpectedly + */ +// eslint-disable-next-line no-unused-vars +async function test_http_connect_stream_closure() { + info("Running test_http_connect_stream_closure"); + await with_node_servers([NodeHTTPServer], async server => { + info(`Testing stream closure with ${server.constructor.name} server`); + + await server.registerPathHandler("/close", (req, resp) => { + // Send partial response then close connection abruptly + resp.writeHead(200, { "Content-Type": "text/plain" }); + resp.write("partial"); + // Simulate connection closure + resp.destroy(); + }); + + let chan = makeChan( + `${server.protocol()}://alt1.example.com:${server.port()}/close` + ); + let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); + + // Should handle connection closure gracefully + Assert.notEqual(req.status, Cr.NS_OK); + info(`Stream closure status: ${req.status}`); + }); +} + +/** + * Test connect-udp - SUCCESS case. + * Will use h3 proxy to connect to h3 server. + */ +async function test_connect_udp() { + info("Running test_connect_udp"); + let h3Port = Services.env.get("MOZHTTP3_PORT"); + info(`h3Port = ${h3Port}`); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + `alt1.example.com;h3=:${h3Port}` + ); + + { + let chan = makeChan(`https://alt1.example.com:${h3Port}/no_body`); + let [req] = await channelOpenPromise( + chan, + CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL + ); + Assert.equal(req.protocolVersion, "h3"); + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(req.responseStatus, 200); + } +} + +async function test_http_connect_fallback() { + info("Running test_http_connect_fallback"); + pps.unregisterFilter(proxyFilter); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "" + ); + + let proxyPort = noResponsePort; + let proxy = new NodeHTTP2ProxyServer(); + await proxy.startWithoutProxyFilter(proxyPort); + Assert.equal(proxyPort, proxy.port()); + dump(`proxy port=${proxy.port()}\n`); + + let server = new NodeHTTP2Server(); + await server.start(); + + // Register multiple endpoints + await server.registerPathHandler("/concurrent1", (req, resp) => { + resp.writeHead(200); + resp.end("response1"); + }); + await server.registerPathHandler("/concurrent2", (req, resp) => { + resp.writeHead(200); + resp.end("response2"); + }); + await server.registerPathHandler("/concurrent3", (req, resp) => { + resp.writeHead(200); + resp.end("response3"); + }); + + let filter = new Http3ProxyFilter( + proxyHost, + proxy.port(), + 0, + "/.well-known/masque/udp/{target_host}/{target_port}/", + proxyAuth + ); + pps.registerFilter(filter, 10); + + registerCleanupFunction(async () => { + await proxy.stop(); + await server.stop(); + }); + + // Create multiple concurrent requests through the tunnel + const promises = []; + for (let i = 1; i <= 3; i++) { + let chan = makeChan( + `${server.protocol()}://alt1.example.com:${server.port()}/concurrent${i}` + ); + promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL)); + } + + const results = await Promise.all(promises); + + // Verify all requests succeeded with correct responses + for (let i = 0; i < 3; i++) { + const [req, buf] = results[i]; + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(buf, `response${i + 1}`); + } + + let h3Port = server.port(); + console.log(`h3Port = ${h3Port}`); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + `alt1.example.com;h3=:${h3Port}` + ); + + let chan = makeChan(`https://alt1.example.com:${h3Port}/concurrent1`); + let [req] = await channelOpenPromise( + chan, + CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL + ); + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(req.responseStatus, 200); + + await proxy.stop(); + pps.unregisterFilter(filter); + await server.stop(); +} + +async function test_inner_connection_fallback() { + info("Running test_inner_connection_fallback"); + let h3Port = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE"); + info(`h3Port = ${h3Port}`); + + // Register the connect-udp proxy. + pps.registerFilter(proxyFilter, 10); + + let server = new NodeHTTPSServer(); + await server.start(h3Port); + + // Register multiple endpoints + await server.registerPathHandler("/concurrent1", (req, resp) => { + resp.writeHead(200); + resp.end("fallback1"); + }); + await server.registerPathHandler("/concurrent2", (req, resp) => { + resp.writeHead(200); + resp.end("fallback2"); + }); + await server.registerPathHandler("/concurrent3", (req, resp) => { + resp.writeHead(200); + resp.end("fallback3"); + }); + registerCleanupFunction(async () => { + await server.stop(); + }); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + `alt1.example.com;h3=:${h3Port}` + ); + + // Create multiple concurrent requests through the tunnel + const promises = []; + for (let i = 1; i <= 3; i++) { + let chan = makeChan( + `${server.protocol()}://alt1.example.com:${h3Port}/concurrent${i}` + ); + promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL)); + } + + const results = await Promise.all(promises); + + // Verify all requests succeeded with correct responses + for (let i = 0; i < 3; i++) { + const [req, buf] = results[i]; + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(buf, `fallback${i + 1}`); + } + await server.stop(); +} diff --git a/netwerk/test/unit/test_http3_proxy.js b/netwerk/test/unit/test_http3_proxy.js @@ -4,471 +4,22 @@ "use strict"; -/* import-globals-from head_cache.js */ -/* import-globals-from head_cookies.js */ -/* import-globals-from head_channels.js */ +/* import-globals-from http3_proxy_common.js */ -const { - Http3ProxyFilter, - with_node_servers, - NodeHTTPServer, - NodeHTTPSServer, - NodeHTTP2Server, - NodeHTTP2ProxyServer, -} = ChromeUtils.importESModule("resource://testing-common/NodeServer.sys.mjs"); +add_setup(async function () { + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 20); -function makeChan(uri) { - let chan = NetUtil.newChannel({ - uri, - loadUsingSystemPrincipal: true, - }).QueryInterface(Ci.nsIHttpChannel); - chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; - return chan; -} - -function channelOpenPromise(chan, flags) { - return new Promise(resolve => { - function finish(req, buffer) { - resolve([req, buffer]); - } - chan.asyncOpen(new ChannelListener(finish, null, flags)); - }); -} - -let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); -let proxyHost; -let proxyPort; -let noResponsePort; -let proxyAuth; -let proxyFilter; - -/** - * Sets up proxy filter to MASQUE H3 proxy - */ -add_setup(async function setup() { - Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); - Services.prefs.setBoolPref("network.dns.disableIPv6", true); - Services.prefs.setIntPref("network.webtransport.datagram_size", 1500); - Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); - Services.prefs.setIntPref("network.http.http3.max_gso_segments", 1); // TODO: fix underflow - let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( - Ci.nsIX509CertDB - ); - addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); - addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); - - proxyHost = "foo.example.com"; - ({ masqueProxyPort: proxyPort, noResponsePort } = - await create_masque_proxy_server()); - proxyAuth = ""; - - Assert.notEqual(proxyPort, null); - Assert.notEqual(proxyPort, ""); - - // A dummy request to make sure AltSvcCache::mStorage is ready. - let chan = makeChan(`https://localhost`); - await channelOpenPromise(chan, CL_EXPECT_FAILURE); - - proxyFilter = new Http3ProxyFilter( - proxyHost, - proxyPort, - 0, - "/.well-known/masque/udp/{target_host}/{target_port}/", - proxyAuth - ); - pps.registerFilter(proxyFilter, 10); - - registerCleanupFunction(() => { - Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); - }); -}); - -/** - * Tests HTTP connect through H3 proxy to HTTP, HTTPS and H2 servers - * Makes multiple requests. Expects success. - */ -add_task(async function test_http_connect() { - await with_node_servers( - [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], - async server => { - info(`Proxying to ${server.constructor.name} server`); - await server.registerPathHandler("/first", (req, resp) => { - resp.writeHead(200); - resp.end("first"); - }); - await server.registerPathHandler("/second", (req, resp) => { - resp.writeHead(200); - resp.end("second"); - }); - await server.registerPathHandler("/third", (req, resp) => { - resp.writeHead(200); - resp.end("third"); - }); - let chan = makeChan( - `${server.protocol()}://alt1.example.com:${server.port()}/first` - ); - let [req, buf] = await channelOpenPromise( - chan, - CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL - ); - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(buf, "first"); - chan = makeChan( - `${server.protocol()}://alt1.example.com:${server.port()}/second` - ); - [req, buf] = await channelOpenPromise( - chan, - CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL - ); - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(buf, "second"); - - chan = makeChan( - `${server.protocol()}://alt1.example.com:${server.port()}/third` - ); - [req, buf] = await channelOpenPromise( - chan, - CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL - ); - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(buf, "third"); - } - ); -}); - -/** - * Test HTTP CONNECT authentication failure - tests behavior when proxy - * authentication is required but not provided or incorrect - */ -add_task(async function test_http_connect_auth_failure() { - await with_node_servers( - [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], - async server => { - info(`Testing auth failure with ${server.constructor.name} server`); - // Register a handler that requires authentication - await server.registerPathHandler("/auth-required", (req, resp) => { - const auth = req.headers.authorization; - if (!auth || auth !== "Basic dGVzdDp0ZXN0") { - resp.writeHead(401, { - "WWW-Authenticate": 'Basic realm="Test Realm"', - "Content-Type": "text/plain", - }); - resp.end(""); - } else { - resp.writeHead(200); - resp.end("Authenticated"); - } - }); - - let chan = makeChan( - `${server.protocol()}://alt1.example.com:${server.port()}/auth-required` - ); - let [req] = await channelOpenPromise( - chan, - CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL - ); - - // Should receive 401 Unauthorized through the tunnel - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 401); - } - ); -}); - -/** - * Test HTTP CONNECT with large request/response data - ensures the tunnel - * can handle substantial data transfer without corruption or truncation - */ -add_task(async function test_http_connect_large_data() { - await with_node_servers( - [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], - async server => { - info( - `Testing large data transfer with ${server.constructor.name} server` - ); - // Create a large response payload (1MB of data) - const largeData = "x".repeat(1024 * 1024); - await server.registerPathHandler("/large", (req, resp) => { - const largeData = "x".repeat(1024 * 1024); - resp.writeHead(200, { "Content-Type": "text/plain" }); - resp.end(largeData); - }); - - let chan = makeChan( - `${server.protocol()}://alt1.example.com:${server.port()}/large` - ); - let [req, buf] = await channelOpenPromise( - chan, - CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL - ); - - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(buf.length, largeData.length); - Assert.equal(buf, largeData); - } - ); -}); - -/** - * Test HTTP CONNECT tunnel connection refused - simulates target server - * being unreachable or refusing connections - */ -add_task(async function test_http_connect_connection_refused() { - // Test connecting to a port that's definitely not in use - let chan = makeChan(`http://alt1.example.com:667/refused`); - let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); - - // Should fail to establish tunnel connection - Assert.notEqual(req.status, Cr.NS_OK); - info(`Connection refused status: ${req.status}`); -}); - -/** - * Test HTTP CONNECT with invalid target host - verifies proper error handling - * when trying to tunnel to a non-existent hostname - */ -add_task(async function test_http_connect_invalid_host() { - let chan = makeChan(`http://nonexistent.invalid.example/test`); - let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); - - // Should fail DNS resolution for invalid hostname - Assert.notEqual(req.status, Cr.NS_OK); - info(`Invalid host status: ${req.status}`); -}); - -/** - * Test concurrent HTTP CONNECT tunnels - ensures multiple simultaneous - * requests can be established and used independently through the same H3 proxy - */ -add_task(async function test_concurrent_http_connect_tunnels() { - await with_node_servers( - [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], - async server => { - info(`Testing concurrent tunnels with ${server.constructor.name} server`); - - // Register multiple endpoints - await server.registerPathHandler("/concurrent1", (req, resp) => { - resp.writeHead(200); - resp.end("response1"); - }); - await server.registerPathHandler("/concurrent2", (req, resp) => { - resp.writeHead(200); - resp.end("response2"); - }); - await server.registerPathHandler("/concurrent3", (req, resp) => { - resp.writeHead(200); - resp.end("response3"); - }); - - // Create multiple concurrent requests through the tunnel - const promises = []; - for (let i = 1; i <= 3; i++) { - let chan = makeChan( - `${server.protocol()}://alt1.example.com:${server.port()}/concurrent${i}` - ); - promises.push( - channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL) - ); - } - - const results = await Promise.all(promises); - - // Verify all requests succeeded with correct responses - for (let i = 0; i < 3; i++) { - const [req, buf] = results[i]; - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(buf, `response${i + 1}`); - } - info("All concurrent tunnels completed successfully"); - } - ); + await setup_http3_proxy(); }); -/** - * Test HTTP CONNECT tunnel stream closure handling - verifies proper cleanup - * when the tunnel connection is closed unexpectedly - */ -add_task(async function test_http_connect_stream_closure() { - await with_node_servers([NodeHTTPServer], async server => { - info(`Testing stream closure with ${server.constructor.name} server`); - - await server.registerPathHandler("/close", (req, resp) => { - // Send partial response then close connection abruptly - resp.writeHead(200, { "Content-Type": "text/plain" }); - resp.write("partial"); - // Simulate connection closure - resp.destroy(); - }); - - let chan = makeChan( - `${server.protocol()}://alt1.example.com:${server.port()}/close` - ); - let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); - - // Should handle connection closure gracefully - Assert.notEqual(req.status, Cr.NS_OK); - info(`Stream closure status: ${req.status}`); - }); -}).skip( - "TODO: Proxy needs to close the stream properly when socket failures occur" -); - -/** - * Test connect-udp - SUCCESS case. - * Will use h3 proxy to connect to h3 server. - */ -add_task(async function test_connect_udp() { - let h3Port = Services.env.get("MOZHTTP3_PORT"); - info(`h3Port = ${h3Port}`); - - Services.prefs.setCharPref( - "network.http.http3.alt-svc-mapping-for-testing", - `alt1.example.com;h3=:${h3Port}` - ); - - { - let chan = makeChan(`https://alt1.example.com:${h3Port}/no_body`); - let [req] = await channelOpenPromise( - chan, - CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL - ); - Assert.equal(req.protocolVersion, "h3"); - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(req.responseStatus, 200); - } -}); - -add_task(async function test_http_connect_fallback() { - pps.unregisterFilter(proxyFilter); - - Services.prefs.setCharPref( - "network.http.http3.alt-svc-mapping-for-testing", - "" - ); - - let proxyPort = noResponsePort; - let proxy = new NodeHTTP2ProxyServer(); - await proxy.startWithoutProxyFilter(proxyPort); - Assert.equal(proxyPort, proxy.port()); - dump(`proxy port=${proxy.port()}\n`); - - let server = new NodeHTTP2Server(); - await server.start(); - - // Register multiple endpoints - await server.registerPathHandler("/concurrent1", (req, resp) => { - resp.writeHead(200); - resp.end("response1"); - }); - await server.registerPathHandler("/concurrent2", (req, resp) => { - resp.writeHead(200); - resp.end("response2"); - }); - await server.registerPathHandler("/concurrent3", (req, resp) => { - resp.writeHead(200); - resp.end("response3"); - }); - - let filter = new Http3ProxyFilter( - proxyHost, - proxy.port(), - 0, - "/.well-known/masque/udp/{target_host}/{target_port}/", - proxyAuth - ); - pps.registerFilter(filter, 10); - - registerCleanupFunction(async () => { - await proxy.stop(); - await server.stop(); - }); - - // Create multiple concurrent requests through the tunnel - const promises = []; - for (let i = 1; i <= 3; i++) { - let chan = makeChan( - `${server.protocol()}://alt1.example.com:${server.port()}/concurrent${i}` - ); - promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL)); - } - - const results = await Promise.all(promises); - - // Verify all requests succeeded with correct responses - for (let i = 0; i < 3; i++) { - const [req, buf] = results[i]; - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(buf, `response${i + 1}`); - } - - let h3Port = server.port(); - console.log(`h3Port = ${h3Port}`); - - Services.prefs.setCharPref( - "network.http.http3.alt-svc-mapping-for-testing", - `alt1.example.com;h3=:${h3Port}` - ); - - let chan = makeChan(`https://alt1.example.com:${h3Port}/concurrent1`); - let [req] = await channelOpenPromise( - chan, - CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL - ); - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(req.responseStatus, 200); - - await proxy.stop(); - pps.unregisterFilter(filter); - await server.stop(); -}); - -add_task(async function test_inner_connection_fallback() { - let h3Port = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE"); - info(`h3Port = ${h3Port}`); - - // Register the connect-udp proxy. - pps.registerFilter(proxyFilter, 10); - - let server = new NodeHTTPSServer(); - await server.start(h3Port); - - // Register multiple endpoints - await server.registerPathHandler("/concurrent1", (req, resp) => { - resp.writeHead(200); - resp.end("fallback1"); - }); - await server.registerPathHandler("/concurrent2", (req, resp) => { - resp.writeHead(200); - resp.end("fallback2"); - }); - await server.registerPathHandler("/concurrent3", (req, resp) => { - resp.writeHead(200); - resp.end("fallback3"); - }); - registerCleanupFunction(async () => { - await server.stop(); - }); - - Services.prefs.setCharPref( - "network.http.http3.alt-svc-mapping-for-testing", - `alt1.example.com;h3=:${h3Port}` - ); - - // Create multiple concurrent requests through the tunnel - const promises = []; - for (let i = 1; i <= 3; i++) { - let chan = makeChan( - `${server.protocol()}://alt1.example.com:${h3Port}/concurrent${i}` - ); - promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL)); - } - - const results = await Promise.all(promises); - - // Verify all requests succeeded with correct responses - for (let i = 0; i < 3; i++) { - const [req, buf] = results[i]; - Assert.equal(req.status, Cr.NS_OK); - Assert.equal(buf, `fallback${i + 1}`); - } -}); +add_task(test_http_connect); +add_task(test_http_connect_auth_failure); +add_task(test_http_connect_large_data); +add_task(test_http_connect_connection_refused); +add_task(test_http_connect_invalid_host); +add_task(test_concurrent_http_connect_tunnels); +// TODO: Proxy needs to close the stream properly when socket failures occur +// add_task(test_http_connect_stream_closure); +add_task(test_connect_udp); +add_task(test_http_connect_fallback); +add_task(test_inner_connection_fallback); diff --git a/netwerk/test/unit/test_http3_proxy_no_speculative.js b/netwerk/test/unit/test_http3_proxy_no_speculative.js @@ -0,0 +1,25 @@ +/* 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 http3_proxy_common.js */ + +add_setup(async function () { + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + + await setup_http3_proxy(); +}); + +add_task(test_http_connect); +add_task(test_http_connect_auth_failure); +add_task(test_http_connect_large_data); +add_task(test_http_connect_connection_refused); +add_task(test_http_connect_invalid_host); +add_task(test_concurrent_http_connect_tunnels); +// TODO: Proxy needs to close the stream properly when socket failures occur +// add_task(test_http_connect_stream_closure); +add_task(test_connect_udp); +add_task(test_http_connect_fallback); +add_task(test_inner_connection_fallback); diff --git a/netwerk/test/unit/xpcshell.toml b/netwerk/test/unit/xpcshell.toml @@ -26,6 +26,7 @@ support-files = [ "trr_common.js", "test_http3_prio_helpers.js", "http2_test_common.js", + "http3_proxy_common.js", ] # dom.serviceWorkers.enabled is currently set to false in StaticPrefList.yaml @@ -856,6 +857,15 @@ run-sequentially = ["true"] # http3server skip-if = ["true"] # Will be reenabled in bug 1865394 ["test_http3_proxy.js"] +head = "head_cookies.js head_channels.js head_cache.js head_http3.js http3_proxy_common.js" +run-sequentially = ["true"] # node server exceptions dont replay well +skip-if = [ + "os == 'android'", + "os == 'win' && os_version == '11.26100' && processor == 'x86_64' && msix'", # Bug 1808049 +] + +["test_http3_proxy_no_speculative.js"] +head = "head_cookies.js head_channels.js head_cache.js head_http3.js http3_proxy_common.js" run-sequentially = ["true"] # node server exceptions dont replay well skip-if = [ "os == 'android'",