Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* 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";
// Verifies that the speculative HTTPS RR prefetch issued by nsHttpChannel is
// reused by Happy Eyeballs instead of being re-fetched.
//
// The channel prefetch and Happy Eyeballs both resolve the HTTPS RR, but they
// used to do so under different OriginAttributes: the prefetch resolved under
// the HTTPS-RR attributes (partition key scheme forced to "https") while Happy
// Eyeballs resolves under the connection's network-state attributes. For a
// partitioned load whose partition key scheme is "http" these two keys differ,
// so the prefetch landed in a different DNS cache entry and Happy Eyeballs had
// to issue its own query -- two HTTPS queries reached the TRR server. Now the
// prefetch resolves under the connection's network-state attributes, so Happy
// Eyeballs hits the same cache entry and the server sees exactly one HTTPS
// query.
const { NodeHTTP2Server } = ChromeUtils.importESModule(
);
var { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
let trrServer;
let h2Server;
// The origin host. Must differ from the TRR endpoint host below, which is
// resolved via the bootstrap address (no TRR query): only a distinct host is
// actually resolved through TRR and thus counted by the server.
const HOST = "alt1.example.com";
const TRR_HOST = "foo.example.com";
// Partition key whose scheme is "http". The HTTPS-RR OriginAttributes rewrites
// the scheme to "https", so the prefetch and the connection's network-state
// attributes only diverge when the partition key scheme isn't already "https".
// A document load would recompute the partition key from its own URI, so this
// must be a partitioned subresource load to keep the http scheme.
const PARTITION_KEY = "(http,example.org)";
// HTTPS RR queries for a non-default port are sent under a port-prefixed name.
function httpsRRName() {
return `_${h2Server.port()}._https.${HOST}`;
}
add_setup(async function () {
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
// Avoid speculative connections issuing their own lookups and muddying the
// per-host query counts.
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
trrServer = new TRRServer();
await trrServer.start();
trr_test_setup();
Services.prefs.setIntPref("network.trr.mode", 3);
Services.prefs.setCharPref(
"network.trr.uri",
`https://${TRR_HOST}:${trrServer.port()}/dns-query`
);
h2Server = new NodeHTTP2Server();
await h2Server.start();
await h2Server.registerPathHandler("/", (_req, resp) => {
resp.writeHead(200, { "Content-Type": "text/plain" });
resp.end("ok");
});
registerCleanupFunction(async () => {
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
trr_clear_prefs();
if (trrServer) {
await trrServer.stop();
}
if (h2Server) {
await h2Server.stop();
}
});
});
async function resetState() {
Services.obs.notifyObservers(null, "net:cancel-all-connections");
let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent);
await nssComponent.asyncClearSSLExternalAndInternalSessionCache();
Services.dns.clearCache(true);
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 1000));
// Reset the TRR server's per-host request counts.
await trrServer.execute("global.dns_query_counts = {}");
}
async function registerAnswers() {
await trrServer.registerDoHAnswers(httpsRRName(), "HTTPS", {
answers: [
{
name: httpsRRName(),
ttl: 55,
type: "HTTPS",
flush: false,
data: {
priority: 1,
name: HOST,
values: [{ key: "alpn", value: ["h2"] }],
},
},
],
});
await trrServer.registerDoHAnswers(HOST, "A", {
answers: [
{ name: HOST, ttl: 55, type: "A", flush: false, data: "127.0.0.1" },
],
});
await trrServer.registerDoHAnswers(HOST, "AAAA", {
answers: [{ name: HOST, ttl: 55, type: "AAAA", flush: false, data: "::1" }],
});
}
async function openChannel() {
// A third-party subresource of an http first party. The http first party
// gives a partition key with the "http" scheme, which is what makes the
// HTTPS-RR attributes (scheme forced to "https") diverge from the
// connection's network-state attributes. The triggering principal must not
// be the system principal, otherwise HTTPS RR is disallowed for a
// non-document load (see nsHttpChannel::OnBeforeConnect).
let principal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI("http://example.org/"),
{}
);
let chan = NetUtil.newChannel({
uri: `https://${HOST}:${h2Server.port()}/`,
loadingPrincipal: principal,
triggeringPrincipal: principal,
securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadInfo.originAttributes = { partitionKey: PARTITION_KEY };
let status = await new Promise(resolve => {
chan.asyncOpen({
onStartRequest(_request) {},
onDataAvailable(_request, stream, _offset, count) {
read_stream(stream, count);
},
onStopRequest(request) {
resolve(request.status);
},
});
});
return { chan, status };
}
// Happy Eyeballs reuses the channel's HTTPS RR prefetch: only one HTTPS query
// reaches the server, even though both the prefetch and Happy Eyeballs resolve
// the HTTPS RR.
add_task(async function test_he_reuses_https_rr_prefetch() {
await resetState();
await registerAnswers();
let { chan, status } = await openChannel();
Assert.equal(status, Cr.NS_OK, "request should succeed");
Assert.equal(
chan.QueryInterface(Ci.nsIHttpChannel).responseStatus,
200,
"response status should be 200"
);
Assert.equal(
await trrServer.requestCount(httpsRRName(), "HTTPS"),
1,
"HE reused the prefetch: only one HTTPS query reached the server"
);
});