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:
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'",