Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
-
CI results over the last 21 days1,843Runs99.95%Pass0Failures1Timeouts0Crashes0Skips
Runs with issues per day (up to 0.1%) Oct 16Nov 5- Timeouts
- Manifest: netwerk/test/unit/xpcshell.toml
/* 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
"use strict";
// An Alt-Svc alternate reached through a SOCKS remote-DNS proxy must still be
// authenticated as the origin.
//
// RFC 7838 requires the alternative service to present a certificate valid for
// the origin; the alternate host name is only where to connect. Here the
// alternate serves a certificate covering ONLY the alternate host, so
// validation must fail and the mapping must never go live -- every response has
// to keep coming from the origin.
//
// NOTE: this cannot fail today. Alt-Svc routes are not given to Happy Eyeballs
// while proxied (the "&& !pi" in AlternateServices.cpp), and even without that
// guard the state machine attempts the origin by name rather than the alt-svc
// entry, so the alternate is never contacted and the check below holds for the
// wrong reason. It guards the invariant once that is fixed.
//
// This is the by-name connection path (SOCKS with
// TRANSPARENT_PROXY_RESOLVES_HOST resolves names itself, so Firefox connects by
// name). On that path the host being connected to is handed to the transport as
// the route, while the origin -- which drives SNI and certificate checks -- must
// keep coming from the connection info. Passing the by-name host as the origin
// instead would authenticate the alternate against its own name and wrongly
// validate the mapping.
const { NodeHTTP2Server, NodeSocks5ForwardServer } = ChromeUtils.importESModule(
);
const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
Ci.nsINativeDNSResolverOverride
);
const ORIGIN_HOST = "origin.altsvc-certorigin.example";
const ALT_HOST = "alt.altsvc-certorigin.example";
const ORIGIN_PATH = "/altsvc-cert-origin";
let originServer = new NodeHTTP2Server();
let altServer = new NodeHTTP2Server();
let socksServer = new NodeSocks5ForwardServer();
let originURL;
let gFilter;
// Routes every Firefox channel through the SOCKS5 remote-DNS proxy.
class SocksRemoteDnsFilter {
constructor(port) {
this._port = port;
this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]);
}
applyFilter(uri, pi, cb) {
cb.onProxyFilterResult(
pps.newProxyInfo(
"socks",
"127.0.0.1",
this._port,
"",
"",
Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST,
1000,
null
)
);
}
}
function makeChan(url) {
let uri = NetUtil.newURI(url);
let principal = Services.scriptSecurityManager.createContentPrincipal(
uri,
{}
);
return NetUtil.newChannel({
uri: url,
loadingPrincipal: principal,
triggeringPrincipal: principal,
securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
}).QueryInterface(Ci.nsIHttpChannel);
}
// Resolves with [status, body] regardless of success/failure.
function fetchOrigin() {
let chan = makeChan(originURL);
return new Promise(resolve => {
let body = "";
chan.asyncOpen({
QueryInterface: ChromeUtils.generateQI([
"nsIStreamListener",
"nsIRequestObserver",
]),
onStartRequest() {},
onDataAvailable(req, stream, off, cnt) {
body += read_stream(stream, cnt);
},
onStopRequest(req, status) {
resolve([status, body]);
},
});
});
}
add_setup(async function setup() {
do_get_profile();
Services.prefs.setBoolPref("network.http.http2.enabled", true);
Services.prefs.setBoolPref("network.http.altsvc.enabled", true);
Services.prefs.setBoolPref("network.http.altsvc.oe", true);
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
Services.prefs.setBoolPref("network.http.happy_eyeballs_proxy_enabled", true);
Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
await originServer.start(0, [ORIGIN_HOST]);
// The alternate's certificate deliberately does NOT cover the origin, so
// authenticating it as the origin (the only correct behaviour) fails.
await altServer.start(0, [ALT_HOST]);
await socksServer.start();
// The proxy forwards any hostname to loopback, so no client-side override is
// needed for connectivity. This one only exists so a client-side resolution
// of the alternate would succeed rather than fail for the wrong reason.
override.addIPOverride(ALT_HOST, "127.0.0.1");
let altSvc = `h2="${ALT_HOST}:${altServer.port()}"; ma=3600`;
await originServer.execute(`global.ALT_SVC = ${JSON.stringify(altSvc)}`);
await originServer.registerPathHandler(ORIGIN_PATH, (req, resp) => {
resp.setHeader("Alt-Svc", global.ALT_SVC);
resp.writeHead(200, { "Content-Type": "text/plain" });
resp.end("origin");
});
await altServer.registerPathHandler(ORIGIN_PATH, (req, resp) => {
resp.writeHead(200, { "Content-Type": "text/plain" });
resp.end("ALT");
});
gFilter = new SocksRemoteDnsFilter(socksServer.port());
pps.registerFilter(gFilter, 10);
registerCleanupFunction(async () => {
pps.unregisterFilter(gFilter);
try {
await originServer.stop();
await altServer.stop();
await socksServer.stop();
} catch (e) {
info("Error stopping servers: " + e);
}
Services.prefs.clearUserPref("network.http.http2.enabled");
Services.prefs.clearUserPref("network.http.altsvc.enabled");
Services.prefs.clearUserPref("network.http.altsvc.oe");
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.http.happy_eyeballs_proxy_enabled");
Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
override.clearOverrides();
Services.dns.clearCache(true);
});
});
add_task(async function altsvc_alternate_must_authenticate_as_origin() {
// Prime the origin so the Alt-Svc header is seen and validation starts.
let [status, body] = await fetchOrigin();
Assert.equal(status, Cr.NS_OK, "first origin fetch succeeded");
Assert.equal(body, "origin", "first response came from the origin");
// Give validation several chances to run, dropping connections in between so
// a later request is not simply coalesced onto the existing origin
// connection. The alternate must never serve a response.
for (let i = 0; i < 10; i++) {
Services.obs.notifyObservers(null, "net:cancel-all-connections");
await new Promise(r => do_timeout(250, r));
[status, body] = await fetchOrigin();
Assert.equal(status, Cr.NS_OK, `origin fetch ${i} succeeded`);
Assert.notEqual(
body,
"ALT",
"the alternate must not serve the origin: its certificate does not " +
"cover the origin, so Alt-Svc validation has to fail"
);
Assert.equal(body, "origin", `response ${i} came from the origin`);
}
// The alternate host should have been contacted through the proxy (so the
// check above is not vacuous), but its handshake must not have validated it.
let hosts = await socksServer.connectedHostnames();
info("hosts the proxy was asked to connect to: " + hosts.join(", "));
});