/* 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";
const { NodeHTTP2Server } = ChromeUtils.importESModule(
"resource://testing-common/NodeServer.sys.mjs"
);
var { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
let trrServer;
let h2Server;
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);
trrServer =
new TRRServer();
await trrServer.start();
trr_test_setup();
Services.prefs.setIntPref(
"network.trr.mode",
3);
Services.prefs.setCharPref(
"network.trr.uri",
`https:
//foo.example.com:${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.http.speculative-parallel-limit");
trr_clear_prefs();
if (trrServer) {
await trrServer.stop();
}
if (h2Server) {
await h2Server.stop();
}
});
});
async
function resetConnections() {
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));
}
async
function openChannelAndGetTimings(uri) {
let chan = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal:
true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
let timedChannel = chan.QueryInterface(Ci.nsITimedChannel);
let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
let transportStatuses = [];
let activityDistributor = Cc[
"@mozilla.org/network/http-activity-distributor;1"
].getService(Ci.nsIHttpActivityDistributor);
let observer = {
observeActivity(aHttpChannel, aActivityType, aActivitySubtype) {
if (
aActivityType ===
Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_SOCKET_TRANSPORT
) {
try {
let otherChan = aHttpChannel.QueryInterface(Ci.nsIChannel);
if (otherChan.URI.spec === uri) {
transportStatuses.push(aActivitySubtype);
}
}
catch (e) {}
}
},
};
activityDistributor.addObserver(observer);
await
new Promise(resolve => {
chan.asyncOpen(
new ChannelListener((_req, _buf) => resolve(),
null, CL_ALLOW_UNKNOWN_CL)
);
});
activityDistributor.removeObserver(observer);
return { timedChannel, internalChannel, transportStatuses };
}
function logTimings(timedChannel) {
info(`domainLookupStartTime=${timedChannel.domainLookupStartTime}`);
info(`domainLookupEndTime=${timedChannel.domainLookupEndTime}`);
info(`connectStartTime=${timedChannel.connectStartTime}`);
info(`tcpConnectEndTime=${timedChannel.tcpConnectEndTime}`);
info(`secureConnectionStartTime=${timedChannel.secureConnectionStartTime}`);
info(`connectEndTime=${timedChannel.connectEndTime}`);
}
function assertTimingsSet(timedChannel) {
Assert.greater(
timedChannel.domainLookupStartTime,
0,
"domainLookupStartTime should be set"
);
Assert.greater(
timedChannel.domainLookupEndTime,
0,
"domainLookupEndTime should be set"
);
Assert.greater(
timedChannel.connectStartTime,
0,
"connectStartTime should be set"
);
Assert.greater(
timedChannel.connectEndTime,
0,
"connectEndTime should be set"
);
Assert.greater(
timedChannel.secureConnectionStartTime,
0,
"secureConnectionStartTime should be set"
);
}
function assertTransportStatusPresent(transportStatuses, expected) {
for (let status of expected) {
Assert.ok(
transportStatuses.includes(status),
`transport status
0x${status.toString(
16)} should be present`
);
}
}
function assertTransportStatusUnique(transportStatuses, expected) {
for (let status of expected) {
Assert.equal(
transportStatuses.filter(s => s === status).length,
1,
`transport status
0x${status.toString(
16)} should appear exactly once`
);
}
}
function assertTimingsOrder(timedChannel) {
Assert.lessOrEqual(
timedChannel.domainLookupStartTime,
timedChannel.domainLookupEndTime,
"domainLookupStart <= domainLookupEnd"
);
Assert.lessOrEqual(
timedChannel.connectStartTime,
timedChannel.secureConnectionStartTime,
"connectStart <= secureConnectionStart"
);
Assert.lessOrEqual(
timedChannel.secureConnectionStartTime,
timedChannel.connectEndTime,
"secureConnectionStart <= connectEnd"
);
}
add_task(async
function test_tcp_timings_speculative_enabled() {
await resetConnections();
Services.prefs.setIntPref(
"network.http.speculative-parallel-limit",
6);
let host =
"foo.example.com";
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" }],
});
let { timedChannel, internalChannel, transportStatuses } =
await openChannelAndGetTimings(`https:
//${host}:${h2Server.port()}/`);
logTimings(timedChannel);
assertTimingsSet(timedChannel);
assertTimingsOrder(timedChannel);
Assert.ok(
internalChannel.remoteAddress ===
"127.0.0.1" ||
internalChannel.remoteAddress ===
"::1",
`remoteAddress should be
127.
0.
0.
1 or ::
1, got ${internalChannel.remoteAddress}`
);
let expectedStatuses = [
Ci.nsISocketTransport.STATUS_RESOLVING,
Ci.nsISocketTransport.STATUS_RESOLVED,
Ci.nsISocketTransport.STATUS_CONNECTING_TO,
Ci.nsISocketTransport.STATUS_CONNECTED_TO,
];
assertTransportStatusPresent(transportStatuses, expectedStatuses);
assertTransportStatusUnique(transportStatuses, expectedStatuses);
});
add_task(async
function test_tcp_timings_speculative_disabled() {
await resetConnections();
Services.prefs.setIntPref(
"network.http.speculative-parallel-limit",
0);
let host =
"alt1.example.com";
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" }],
});
let { timedChannel, internalChannel, transportStatuses } =
await openChannelAndGetTimings(`https:
//${host}:${h2Server.port()}/`);
logTimings(timedChannel);
assertTimingsSet(timedChannel);
assertTimingsOrder(timedChannel);
Assert.ok(
internalChannel.remoteAddress ===
"127.0.0.1" ||
internalChannel.remoteAddress ===
"::1",
`remoteAddress should be
127.
0.
0.
1 or ::
1, got ${internalChannel.remoteAddress}`
);
let expectedStatuses = [
Ci.nsISocketTransport.STATUS_RESOLVING,
Ci.nsISocketTransport.STATUS_RESOLVED,
Ci.nsISocketTransport.STATUS_CONNECTING_TO,
Ci.nsISocketTransport.STATUS_CONNECTED_TO,
];
assertTransportStatusPresent(transportStatuses, expectedStatuses);
assertTransportStatusUnique(transportStatuses, expectedStatuses);
});
function assertH3Timings(timedChannel) {
Assert.greater(
timedChannel.domainLookupStartTime,
0,
"domainLookupStartTime should be set"
);
Assert.greater(
timedChannel.domainLookupEndTime,
0,
"domainLookupEndTime should be set"
);
Assert.greater(
timedChannel.connectStartTime,
0,
"connectStartTime should be set"
);
Assert.greater(
timedChannel.connectEndTime,
0,
"connectEndTime should be set"
);
Assert.lessOrEqual(
timedChannel.domainLookupStartTime,
timedChannel.domainLookupEndTime,
"domainLookupStart <= domainLookupEnd"
);
Assert.lessOrEqual(
timedChannel.domainLookupEndTime,
timedChannel.connectStartTime,
"domainLookupEnd <= connectStart"
);
// For HTTP/3, connectStart == secureConnectionStart.
Assert.equal(
timedChannel.connectStartTime,
timedChannel.secureConnectionStartTime,
"connectStart == secureConnectionStart for HTTP/3"
);
}
async
function do_test_h3_timings(host) {
await resetConnections();
let { timedChannel, internalChannel, transportStatuses } =
await openChannelAndGetTimings(`https:
//${host}/`);
logTimings(timedChannel);
assertH3Timings(timedChannel);
Assert.equal(
internalChannel.remoteAddress,
"127.0.0.1",
"remoteAddress should be 127.0.0.1 for HTTP/3"
);
let expectedStatuses = [
Ci.nsISocketTransport.STATUS_RESOLVING,
Ci.nsISocketTransport.STATUS_RESOLVED,
];
assertTransportStatusPresent(transportStatuses, expectedStatuses);
assertTransportStatusUnique(transportStatuses, expectedStatuses);
}
add_task(async
function test_h3_timings() {
let h3Port = Services.env.get(
"MOZHTTP3_PORT");
Assert.notEqual(h3Port,
null);
Assert.notEqual(h3Port,
"");
await resetConnections();
Services.prefs.setBoolPref(
"network.http.http3.enable",
true);
Services.prefs.setBoolPref(
"network.dns.disableIPv6",
true);
let host =
"alt2.example.com";
Services.prefs.setCharPref(
"network.http.http3.alt-svc-mapping-for-testing",
`${host};h3=:${h3Port}`
);
await trrServer.registerDoHAnswers(host,
"A", {
answers: [
{ name: host, ttl:
55, type:
"A", flush:
false, data:
"127.0.0.1" },
],
});
Services.prefs.setIntPref(
"network.http.speculative-parallel-limit",
0);
await do_test_h3_timings(host);
Services.prefs.setIntPref(
"network.http.speculative-parallel-limit",
6);
await do_test_h3_timings(host);
Services.prefs.clearUserPref(
"network.http.http3.enable");
Services.prefs.clearUserPref(
"network.dns.disableIPv6");
Services.prefs.clearUserPref(
"network.http.http3.alt-svc-mapping-for-testing"
);
});