Bug 2068800 - Stop presenting network resets on https as SSL errors r=dom-core-reviewers,smaug,valentin
NS_ERROR_NET_RESET and NS_ERROR_NET_INTERRUPT on an https URI are not evidence
of TLS version intolerance: the heuristic applies only during the handshake,
insecure fallback was disabled in bug 1479501, and genuine TLS failures reach
nssFailure2 by their own path.
Give netReset and NS_ERROR_NET_EMPTY_RESPONSE distinct error-page configs, and
test the empty-response case with an actual empty response.
Differential Revision: https://phabricator.services.mozilla.com/D323597
This commit is contained in:
committed by
leggert@mozilla.com
parent
e986c9e88a
commit
a82f826396
@@ -98,6 +98,8 @@ support-files = [
|
||||
|
||||
["browser_aboutNetError_netInterrupt.js"]
|
||||
|
||||
["browser_aboutNetError_netReset.js"]
|
||||
|
||||
["browser_aboutNetError_searchCTA.js"]
|
||||
|
||||
["browser_aboutNetError_searchCTA_connectivity.js"]
|
||||
|
||||
@@ -3,28 +3,34 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
function startDropServer() {
|
||||
const server = Cc["@mozilla.org/network/server-socket;1"].createInstance(
|
||||
Ci.nsIServerSocket
|
||||
);
|
||||
info("Using a random port.");
|
||||
server.init(-1, true, -1);
|
||||
server.asyncListen({
|
||||
onSocketAccepted(socket, transport) {
|
||||
// Close immediately, no response sent.
|
||||
transport.close(Cr.NS_OK);
|
||||
},
|
||||
onStopListening() {},
|
||||
});
|
||||
registerCleanupFunction(() => server.close());
|
||||
return server.port;
|
||||
const { NodeHTTPServer } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/NodeServer.sys.mjs"
|
||||
);
|
||||
|
||||
// A 4xx response with no body is what nsURILoader turns into
|
||||
// NS_ERROR_NET_EMPTY_RESPONSE.
|
||||
//
|
||||
// registerPathHandler ships the handler to a separate Node process via
|
||||
// handler.toString(), so it must not close over anything from this scope;
|
||||
// pick a self-contained handler before registering it.
|
||||
async function startEmptyResponseServer(setContentLength) {
|
||||
const server = new NodeHTTPServer();
|
||||
await server.start();
|
||||
registerCleanupFunction(() => server.stop());
|
||||
const handler = setContentLength
|
||||
? (req, resp) => {
|
||||
resp.writeHead(404, { "Content-Length": "0" });
|
||||
resp.end();
|
||||
}
|
||||
: (req, resp) => {
|
||||
resp.writeHead(404);
|
||||
resp.end();
|
||||
};
|
||||
await server.registerPathHandler("/empty", handler);
|
||||
return `${server.origin()}/empty`;
|
||||
}
|
||||
|
||||
add_task(async function test_net_empty_response_copy() {
|
||||
await setSecurityCertErrorsFeltPrivacyToTrue();
|
||||
|
||||
const port = startDropServer();
|
||||
const url = `http://127.0.0.1:${port}/`;
|
||||
async function loadEmptyResponseErrorPage(url) {
|
||||
let browser, tab;
|
||||
let pageLoaded;
|
||||
await BrowserTestUtils.openNewForegroundTab(
|
||||
@@ -40,8 +46,10 @@ add_task(async function test_net_empty_response_copy() {
|
||||
|
||||
info("Loading and waiting for the net error.");
|
||||
await pageLoaded;
|
||||
return { browser, tab };
|
||||
}
|
||||
|
||||
Assert.ok("Loaded empty server response.");
|
||||
async function assertEmptyResponseCopy(browser) {
|
||||
await SpecialPowers.spawn(browser, [], async () => {
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => content?.document?.querySelector("net-error-card"),
|
||||
@@ -60,27 +68,40 @@ add_task(async function test_net_empty_response_copy() {
|
||||
);
|
||||
Assert.equal(
|
||||
netErrorCard.errorIntro.dataset.l10nId,
|
||||
"neterror-http-empty-response-description",
|
||||
"Using the 'empty response' intro."
|
||||
"fp-neterror-http-error-intro",
|
||||
"Using the HTTP error intro."
|
||||
);
|
||||
const list = netErrorCard.renderRoot.querySelector(".what-can-you-do-list");
|
||||
Assert.ok(list, "NetErrorCard has what-can-you-do list.");
|
||||
Assert.ok(
|
||||
list.querySelector('[data-l10n-id="neterror-http-error-page"]'),
|
||||
"List includes check-the-address item"
|
||||
);
|
||||
Assert.ok(
|
||||
list.querySelector('[data-l10n-id="neterror-load-error-try-again"]'),
|
||||
"List includes try-again item"
|
||||
);
|
||||
Assert.ok(
|
||||
list.querySelector('[data-l10n-id="neterror-load-error-connection"]'),
|
||||
"List includes connection item"
|
||||
);
|
||||
Assert.ok(
|
||||
list.querySelector('[data-l10n-id="neterror-load-error-firewall"]'),
|
||||
"List includes firewall item"
|
||||
);
|
||||
Assert.ok(
|
||||
ContentTaskUtils.isVisible(netErrorCard.tryAgainButton),
|
||||
"The 'Try Again' button is shown."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
add_task(async function test_net_empty_response_copy() {
|
||||
await setSecurityCertErrorsFeltPrivacyToTrue();
|
||||
|
||||
const url = await startEmptyResponseServer(true);
|
||||
const { browser, tab } = await loadEmptyResponseErrorPage(url);
|
||||
await assertEmptyResponseCopy(browser);
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_net_empty_response_copy_no_content_length() {
|
||||
await setSecurityCertErrorsFeltPrivacyToTrue();
|
||||
|
||||
const url = await startEmptyResponseServer(false);
|
||||
const { browser, tab } = await loadEmptyResponseErrorPage(url);
|
||||
await assertEmptyResponseCopy(browser);
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Closing the connection right after accepting it, before any response is
|
||||
// sent, surfaces as NS_ERROR_NET_RESET.
|
||||
function startDropServer() {
|
||||
const server = Cc["@mozilla.org/network/server-socket;1"].createInstance(
|
||||
Ci.nsIServerSocket
|
||||
);
|
||||
server.init(-1, true, -1);
|
||||
server.asyncListen({
|
||||
onSocketAccepted(socket, transport) {
|
||||
transport.close(Cr.NS_OK);
|
||||
},
|
||||
onStopListening() {},
|
||||
});
|
||||
registerCleanupFunction(() => server.close());
|
||||
return server.port;
|
||||
}
|
||||
|
||||
// nsITLSServerSocket needs a certificate with a corresponding private key
|
||||
// available. In mochitests, the certificate with the common name "Mochitest
|
||||
// client" has such a key.
|
||||
async function getTestServerCertificate() {
|
||||
const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
|
||||
Ci.nsIX509CertDB
|
||||
);
|
||||
for (const cert of await certDB.getCerts()) {
|
||||
if (cert.commonName == "Mochitest client") {
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Completes the TLS handshake, then closes the connection right after, also
|
||||
// surfacing as NS_ERROR_NET_RESET (see browser_navigation_failures.js).
|
||||
function startTLSDropServer(cert) {
|
||||
const server = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
|
||||
Ci.nsITLSServerSocket
|
||||
);
|
||||
server.init(-1, true, -1);
|
||||
server.serverCert = cert;
|
||||
|
||||
let input, output;
|
||||
const listener = {
|
||||
onSocketAccepted(socket, transport) {
|
||||
const connectionInfo = transport.securityCallbacks.getInterface(
|
||||
Ci.nsITLSServerConnectionInfo
|
||||
);
|
||||
connectionInfo.setSecurityObserver(listener);
|
||||
input = transport.openInputStream(0, 0, 0);
|
||||
output = transport.openOutputStream(0, 0, 0);
|
||||
},
|
||||
onHandshakeDone() {
|
||||
input.asyncWait(
|
||||
{
|
||||
onInputStreamReady() {
|
||||
input.close();
|
||||
output.close();
|
||||
},
|
||||
},
|
||||
0,
|
||||
0,
|
||||
Services.tm.currentThread
|
||||
);
|
||||
},
|
||||
onStopListening() {},
|
||||
};
|
||||
|
||||
server.setSessionTickets(false);
|
||||
server.asyncListen(listener);
|
||||
registerCleanupFunction(() => server.close());
|
||||
return server;
|
||||
}
|
||||
|
||||
async function loadDroppedConnectionErrorPage(url) {
|
||||
let browser, tab;
|
||||
let pageLoaded;
|
||||
await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
() => {
|
||||
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url);
|
||||
browser = gBrowser.selectedBrowser;
|
||||
tab = gBrowser.selectedTab;
|
||||
pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
|
||||
},
|
||||
false
|
||||
);
|
||||
await pageLoaded;
|
||||
return { browser, tab };
|
||||
}
|
||||
|
||||
add_task(async function test_netReset_from_dropped_connection_http() {
|
||||
const port = startDropServer();
|
||||
const { browser, tab } = await loadDroppedConnectionErrorPage(
|
||||
`http://127.0.0.1:${port}/`
|
||||
);
|
||||
|
||||
await SpecialPowers.spawn(browser, [], () => {
|
||||
Assert.ok(
|
||||
content.document.documentURI.includes("e=netReset"),
|
||||
"A dropped http connection shows the netReset error page"
|
||||
);
|
||||
});
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_netReset_from_dropped_connection_https() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
// This test fails on some platforms if we leave IPv6 enabled.
|
||||
set: [["network.dns.disableIPv6", true]],
|
||||
});
|
||||
|
||||
const certOverrideService = Cc[
|
||||
"@mozilla.org/security/certoverride;1"
|
||||
].getService(Ci.nsICertOverrideService);
|
||||
const cert = await getTestServerCertificate();
|
||||
const server = startTLSDropServer(cert);
|
||||
certOverrideService.rememberValidityOverride(
|
||||
"localhost",
|
||||
server.port,
|
||||
{},
|
||||
cert,
|
||||
true
|
||||
);
|
||||
registerCleanupFunction(() => {
|
||||
certOverrideService.clearValidityOverride("localhost", server.port, {});
|
||||
});
|
||||
|
||||
const { browser, tab } = await loadDroppedConnectionErrorPage(
|
||||
`https://localhost:${server.port}/`
|
||||
);
|
||||
|
||||
await SpecialPowers.spawn(browser, [], () => {
|
||||
Assert.ok(
|
||||
content.document.documentURI.includes("e=netReset"),
|
||||
"A dropped https connection shows the netReset error page"
|
||||
);
|
||||
});
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_netReset_error_page_elements() {
|
||||
const { browser, tab } = await loadNetErrorPage("netReset", "127.0.0.1");
|
||||
|
||||
await SpecialPowers.spawn(browser, [], async function () {
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => content?.document?.querySelector("net-error-card"),
|
||||
"Wait for net-error-card to render"
|
||||
);
|
||||
const doc = content.document;
|
||||
const netErrorCard = doc.querySelector("net-error-card").wrappedJSObject;
|
||||
await netErrorCard.getUpdateComplete();
|
||||
|
||||
Assert.equal(
|
||||
netErrorCard.errorTitle.dataset.l10nId,
|
||||
"netReset-title",
|
||||
"Using the netReset title"
|
||||
);
|
||||
Assert.equal(
|
||||
netErrorCard.errorIntro.dataset.l10nId,
|
||||
"fp-neterror-offline-intro",
|
||||
"Using the netReset intro"
|
||||
);
|
||||
const list = netErrorCard.renderRoot.querySelector(".what-can-you-do-list");
|
||||
Assert.ok(list, "NetErrorCard has what-can-you-do list");
|
||||
Assert.ok(
|
||||
list.querySelector('[data-l10n-id="neterror-load-error-try-again"]'),
|
||||
"List includes try-again item"
|
||||
);
|
||||
Assert.ok(
|
||||
list.querySelector('[data-l10n-id="neterror-load-error-connection"]'),
|
||||
"List includes connection item"
|
||||
);
|
||||
Assert.ok(
|
||||
list.querySelector('[data-l10n-id="neterror-load-error-firewall"]'),
|
||||
"List includes firewall item"
|
||||
);
|
||||
Assert.ok(
|
||||
ContentTaskUtils.isVisible(netErrorCard.tryAgainButton),
|
||||
"The 'Try Again' button is shown"
|
||||
);
|
||||
Assert.ok(
|
||||
!netErrorCard.renderRoot.querySelector(
|
||||
'[data-l10n-id="fp-cert-error-code"]'
|
||||
),
|
||||
"No error code is shown for netReset"
|
||||
);
|
||||
});
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
@@ -155,10 +155,6 @@ add_task(async function () {
|
||||
);
|
||||
|
||||
let identityMode = window.document.getElementById("identity-box").className;
|
||||
is(
|
||||
identityMode,
|
||||
"certErrorPage notSecureText",
|
||||
"identity should be 'unknown'"
|
||||
);
|
||||
is(identityMode, "unknownIdentity", "identity should be 'unknown'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3756,12 +3756,6 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
|
||||
// Display the error as a page or an alert prompt
|
||||
NS_ENSURE_FALSE(messageStr.IsEmpty(), NS_ERROR_FAILURE);
|
||||
|
||||
if ((NS_ERROR_NET_INTERRUPT == aError || NS_ERROR_NET_RESET == aError) &&
|
||||
aURI->SchemeIs("https")) {
|
||||
// Maybe TLS intolerant. Treat this as an SSL error.
|
||||
error = "nssFailure2";
|
||||
}
|
||||
|
||||
if (mBrowsingContext->GetUseErrorPages()) {
|
||||
// Display an error page
|
||||
nsresult loadedPage =
|
||||
|
||||
@@ -323,6 +323,8 @@ support-files = [
|
||||
["browser_dataURI_unique_opaque_origin.js"]
|
||||
https_first_disabled = true
|
||||
|
||||
["browser_displayLoadError_netReset.js"]
|
||||
|
||||
["browser_fall_back_to_https.js"]
|
||||
https_first_disabled = true
|
||||
skip-if = [
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Bug 2068800: a connection reset or interruption on an https URI used to be
|
||||
// relabelled as an SSL error, on the assumption that it meant TLS version
|
||||
// intolerance. Check that these errors reach their own error pages instead.
|
||||
|
||||
async function errorPageFor(browser, errorName, uri) {
|
||||
const loaded = BrowserTestUtils.browserLoaded(browser, false, uri, true);
|
||||
await SpecialPowers.spawn(browser, [errorName, uri], (name, spec) => {
|
||||
docShell.displayLoadError(Cr[name], Services.io.newURI(spec), null, null);
|
||||
});
|
||||
const internalURL = await loaded;
|
||||
return new URLSearchParams(internalURL.split("?")[1]).get("e");
|
||||
}
|
||||
|
||||
add_task(async function test_net_errors_are_not_ssl_errors() {
|
||||
await BrowserTestUtils.withNewTab("about:blank", async browser => {
|
||||
for (const [errorName, expected] of [
|
||||
["NS_ERROR_NET_RESET", "netReset"],
|
||||
["NS_ERROR_NET_INTERRUPT", "netInterrupt"],
|
||||
]) {
|
||||
for (const scheme of ["https", "http"]) {
|
||||
const shown = await errorPageFor(
|
||||
browser,
|
||||
errorName,
|
||||
`${scheme}://example.com/`
|
||||
);
|
||||
Assert.equal(
|
||||
shown,
|
||||
expected,
|
||||
`${errorName} on ${scheme} shows the ${expected} page`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -175,38 +175,6 @@ export const NET_ERRORS = [
|
||||
},
|
||||
hasNoUserFix: false,
|
||||
},
|
||||
{
|
||||
id: "netReset",
|
||||
errorCode: "NS_ERROR_NET_EMPTY_RESPONSE",
|
||||
category: "net",
|
||||
bodyTitleL10nId: "problem-with-this-site-title",
|
||||
introContent: {
|
||||
dataL10nId: "neterror-http-empty-response-description",
|
||||
dataL10nArgs: { hostname: null },
|
||||
},
|
||||
descriptionParts: DESCRIPTION_PARTS_MAP.connectionFailureDescription,
|
||||
buttons: {
|
||||
showTryAgain: true,
|
||||
showGoBack: false,
|
||||
},
|
||||
customNetError: {
|
||||
titleL10nId: "problem-with-this-site-title",
|
||||
whatCanYouDoItems(context) {
|
||||
const items = [
|
||||
"neterror-load-error-try-again",
|
||||
"neterror-load-error-connection",
|
||||
"neterror-load-error-firewall",
|
||||
];
|
||||
if (context.showOSXPermissionWarning) {
|
||||
items.push("neterror-load-osx-permission");
|
||||
}
|
||||
return items;
|
||||
},
|
||||
showErrorCode: true,
|
||||
},
|
||||
hasNoUserFix: false,
|
||||
image: NET_ERROR_ILLUSTRATIONS.noConnection,
|
||||
},
|
||||
{
|
||||
id: "nssBadCert",
|
||||
errorCode: "nssBadCert",
|
||||
@@ -305,6 +273,37 @@ export const NET_ERRORS = [
|
||||
hasNoUserFix: false,
|
||||
image: NET_ERROR_ILLUSTRATIONS.noConnection,
|
||||
},
|
||||
{
|
||||
id: "netReset",
|
||||
errorCode: "netReset",
|
||||
category: "net",
|
||||
bodyTitleL10nId: "netReset-title",
|
||||
introContent: {
|
||||
dataL10nId: "fp-neterror-offline-intro",
|
||||
dataL10nArgs: { hostname: null },
|
||||
},
|
||||
descriptionParts: DESCRIPTION_PARTS_MAP.connectionFailureDescription,
|
||||
buttons: {
|
||||
showTryAgain: true,
|
||||
showGoBack: false,
|
||||
},
|
||||
customNetError: {
|
||||
titleL10nId: "netReset-title",
|
||||
whatCanYouDoItems(context) {
|
||||
const items = [
|
||||
"neterror-load-error-try-again",
|
||||
"neterror-load-error-connection",
|
||||
"neterror-load-error-firewall",
|
||||
];
|
||||
if (context.showOSXPermissionWarning) {
|
||||
items.push("neterror-load-osx-permission");
|
||||
}
|
||||
return items;
|
||||
},
|
||||
},
|
||||
hasNoUserFix: false,
|
||||
image: NET_ERROR_ILLUSTRATIONS.noConnection,
|
||||
},
|
||||
{
|
||||
id: "netTimeout",
|
||||
errorCode: "netTimeout",
|
||||
|
||||
@@ -99,9 +99,6 @@ neterror-load-osx-permission = If you are trying to load a local network page, p
|
||||
|
||||
neterror-http-error-page = Check to make sure you’ve typed the website address correctly.
|
||||
neterror-http-empty-response = Check to make sure you’ve typed the website address correctly and try again in a few moments.
|
||||
# Variables:
|
||||
# $hostname (String) - Hostname of the website to which the user was trying to connect.
|
||||
neterror-http-empty-response-description = { $hostname } sent back an empty page.
|
||||
|
||||
neterror-captive-portal = You must log in to this network before you can access the internet.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user