Source code

Revision control

Copy as Markdown

Other Tools

/* 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 [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();
}