Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* 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/. */
// Bug 2074493: an HTTP/2 request sent as 0-RTT whose first flight is larger
// than the server's early data allowance. Once the allowance is used up NSS
// accepts no more bytes until the handshake completes; the rest of the request
// must be sent after the handshake instead of the connection being treated as
// closed.
//
// ZeroRttAcceptServer is started with MOZ_ZERORTT_MAX_EARLY_DATA, so its
// session tickets allow only a small amount of early data. The server reports
// each HEADERS frame it receives as "early" (on a connection that accepted
// 0-RTT) or "std" (on a connection that did not).
"use strict";
const { HttpServer } = ChromeUtils.importESModule(
);
var { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
// The server accepts 0-RTT for the first host and refuses it for the second.
const ACCEPT_HOST = "0rtt-accept-h2.example.com";
const REJECT_HOST = "0rtt-reject-h2.example.com";
const MAX_EARLY_DATA = 512;
let callbackServer;
let gServerStarted = false;
let earlyCount = 0;
let stdCount = 0;
function callbackHandler(metadata) {
if (metadata.path === "/callback/request/early") {
earlyCount++;
} else if (metadata.path === "/callback/request/std") {
stdCount++;
}
}
add_setup(
{
skip_if: () => AppConstants.MOZ_SYSTEM_NSS,
},
async () => {
callbackServer = new HttpServer();
callbackServer.registerPrefixHandler("/callback/", callbackHandler);
callbackServer.start(-1);
Services.env.set(
"MOZ_ZERORTT_ACCEPT_CALLBACK_PORT",
callbackServer.identity.primaryPort
);
Services.env.set("MOZ_TLS_SERVER_0RTT", "1");
Services.env.set("MOZ_ZERORTT_MAX_EARLY_DATA", `${MAX_EARLY_DATA}`);
const started = await asyncStartTLSTestServer(
"ZeroRttAcceptServer",
"../../../security/manager/ssl/tests/unit/test_faulty_server"
);
Services.env.set("MOZ_ZERORTT_MAX_EARLY_DATA", "");
if (!started) {
return;
}
gServerStarted = true;
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
Services.prefs.setBoolPref("network.ssl_tokens_cache_enabled", true);
Services.prefs.setBoolPref("network.http.http3.enable", false);
// The test server listens on IPv4 only; an IPv6 attempt would use up the
// single-use session ticket.
Services.prefs.setBoolPref("network.dns.disableIPv6", true);
Services.prefs.setCharPref(
"network.dns.localDomains",
`${ACCEPT_HOST},${REJECT_HOST}`
);
registerCleanupFunction(async () => {
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
Services.prefs.clearUserPref("network.ssl_tokens_cache_enabled");
Services.prefs.clearUserPref("network.http.http3.enable");
Services.prefs.clearUserPref("network.dns.disableIPv6");
Services.prefs.clearUserPref("network.dns.localDomains");
if (callbackServer) {
await callbackServer.stop();
}
});
}
);
// A header value HPACK can't compress much, several times the early data
// allowance.
function bigHeaderValue() {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let seed = 7;
let value = "";
for (let i = 0; i < 4096; i++) {
seed = (seed * 1103515245 + 12345) % 2147483648;
value += chars[seed % chars.length];
}
return value;
}
function fetch(aHost, aBigHeader) {
return new Promise(resolve => {
let chan = NetUtil.newChannel({
uri: `https://${aHost}:8443/`,
loadUsingSystemPrincipal: true,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
if (aBigHeader) {
chan.setRequestHeader("X-Big", bigHeaderValue(), false);
}
chan.asyncOpen({
QueryInterface: ChromeUtils.generateQI([
"nsIStreamListener",
"nsIRequestObserver",
]),
onStartRequest() {},
onDataAvailable(request, stream, offset, count) {
read_stream(stream, count);
},
onStopRequest(request, status) {
let httpChan = request.QueryInterface(Ci.nsIHttpChannel);
let responseStatus = 0;
let protocol = "";
try {
responseStatus = httpChan.responseStatus;
protocol = httpChan.protocolVersion;
} catch (e) {}
resolve({ status: responseStatus, protocol, result: status });
},
});
});
}
async function closeConnections() {
Services.obs.notifyObservers(null, "net:cancel-all-connections");
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 200));
}
async function runTest(aHost) {
let tokenCache = Cc["@mozilla.org/network/ssl-tokens-cache;1"].getService(
Ci.nsISSLTokensCache
);
await tokenCache.asyncClearSSLExternalAndInternalSessionCache();
earlyCount = 0;
stdCount = 0;
// Full handshake, which gives us a session ticket allowing 0-RTT.
let first = await fetch(aHost, false);
Assert.equal(first.status, 200, "first fetch succeeds");
Assert.equal(first.protocol, "h2", "first fetch uses h2");
Assert.equal(stdCount, 1, "first fetch arrived without early data");
// Let the NewSessionTicket arrive and the anti-replay window pass.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 1500));
await closeConnections();
// Resumed with 0-RTT; the request does not fit in the early data allowance.
let second = await fetch(aHost, true);
info(
`second fetch: result=0x${second.result.toString(16)} early=${earlyCount} std=${stdCount}`
);
Assert.equal(second.result, Cr.NS_OK, "second fetch succeeds");
Assert.equal(second.status, 200, "second fetch gets a response");
if (aHost == ACCEPT_HOST) {
Assert.equal(
earlyCount,
1,
"second fetch completed on the connection that accepted 0-RTT"
);
Assert.equal(
stdCount,
1,
"second fetch was not retried on another connection"
);
} else {
// The server refuses the early data, so the whole request is resent once
// the handshake completes.
Assert.equal(earlyCount, 0, "no request arrived as 0-RTT");
Assert.equal(stdCount, 2, "second fetch arrived after the handshake");
}
await closeConnections();
}
add_task(
{
skip_if: () => AppConstants.MOZ_SYSTEM_NSS || !gServerStarted,
},
async function test_h2_0rtt_early_data_limit_accepted() {
await runTest(ACCEPT_HOST);
}
);
add_task(
{
skip_if: () => AppConstants.MOZ_SYSTEM_NSS || !gServerStarted,
},
async function test_h2_0rtt_early_data_limit_rejected() {
await runTest(REJECT_HOST);
}
);