fune/toolkit/content/aboutNetError.mjs
Valentin Gosu c7a231e018 Bug 1848304 - A bad TRR URL shows unexpected error in TRR mode3 error page r=necko-reviewers,fluent-reviewers,flod,kershaw
This patch adds a new TRR skip reason used when the TRR request is made
with an invalid URL. Normally this is a URL that doesn't parse, but if
the URL is a non-HTTPS URL, then the TRR service will just use an empty
string as the URL, leading to the same failure to parse it.

This skip reason will be reported when the page load is triggered.
We should report the cause to the user instead of just saying it failed
because of an "Unexpected problem".

Differential Revision: https://phabricator.services.mozilla.com/D185991
2023-08-14 12:45:10 +00:00

1559 lines
50 KiB
JavaScript

/* 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/. */
/* eslint-env mozilla/remote-page */
/* eslint-disable import/no-unassigned-import */
import {
parse,
pemToDER,
} from "chrome://global/content/certviewer/certDecoder.mjs";
const formatter = new Intl.DateTimeFormat();
const HOST_NAME = getHostName();
function getHostName() {
try {
return new URL(RPMGetInnerMostURI(document.location.href)).hostname;
} catch (error) {
console.error("Could not parse URL", error);
}
return "";
}
// Used to check if we have a specific localized message for an error.
const KNOWN_ERROR_TITLE_IDS = new Set([
// Error titles:
"connectionFailure-title",
"deniedPortAccess-title",
"dnsNotFound-title",
"dns-not-found-trr-only-title2",
"fileNotFound-title",
"fileAccessDenied-title",
"generic-title",
"captivePortal-title",
"malformedURI-title",
"netInterrupt-title",
"notCached-title",
"netOffline-title",
"contentEncodingError-title",
"unsafeContentType-title",
"netReset-title",
"netTimeout-title",
"unknownProtocolFound-title",
"proxyConnectFailure-title",
"proxyResolveFailure-title",
"redirectLoop-title",
"unknownSocketType-title",
"nssFailure2-title",
"csp-xfo-error-title",
"corruptedContentError-title",
"sslv3Used-title",
"inadequateSecurityError-title",
"blockedByPolicy-title",
"clockSkewError-title",
"networkProtocolError-title",
"nssBadCert-title",
"nssBadCert-sts-title",
"certerror-mitm-title",
]);
/* The error message IDs from nsserror.ftl get processed into
* aboutNetErrorCodes.js which is loaded before we are: */
/* global KNOWN_ERROR_MESSAGE_IDS */
const ERROR_MESSAGES_FTL = "toolkit/neterror/nsserrors.ftl";
// The following parameters are parsed from the error URL:
// e - the error code
// s - custom CSS class to allow alternate styling/favicons
// d - error description
// captive - "true" to indicate we're behind a captive portal.
// Any other value is ignored.
// Note that this file uses document.documentURI to get
// the URL (with the format from above). This is because
// document.location.href gets the current URI off the docshell,
// which is the URL displayed in the location bar, i.e.
// the URI that the user attempted to load.
let searchParams = new URLSearchParams(document.documentURI.split("?")[1]);
let gErrorCode = searchParams.get("e");
let gIsCertError = gErrorCode == "nssBadCert";
let gHasSts = gIsCertError && getCSSClass() === "badStsCert";
// If the location of the favicon changes, FAVICON_CERTERRORPAGE_URL and/or
// FAVICON_ERRORPAGE_URL in toolkit/components/places/nsFaviconService.idl
// should also be updated.
document.getElementById("favicon").href =
gIsCertError || gErrorCode == "nssFailure2"
? "chrome://global/skin/icons/warning.svg"
: "chrome://global/skin/icons/info.svg";
function getCSSClass() {
return searchParams.get("s");
}
function getDescription() {
return searchParams.get("d");
}
function isCaptive() {
return searchParams.get("captive") == "true";
}
/**
* We don't actually know what the MitM is called (since we don't
* maintain a list), so we'll try and display the common name of the
* root issuer to the user. In the worst case they are as clueless as
* before, in the best case this gives them an actionable hint.
* This may be revised in the future.
*/
function getMitmName(failedCertInfo) {
return failedCertInfo.issuerCommonName;
}
function retryThis(buttonEl) {
RPMSendAsyncMessage("Browser:EnableOnlineMode");
buttonEl.disabled = true;
}
function showPrefChangeContainer() {
const panel = document.getElementById("prefChangeContainer");
panel.hidden = false;
document.getElementById("netErrorButtonContainer").hidden = true;
document
.getElementById("prefResetButton")
.addEventListener("click", function resetPreferences() {
RPMSendAsyncMessage("Browser:ResetSSLPreferences");
});
setFocus("#prefResetButton", "beforeend");
}
function toggleCertErrorDebugInfoVisibility(shouldShow) {
let debugInfo = document.getElementById("certificateErrorDebugInformation");
let copyButton = document.getElementById("copyToClipboardTop");
if (shouldShow === undefined) {
shouldShow = debugInfo.hidden;
}
debugInfo.hidden = !shouldShow;
if (shouldShow) {
copyButton.scrollIntoView({ block: "start", behavior: "smooth" });
copyButton.focus();
}
}
function setupAdvancedButton() {
// Get the hostname and add it to the panel
var panel = document.getElementById("badCertAdvancedPanel");
// Register click handler for the weakCryptoAdvancedPanel
document
.getElementById("advancedButton")
.addEventListener("click", togglePanelVisibility);
function togglePanelVisibility() {
if (panel.hidden) {
// Reveal
revealAdvancedPanelSlowlyAsync();
// send event to trigger telemetry ping
document.dispatchEvent(
new CustomEvent("AboutNetErrorUIExpanded", { bubbles: true })
);
} else {
// Hide
panel.hidden = true;
}
}
if (getCSSClass() == "expertBadCert") {
revealAdvancedPanelSlowlyAsync();
}
}
async function revealAdvancedPanelSlowlyAsync() {
const badCertAdvancedPanel = document.getElementById("badCertAdvancedPanel");
const exceptionDialogButton = document.getElementById(
"exceptionDialogButton"
);
// Toggling the advanced panel must ensure that the debugging
// information panel is hidden as well, since it's opened by the
// error code link in the advanced panel.
toggleCertErrorDebugInfoVisibility(false);
// Reveal, but disabled (and grayed-out) for 3.0s.
badCertAdvancedPanel.hidden = false;
exceptionDialogButton.disabled = true;
// -
if (exceptionDialogButton.resetReveal) {
exceptionDialogButton.resetReveal(); // Reset if previous is pending.
}
let wasReset = false;
exceptionDialogButton.resetReveal = () => {
wasReset = true;
};
// Wait for 10 frames to ensure that the warning text is rendered
// and gets all the way to the screen for the user to read it.
// This is only ~0.160s at 60Hz, so it's not too much extra time that we're
// taking to ensure that we're caught up with rendering, on top of the
// (by default) whole second(s) we're going to wait based on the
// security.dialog_enable_delay pref.
// The catching-up to rendering is the important part, not the
// N-frame-delay here.
for (let i = 0; i < 10; i++) {
await new Promise(requestAnimationFrame);
}
// Wait another Nms (default: 1000) for the user to be very sure. (Sorry speed readers!)
const securityDelayMs = RPMGetIntPref("security.dialog_enable_delay", 1000);
await new Promise(go => setTimeout(go, securityDelayMs));
if (wasReset) {
return;
}
// Enable and un-gray-out.
exceptionDialogButton.disabled = false;
}
function disallowCertOverridesIfNeeded() {
// Disallow overrides if this is a Strict-Transport-Security
// host and the cert is bad (STS Spec section 7.3) or if the
// certerror is in a frame (bug 633691).
if (gHasSts || window != top) {
document.getElementById("exceptionDialogButton").hidden = true;
}
if (gHasSts) {
const stsExplanation = document.getElementById("badStsCertExplanation");
document.l10n.setAttributes(
stsExplanation,
"certerror-what-should-i-do-bad-sts-cert-explanation",
{ hostname: HOST_NAME }
);
stsExplanation.hidden = false;
document.l10n.setAttributes(
document.getElementById("returnButton"),
"neterror-return-to-previous-page-button"
);
document.l10n.setAttributes(
document.getElementById("advancedPanelReturnButton"),
"neterror-return-to-previous-page-button"
);
}
}
function recordTRREventTelemetry(
warningPageType,
trrMode,
trrDomain,
skipReason
) {
RPMRecordTelemetryEvent(
"security.doh.neterror",
"load",
"dohwarning",
warningPageType,
{
mode: trrMode,
provider_key: trrDomain,
skip_reason: skipReason,
}
);
const netErrorButtonDiv = document.getElementById("netErrorButtonContainer");
const buttons = netErrorButtonDiv.querySelectorAll("button");
for (let b of buttons) {
b.addEventListener("click", function (e) {
let target = e.originalTarget;
let telemetryId = target.dataset.telemetryId;
RPMRecordTelemetryEvent(
"security.doh.neterror",
"click",
telemetryId,
warningPageType,
{
mode: trrMode,
provider_key: trrDomain,
skip_reason: skipReason,
}
);
});
}
}
function initPage() {
// We show an offline support page in case of a system-wide error,
// when a user cannot connect to the internet and access the SUMO website.
// For example, clock error, which causes certerrors across the web or
// a security software conflict where the user is unable to connect
// to the internet.
// The URL that prompts us to show an offline support page should have the following
// format: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/supportPageSlug",
// so we can extract the support page slug.
let baseURL = RPMGetFormatURLPref("app.support.baseURL");
if (document.location.href.startsWith(baseURL)) {
let supportPageSlug = document.location.pathname.split("/").pop();
RPMSendAsyncMessage("DisplayOfflineSupportPage", {
supportPageSlug,
});
}
const className = getCSSClass();
if (className) {
document.body.classList.add(className);
}
const isTRROnlyFailure = gErrorCode == "dnsNotFound" && RPMIsTRROnlyFailure();
let isNativeFallbackWarning = false;
if (RPMGetBoolPref("network.trr.display_fallback_warning")) {
isNativeFallbackWarning =
gErrorCode == "dnsNotFound" && RPMIsNativeFallbackFailure();
}
const docTitle = document.querySelector("title");
const bodyTitle = document.querySelector(".title-text");
const shortDesc = document.getElementById("errorShortDesc");
if (gIsCertError) {
const isStsError = window !== window.top || gHasSts;
const errArgs = { hostname: HOST_NAME };
if (isCaptive()) {
document.l10n.setAttributes(
docTitle,
"neterror-captive-portal-page-title"
);
document.l10n.setAttributes(bodyTitle, "captivePortal-title");
document.l10n.setAttributes(
shortDesc,
"neterror-captive-portal",
errArgs
);
initPageCaptivePortal();
} else {
if (isStsError) {
document.l10n.setAttributes(docTitle, "certerror-sts-page-title");
document.l10n.setAttributes(bodyTitle, "nssBadCert-sts-title");
document.l10n.setAttributes(shortDesc, "certerror-sts-intro", errArgs);
} else {
document.l10n.setAttributes(docTitle, "certerror-page-title");
document.l10n.setAttributes(bodyTitle, "nssBadCert-title");
document.l10n.setAttributes(shortDesc, "certerror-intro", errArgs);
}
initPageCertError();
}
initCertErrorPageActions();
setTechnicalDetailsOnCertError();
return;
}
document.body.classList.add("neterror");
let longDesc = document.getElementById("errorLongDesc");
const tryAgain = document.getElementById("netErrorButtonContainer");
tryAgain.hidden = false;
const learnMore = document.getElementById("learnMoreContainer");
const learnMoreLink = document.getElementById("learnMoreLink");
learnMoreLink.setAttribute("href", baseURL + "connection-not-secure");
let pageTitleId = "neterror-page-title";
let bodyTitleId = gErrorCode + "-title";
switch (gErrorCode) {
case "blockedByPolicy":
pageTitleId = "neterror-blocked-by-policy-page-title";
document.body.classList.add("blocked");
// Remove the "Try again" button from pages that don't need it.
// For pages blocked by policy, trying again won't help.
tryAgain.hidden = true;
break;
case "cspBlocked":
case "xfoBlocked": {
bodyTitleId = "csp-xfo-error-title";
// Remove the "Try again" button for XFO and CSP violations,
// since it's almost certainly useless. (Bug 553180)
tryAgain.hidden = true;
// Adding a button for opening websites blocked for CSP and XFO violations
// in a new window. (Bug 1461195)
document.getElementById("errorShortDesc").hidden = true;
document.l10n.setAttributes(longDesc, "csp-xfo-blocked-long-desc", {
hostname: HOST_NAME,
});
longDesc = null;
document.getElementById("openInNewWindowContainer").hidden = false;
const openInNewWindowButton = document.getElementById(
"openInNewWindowButton"
);
openInNewWindowButton.href = document.location.href;
// Add a learn more link
learnMore.hidden = false;
learnMoreLink.setAttribute("href", baseURL + "xframe-neterror-page");
setupBlockingReportingUI();
break;
}
case "dnsNotFound":
pageTitleId = "neterror-dns-not-found-title";
if (!isTRROnlyFailure) {
RPMCheckAlternateHostAvailable();
}
break;
case "inadequateSecurityError":
// Remove the "Try again" button from pages that don't need it.
// For HTTP/2 inadequate security, trying again won't help.
tryAgain.hidden = true;
break;
case "malformedURI":
pageTitleId = "neterror-malformed-uri-page-title";
// Remove the "Try again" button from pages that don't need it.
tryAgain.hidden = true;
break;
// Pinning errors are of type nssFailure2
case "nssFailure2": {
learnMore.hidden = false;
const errorCode = document.getNetErrorInfo().errorCodeString;
switch (errorCode) {
case "SSL_ERROR_UNSUPPORTED_VERSION":
case "SSL_ERROR_PROTOCOL_VERSION_ALERT": {
const tlsNotice = document.getElementById("tlsVersionNotice");
tlsNotice.hidden = false;
document.l10n.setAttributes(tlsNotice, "cert-error-old-tls-version");
}
// fallthrough
case "interrupted": // This happens with subresources that are above the max tls
case "SSL_ERROR_NO_CIPHERS_SUPPORTED":
case "SSL_ERROR_NO_CYPHER_OVERLAP":
case "SSL_ERROR_SSL_DISABLED":
RPMAddMessageListener("HasChangedCertPrefs", msg => {
if (msg.data.hasChangedCertPrefs) {
// Configuration overrides might have caused this; offer to reset.
showPrefChangeContainer();
}
});
RPMSendAsyncMessage("GetChangedCertPrefs");
}
break;
}
case "sslv3Used":
learnMore.hidden = false;
document.body.className = "certerror";
break;
}
if (!KNOWN_ERROR_TITLE_IDS.has(bodyTitleId)) {
console.error("No strings exist for error:", gErrorCode);
bodyTitleId = "generic-title";
}
// The TRR errors may present options that direct users to settings only available on Firefox Desktop
if (RPMIsFirefox()) {
if (isTRROnlyFailure) {
document.body.className = "certerror"; // Shows warning icon
pageTitleId = "dns-not-found-trr-only-title2";
document.l10n.setAttributes(docTitle, pageTitleId);
bodyTitleId = "dns-not-found-trr-only-title2";
document.l10n.setAttributes(bodyTitle, bodyTitleId);
shortDesc.textContent = "";
let skipReason = RPMGetTRRSkipReason();
// enable buttons
let trrExceptionButton = document.getElementById("trrExceptionButton");
trrExceptionButton.addEventListener("click", () => {
RPMSendQuery("Browser:AddTRRExcludedDomain", {
hostname: HOST_NAME,
}).then(msg => {
retryThis(trrExceptionButton);
});
});
let isTrrServerError = true;
if (RPMIsSiteSpecificTRRError()) {
// Only show the exclude button if the failure is specific to this
// domain. If the TRR server is inaccessible we don't want to allow
// the user to add an exception just for this domain.
trrExceptionButton.hidden = false;
isTrrServerError = false;
}
let trrSettingsButton = document.getElementById("trrSettingsButton");
trrSettingsButton.addEventListener("click", () => {
RPMSendAsyncMessage("OpenTRRPreferences");
});
trrSettingsButton.hidden = false;
let message = document.getElementById("trrOnlyMessage");
document.l10n.setAttributes(
message,
"neterror-dns-not-found-trr-only-reason",
{
hostname: HOST_NAME,
}
);
let descriptionTag = "neterror-dns-not-found-trr-unknown-problem";
let args = { trrDomain: RPMGetTRRDomain() };
if (
skipReason == "TRR_FAILED" ||
skipReason == "TRR_CHANNEL_DNS_FAIL" ||
skipReason == "TRR_UNKNOWN_CHANNEL_FAILURE" ||
skipReason == "TRR_NET_REFUSED" ||
skipReason == "TRR_NET_INTERRUPT" ||
skipReason == "TRR_NET_INADEQ_SEQURITY"
) {
descriptionTag = "neterror-dns-not-found-trr-only-could-not-connect";
} else if (skipReason == "TRR_TIMEOUT") {
descriptionTag = "neterror-dns-not-found-trr-only-timeout";
} else if (
skipReason == "TRR_IS_OFFLINE" ||
skipReason == "TRR_NO_CONNECTIVITY"
) {
descriptionTag = "neterror-dns-not-found-trr-offline";
} else if (
skipReason == "TRR_NO_ANSWERS" ||
skipReason == "TRR_NXDOMAIN" ||
skipReason == "TRR_RCODE_FAIL"
) {
descriptionTag = "neterror-dns-not-found-trr-unknown-host2";
} else if (
skipReason == "TRR_DECODE_FAILED" ||
skipReason == "TRR_SERVER_RESPONSE_ERR"
) {
descriptionTag = "neterror-dns-not-found-trr-server-problem";
} else if (skipReason == "TRR_BAD_URL") {
descriptionTag = "neterror-dns-not-found-bad-trr-url";
}
let trrMode = RPMGetIntPref("network.trr.mode").toString();
recordTRREventTelemetry(
"TRROnlyFailure",
trrMode,
args.trrDomain,
skipReason
);
let description = document.getElementById("trrOnlyDescription");
document.l10n.setAttributes(description, descriptionTag, args);
const trrLearnMoreContainer = document.getElementById(
"trrLearnMoreContainer"
);
trrLearnMoreContainer.hidden = false;
let trrOnlyLearnMoreLink = document.getElementById(
"trrOnlylearnMoreLink"
);
if (isTrrServerError) {
// Go to DoH settings page
trrOnlyLearnMoreLink.href = "about:preferences#privacy-doh";
trrOnlyLearnMoreLink.addEventListener("click", event => {
event.preventDefault();
RPMSendAsyncMessage("OpenTRRPreferences");
RPMRecordTelemetryEvent(
"security.doh.neterror",
"click",
"settings_button",
"TRROnlyFailure",
{
mode: trrMode,
provider_key: args.trrDomain,
skip_reason: skipReason,
}
);
});
} else {
// This will be replaced at a later point with a link to an offline support page
// https://bugzilla.mozilla.org/show_bug.cgi?id=1806257
trrOnlyLearnMoreLink.href =
RPMGetFormatURLPref("network.trr_ui.skip_reason_learn_more_url") +
skipReason.toLowerCase().replaceAll("_", "-");
}
let div = document.getElementById("trrOnlyContainer");
div.hidden = false;
return;
} else if (isNativeFallbackWarning) {
showNativeFallbackWarning();
return;
}
}
document.l10n.setAttributes(docTitle, pageTitleId);
document.l10n.setAttributes(bodyTitle, bodyTitleId);
shortDesc.textContent = getDescription();
setFocus("#netErrorButtonContainer > .try-again");
if (longDesc) {
const parts = getNetErrorDescParts();
setNetErrorMessageFromParts(longDesc, parts);
}
setNetErrorMessageFromCode();
}
function showNativeFallbackWarning() {
const docTitle = document.querySelector("title");
const bodyTitle = document.querySelector(".title-text");
const shortDesc = document.getElementById("errorShortDesc");
let pageTitleId = "neterror-page-title";
let bodyTitleId = gErrorCode + "-title";
document.body.className = "certerror"; // Shows warning icon
pageTitleId = "dns-not-found-native-fallback-title2";
document.l10n.setAttributes(docTitle, pageTitleId);
bodyTitleId = "dns-not-found-native-fallback-title2";
document.l10n.setAttributes(bodyTitle, bodyTitleId);
shortDesc.textContent = "";
let nativeFallbackIgnoreButton = document.getElementById(
"nativeFallbackIgnoreButton"
);
nativeFallbackIgnoreButton.addEventListener("click", () => {
RPMSetPref("network.trr.display_fallback_warning", false);
retryThis(nativeFallbackIgnoreButton);
});
let continueThisTimeButton = document.getElementById(
"nativeFallbackContinueThisTimeButton"
);
continueThisTimeButton.addEventListener("click", () => {
RPMSetTRRDisabledLoadFlags();
document.location.reload();
});
continueThisTimeButton.hidden = false;
nativeFallbackIgnoreButton.hidden = false;
let message = document.getElementById("nativeFallbackMessage");
document.l10n.setAttributes(
message,
"neterror-dns-not-found-native-fallback-reason",
{
hostname: HOST_NAME,
}
);
let skipReason = RPMGetTRRSkipReason();
let descriptionTag = "neterror-dns-not-found-trr-unknown-problem";
let args = { trrDomain: RPMGetTRRDomain() };
if (skipReason.includes("HEURISTIC_TRIPPED")) {
descriptionTag = "neterror-dns-not-found-native-fallback-heuristic";
} else if (skipReason == "TRR_NOT_CONFIRMED") {
descriptionTag = "neterror-dns-not-found-native-fallback-not-confirmed2";
}
let description = document.getElementById("nativeFallbackDescription");
document.l10n.setAttributes(description, descriptionTag, args);
let learnMoreContainer = document.getElementById(
"nativeFallbackLearnMoreContainer"
);
learnMoreContainer.hidden = false;
let learnMoreLink = document.getElementById("nativeFallbackLearnMoreLink");
learnMoreLink.href =
RPMGetFormatURLPref("network.trr_ui.skip_reason_learn_more_url") +
skipReason.toLowerCase().replaceAll("_", "-");
let div = document.getElementById("nativeFallbackContainer");
div.hidden = false;
recordTRREventTelemetry(
"NativeFallbackWarning",
RPMGetIntPref("network.trr.mode").toString(),
args.trrDomain,
skipReason
);
}
/**
* Builds HTML elements from `parts` and appends them to `parentElement`.
*
* @param {HTMLElement} parentElement
* @param {Array<["li" | "p" | "span", string, Record<string, string> | undefined]>} parts
*/
function setNetErrorMessageFromParts(parentElement, parts) {
let list = null;
for (let [tag, l10nId, l10nArgs] of parts) {
const elem = document.createElement(tag);
elem.dataset.l10nId = l10nId;
if (l10nArgs) {
elem.dataset.l10nArgs = JSON.stringify(l10nArgs);
}
if (tag === "li") {
if (!list) {
list = document.createElement("ul");
parentElement.appendChild(list);
}
list.appendChild(elem);
} else {
if (list) {
list = null;
}
parentElement.appendChild(elem);
}
}
}
/**
* Returns an array of tuples determining the parts of an error message:
* - HTML tag name
* - l10n id
* - l10n args (optional)
*
* @returns { Array<["li" | "p" | "span", string, Record<string, string> | undefined]> }
*/
function getNetErrorDescParts() {
switch (gErrorCode) {
case "connectionFailure":
case "netInterrupt":
case "netReset":
case "netTimeout":
return [
["li", "neterror-load-error-try-again"],
["li", "neterror-load-error-connection"],
["li", "neterror-load-error-firewall"],
];
case "blockedByPolicy":
case "deniedPortAccess":
case "malformedURI":
return [];
case "captivePortal":
return [["p", ""]];
case "contentEncodingError":
return [["li", "neterror-content-encoding-error"]];
case "corruptedContentErrorv2":
return [
["p", "neterror-corrupted-content-intro"],
["li", "neterror-corrupted-content-contact-website"],
];
case "dnsNotFound":
return [
["span", "neterror-dns-not-found-hint-header"],
["li", "neterror-dns-not-found-hint-try-again"],
["li", "neterror-dns-not-found-hint-check-network"],
["li", "neterror-dns-not-found-hint-firewall"],
];
case "fileAccessDenied":
return [["li", "neterror-access-denied"]];
case "fileNotFound":
return [
["li", "neterror-file-not-found-filename"],
["li", "neterror-file-not-found-moved"],
];
case "inadequateSecurityError":
return [
["p", "neterror-inadequate-security-intro", { hostname: HOST_NAME }],
["p", "neterror-inadequate-security-code"],
];
case "mitm": {
const failedCertInfo = document.getFailedCertSecurityInfo();
const errArgs = {
hostname: HOST_NAME,
mitm: getMitmName(failedCertInfo),
};
return [["span", "certerror-mitm", errArgs]];
}
case "netOffline":
return [["li", "neterror-net-offline"]];
case "networkProtocolError":
return [
["p", "neterror-network-protocol-error-intro"],
["li", "neterror-network-protocol-error-contact-website"],
];
case "notCached":
return [
["p", "neterror-not-cached-intro"],
["li", "neterror-not-cached-sensitive"],
["li", "neterror-not-cached-try-again"],
];
case "nssFailure2":
return [
["li", "neterror-nss-failure-not-verified"],
["li", "neterror-nss-failure-contact-website"],
];
case "proxyConnectFailure":
return [
["li", "neterror-proxy-connect-failure-settings"],
["li", "neterror-proxy-connect-failure-contact-admin"],
];
case "proxyResolveFailure":
return [
["li", "neterror-proxy-resolve-failure-settings"],
["li", "neterror-proxy-resolve-failure-connection"],
["li", "neterror-proxy-resolve-failure-firewall"],
];
case "redirectLoop":
return [["li", "neterror-redirect-loop"]];
case "sslv3Used":
return [["span", "neterror-sslv3-used"]];
case "unknownProtocolFound":
return [["li", "neterror-unknown-protocol"]];
case "unknownSocketType":
return [
["li", "neterror-unknown-socket-type-psm-installed"],
["li", "neterror-unknown-socket-type-server-config"],
];
case "unsafeContentType":
return [["li", "neterror-unsafe-content-type"]];
default:
return [["p", "neterror-generic-error"]];
}
}
function setNetErrorMessageFromCode() {
let errorCode;
try {
errorCode = document.getNetErrorInfo().errorCodeString;
} catch (ex) {
// We don't have a securityInfo when this is for example a DNS error.
return;
}
let errorMessage;
if (errorCode) {
const l10nId = errorCode.replace(/_/g, "-").toLowerCase();
if (KNOWN_ERROR_MESSAGE_IDS.has(l10nId)) {
const l10n = new Localization([ERROR_MESSAGES_FTL], true);
errorMessage = l10n.formatValueSync(l10nId);
}
const shortDesc2 = document.getElementById("errorShortDesc2");
document.l10n.setAttributes(shortDesc2, "cert-error-code-prefix", {
error: errorCode,
});
} else {
console.warn("This error page has no error code in its security info");
}
let hostname = HOST_NAME;
const { port } = document.location;
if (port && port != 443) {
hostname += ":" + port;
}
const shortDesc = document.getElementById("errorShortDesc");
document.l10n.setAttributes(shortDesc, "cert-error-ssl-connection-error", {
errorMessage: errorMessage ?? errorCode ?? "",
hostname,
});
}
function setupBlockingReportingUI() {
let checkbox = document.getElementById("automaticallyReportBlockingInFuture");
let reportingAutomatic = RPMGetBoolPref(
"security.xfocsp.errorReporting.automatic"
);
checkbox.checked = !!reportingAutomatic;
checkbox.addEventListener("change", function ({ target: { checked } }) {
RPMSetPref("security.xfocsp.errorReporting.automatic", checked);
// If we're enabling reports, send a report for this failure.
if (checked) {
reportBlockingError();
}
});
let reportingEnabled = RPMGetBoolPref(
"security.xfocsp.errorReporting.enabled"
);
if (reportingEnabled) {
// Display blocking error reporting UI for XFO error and CSP error.
document.getElementById("blockingErrorReporting").hidden = false;
if (reportingAutomatic) {
reportBlockingError();
}
}
}
function reportBlockingError() {
// We only report if we are in a frame.
if (window === window.top) {
return;
}
let err = gErrorCode;
// Ensure we only deal with XFO and CSP here.
if (!["xfoBlocked", "cspBlocked"].includes(err)) {
return;
}
let xfo_header = RPMGetHttpResponseHeader("X-Frame-Options");
let csp_header = RPMGetHttpResponseHeader("Content-Security-Policy");
// Extract the 'CSP: frame-ancestors' from the CSP header.
let reg = /(?:^|\s)frame-ancestors\s([^;]*)[$]*/i;
let match = reg.exec(csp_header);
csp_header = match ? match[1] : "";
// If it's the csp error page without the CSP: frame-ancestors, this means
// this error page is not triggered by CSP: frame-ancestors. So, we bail out
// early.
if (err === "cspBlocked" && !csp_header) {
return;
}
let xfoAndCspInfo = {
error_type: err === "xfoBlocked" ? "xfo" : "csp",
xfo_header,
csp_header,
};
// Trimming the tail colon symbol.
let scheme = document.location.protocol.slice(0, -1);
RPMSendAsyncMessage("ReportBlockingError", {
scheme,
host: document.location.host,
port: parseInt(document.location.port) || -1,
path: document.location.pathname,
xfoAndCspInfo,
});
}
function initPageCaptivePortal() {
document.body.className = "captiveportal";
document.getElementById("returnButton").hidden = true;
const openButton = document.getElementById("openPortalLoginPageButton");
openButton.hidden = false;
openButton.addEventListener("click", () => {
RPMSendAsyncMessage("Browser:OpenCaptivePortalPage");
});
setFocus("#openPortalLoginPageButton");
setupAdvancedButton();
disallowCertOverridesIfNeeded();
// When the portal is freed, an event is sent by the parent process
// that we can pick up and attempt to reload the original page.
RPMAddMessageListener("AboutNetErrorCaptivePortalFreed", () => {
document.location.reload();
});
}
function initPageCertError() {
document.body.classList.add("certerror");
setFocus("#returnButton");
setupAdvancedButton();
disallowCertOverridesIfNeeded();
const hideAddExceptionButton = RPMGetBoolPref(
"security.certerror.hideAddException",
false
);
if (hideAddExceptionButton) {
document.getElementById("exceptionDialogButton").hidden = true;
}
const els = document.querySelectorAll("[data-telemetry-id]");
for (let el of els) {
el.addEventListener("click", recordClickTelemetry);
}
const failedCertInfo = document.getFailedCertSecurityInfo();
// Truncate the error code to avoid going over the allowed
// string size limit for telemetry events.
const errorCode = failedCertInfo.errorCodeString.substring(0, 40);
RPMRecordTelemetryEvent(
"security.ui.certerror",
"load",
"aboutcerterror",
errorCode,
{
has_sts: gHasSts.toString(),
is_frame: (window.parent != window).toString(),
}
);
setCertErrorDetails();
}
function recordClickTelemetry(e) {
let target = e.originalTarget;
let telemetryId = target.dataset.telemetryId;
let failedCertInfo = document.getFailedCertSecurityInfo();
// Truncate the error code to avoid going over the allowed
// string size limit for telemetry events.
let errorCode = failedCertInfo.errorCodeString.substring(0, 40);
RPMRecordTelemetryEvent(
"security.ui.certerror",
"click",
telemetryId,
errorCode,
{
has_sts: gHasSts.toString(),
is_frame: (window.parent != window).toString(),
}
);
}
function initCertErrorPageActions() {
document.getElementById(
"certErrorAndCaptivePortalButtonContainer"
).hidden = false;
document
.getElementById("returnButton")
.addEventListener("click", onReturnButtonClick);
document
.getElementById("advancedPanelReturnButton")
.addEventListener("click", onReturnButtonClick);
document
.getElementById("copyToClipboardTop")
.addEventListener("click", copyPEMToClipboard);
document
.getElementById("copyToClipboardBottom")
.addEventListener("click", copyPEMToClipboard);
document
.getElementById("exceptionDialogButton")
.addEventListener("click", addCertException);
}
function addCertException() {
const isPermanent =
!RPMIsWindowPrivate() &&
RPMGetBoolPref("security.certerrors.permanentOverride");
document.addCertException(!isPermanent).then(
() => {
location.reload();
},
err => {}
);
}
function onReturnButtonClick(e) {
RPMSendAsyncMessage("Browser:SSLErrorGoBack");
}
function copyPEMToClipboard(e) {
const errorText = document.getElementById("certificateErrorText");
navigator.clipboard.writeText(errorText.textContent);
}
async function getFailedCertificatesAsPEMString() {
let locationUrl = document.location.href;
let failedCertInfo = document.getFailedCertSecurityInfo();
let errorMessage = failedCertInfo.errorMessage;
let hasHSTS = failedCertInfo.hasHSTS.toString();
let hasHPKP = failedCertInfo.hasHPKP.toString();
let [hstsLabel, hpkpLabel, failedChainLabel] =
await document.l10n.formatValues([
{ id: "cert-error-details-hsts-label", args: { hasHSTS } },
{ id: "cert-error-details-key-pinning-label", args: { hasHPKP } },
{ id: "cert-error-details-cert-chain-label" },
]);
let certStrings = failedCertInfo.certChainStrings;
let failedChainCertificates = "";
for (let der64 of certStrings) {
let wrapped = der64.replace(/(\S{64}(?!$))/g, "$1\r\n");
failedChainCertificates +=
"-----BEGIN CERTIFICATE-----\r\n" +
wrapped +
"\r\n-----END CERTIFICATE-----\r\n";
}
let details =
locationUrl +
"\r\n\r\n" +
errorMessage +
"\r\n\r\n" +
hstsLabel +
"\r\n" +
hpkpLabel +
"\r\n\r\n" +
failedChainLabel +
"\r\n\r\n" +
failedChainCertificates;
return details;
}
function setCertErrorDetails() {
// Check if the connection is being man-in-the-middled. When the parent
// detects an intercepted connection, the page may be reloaded with a new
// error code (MOZILLA_PKIX_ERROR_MITM_DETECTED).
const failedCertInfo = document.getFailedCertSecurityInfo();
const mitmPrimingEnabled = RPMGetBoolPref(
"security.certerrors.mitm.priming.enabled"
);
if (
mitmPrimingEnabled &&
failedCertInfo.errorCodeString == "SEC_ERROR_UNKNOWN_ISSUER" &&
// Only do this check for top-level failures.
window.parent == window
) {
RPMSendAsyncMessage("Browser:PrimeMitm");
}
document.body.setAttribute("code", failedCertInfo.errorCodeString);
const learnMore = document.getElementById("learnMoreContainer");
learnMore.hidden = false;
const learnMoreLink = document.getElementById("learnMoreLink");
const baseURL = RPMGetFormatURLPref("app.support.baseURL");
learnMoreLink.href = baseURL + "connection-not-secure";
const bodyTitle = document.querySelector(".title-text");
const shortDesc = document.getElementById("errorShortDesc");
const shortDesc2 = document.getElementById("errorShortDesc2");
let whatToDoParts = null;
switch (failedCertInfo.errorCodeString) {
case "SSL_ERROR_BAD_CERT_DOMAIN":
whatToDoParts = [
["p", "certerror-bad-cert-domain-what-can-you-do-about-it"],
];
break;
case "SEC_ERROR_OCSP_INVALID_SIGNING_CERT": // FIXME - this would have thrown?
break;
case "SEC_ERROR_UNKNOWN_ISSUER":
whatToDoParts = [
["p", "certerror-unknown-issuer-what-can-you-do-about-it-website"],
[
"p",
"certerror-unknown-issuer-what-can-you-do-about-it-contact-admin",
],
];
break;
// This error code currently only exists for the Symantec distrust
// in Firefox 63, so we add copy explaining that to the user.
// In case of future distrusts of that scale we might need to add
// additional parameters that allow us to identify the affected party
// without replicating the complex logic from certverifier code.
case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED": {
document.l10n.setAttributes(
shortDesc2,
"cert-error-symantec-distrust-description",
{ hostname: HOST_NAME }
);
// FIXME - this does nothing
const adminDesc = document.createElement("p");
document.l10n.setAttributes(
adminDesc,
"cert-error-symantec-distrust-admin"
);
learnMoreLink.href = baseURL + "symantec-warning";
break;
}
case "MOZILLA_PKIX_ERROR_MITM_DETECTED": {
const autoEnabledEnterpriseRoots = RPMGetBoolPref(
"security.enterprise_roots.auto-enabled",
false
);
if (mitmPrimingEnabled && autoEnabledEnterpriseRoots) {
RPMSendAsyncMessage("Browser:ResetEnterpriseRootsPref");
}
learnMoreLink.href = baseURL + "security-error";
document.l10n.setAttributes(bodyTitle, "certerror-mitm-title");
document.l10n.setAttributes(shortDesc, "certerror-mitm", {
hostname: HOST_NAME,
mitm: getMitmName(failedCertInfo),
});
const id3 = gHasSts
? "certerror-mitm-what-can-you-do-about-it-attack-sts"
: "certerror-mitm-what-can-you-do-about-it-attack";
whatToDoParts = [
["li", "certerror-mitm-what-can-you-do-about-it-antivirus"],
["li", "certerror-mitm-what-can-you-do-about-it-corporate"],
["li", id3, { mitm: getMitmName(failedCertInfo) }],
];
break;
}
case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
learnMoreLink.href = baseURL + "security-error";
break;
// In case the certificate expired we make sure the system clock
// matches the remote-settings service (blocklist via Kinto) ping time
// and is not before the build date.
case "SEC_ERROR_EXPIRED_CERTIFICATE":
case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE":
case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE": {
learnMoreLink.href = baseURL + "time-errors";
// We check against the remote-settings server time first if available, because that allows us
// to give the user an approximation of what the correct time is.
const difference = RPMGetIntPref(
"services.settings.clock_skew_seconds",
0
);
const lastFetched =
RPMGetIntPref("services.settings.last_update_seconds", 0) * 1000;
// This is set to true later if the user's system clock is at fault for this error.
let clockSkew = false;
const now = Date.now();
const certRange = {
notBefore: failedCertInfo.certValidityRangeNotBefore,
notAfter: failedCertInfo.certValidityRangeNotAfter,
};
const approximateDate = now - difference * 1000;
// If the difference is more than a day, we last fetched the date in the last 5 days,
// and adjusting the date per the interval would make the cert valid, warn the user:
if (
Math.abs(difference) > 60 * 60 * 24 &&
now - lastFetched <= 60 * 60 * 24 * 5 * 1000 &&
certRange.notBefore < approximateDate &&
certRange.notAfter > approximateDate
) {
clockSkew = true;
// If there is no clock skew with Kinto servers, check against the build date.
// (The Kinto ping could have happened when the time was still right, or not at all)
} else {
const appBuildID = RPMGetAppBuildID();
const year = parseInt(appBuildID.substr(0, 4), 10);
const month = parseInt(appBuildID.substr(4, 2), 10) - 1;
const day = parseInt(appBuildID.substr(6, 2), 10);
const buildDate = new Date(year, month, day);
// We don't check the notBefore of the cert with the build date,
// as it is of course almost certain that it is now later than the build date,
// so we shouldn't exclude the possibility that the cert has become valid
// since the build date.
if (buildDate > now && new Date(certRange.notAfter) > buildDate) {
clockSkew = true;
}
}
if (clockSkew) {
document.body.classList.add("clockSkewError");
document.l10n.setAttributes(bodyTitle, "clockSkewError-title");
document.l10n.setAttributes(shortDesc, "neterror-clock-skew-error", {
hostname: HOST_NAME,
now,
});
document.getElementById("returnButton").hidden = true;
document.getElementById("certErrorTryAgainButton").hidden = false;
document.getElementById("advancedButton").hidden = true;
document.getElementById("advancedPanelReturnButton").hidden = true;
document.getElementById("advancedPanelTryAgainButton").hidden = false;
document.getElementById("exceptionDialogButton").hidden = true;
break;
}
document.l10n.setAttributes(shortDesc, "certerror-expired-cert-intro", {
hostname: HOST_NAME,
});
// The secondary description mentions expired certificates explicitly
// and should only be shown if the certificate has actually expired
// instead of being not yet valid.
if (failedCertInfo.errorCodeString == "SEC_ERROR_EXPIRED_CERTIFICATE") {
const sd2Id = gHasSts
? "certerror-expired-cert-sts-second-para"
: "certerror-expired-cert-second-para";
document.l10n.setAttributes(shortDesc2, sd2Id);
if (
Math.abs(difference) <= 60 * 60 * 24 &&
now - lastFetched <= 60 * 60 * 24 * 5 * 1000
) {
whatToDoParts = [
["p", "certerror-bad-cert-domain-what-can-you-do-about-it"],
];
}
}
whatToDoParts ??= [
[
"p",
"certerror-expired-cert-what-can-you-do-about-it-clock",
{ hostname: HOST_NAME, now },
],
[
"p",
"certerror-expired-cert-what-can-you-do-about-it-contact-website",
],
];
break;
}
}
if (whatToDoParts) {
setNetErrorMessageFromParts(
document.getElementById("errorWhatToDoText"),
whatToDoParts
);
document.getElementById("errorWhatToDo").hidden = false;
}
}
async function getSubjectAltNames(failedCertInfo) {
const serverCertBase64 = failedCertInfo.certChainStrings[0];
const parsed = await parse(pemToDER(serverCertBase64));
const subjectAltNamesExtension = parsed.ext.san;
const subjectAltNames = [];
if (subjectAltNamesExtension) {
for (let [key, value] of subjectAltNamesExtension.altNames) {
if (key === "DNS Name" && value.length) {
subjectAltNames.push(value);
}
}
}
return subjectAltNames;
}
// The optional argument is only here for testing purposes.
function setTechnicalDetailsOnCertError(
failedCertInfo = document.getFailedCertSecurityInfo()
) {
let technicalInfo = document.getElementById("badCertTechnicalInfo");
technicalInfo.textContent = "";
function addLabel(l10nId, args = null, attrs = null) {
let elem = document.createElement("label");
technicalInfo.appendChild(elem);
let newLines = document.createTextNode("\n \n");
technicalInfo.appendChild(newLines);
if (attrs) {
let link = document.createElement("a");
for (let [attr, value] of Object.entries(attrs)) {
link.setAttribute(attr, value);
}
elem.appendChild(link);
}
document.l10n.setAttributes(elem, l10nId, args);
}
function addErrorCodeLink() {
addLabel(
"cert-error-code-prefix-link",
{ error: failedCertInfo.errorCodeString },
{
title: failedCertInfo.errorCodeString,
id: "errorCode",
"data-l10n-name": "error-code-link",
"data-telemetry-id": "error_code_link",
href: "#certificateErrorDebugInformation",
}
);
// We're attaching the event listener to the parent element and not on
// the errorCodeLink itself because event listeners cannot be attached
// to fluent DOM overlays.
technicalInfo.addEventListener("click", event => {
if (event.target.id === "errorCode") {
event.preventDefault();
toggleCertErrorDebugInfoVisibility();
recordClickTelemetry(event);
}
});
}
let hostname = HOST_NAME;
const { port } = document.location;
if (port && port != 443) {
hostname += ":" + port;
}
switch (failedCertInfo.overridableErrorCategory) {
case "trust-error":
switch (failedCertInfo.errorCodeString) {
case "MOZILLA_PKIX_ERROR_MITM_DETECTED":
addLabel("cert-error-mitm-intro");
addLabel("cert-error-mitm-mozilla");
addLabel("cert-error-mitm-connection");
break;
case "SEC_ERROR_UNKNOWN_ISSUER":
addLabel("cert-error-trust-unknown-issuer-intro");
addLabel("cert-error-trust-unknown-issuer", { hostname });
break;
case "SEC_ERROR_CA_CERT_INVALID":
addLabel("cert-error-intro", { hostname });
addLabel("cert-error-trust-cert-invalid");
break;
case "SEC_ERROR_UNTRUSTED_ISSUER":
addLabel("cert-error-intro", { hostname });
addLabel("cert-error-trust-untrusted-issuer");
break;
case "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED":
addLabel("cert-error-intro", { hostname });
addLabel("cert-error-trust-signature-algorithm-disabled");
break;
case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
addLabel("cert-error-intro", { hostname });
addLabel("cert-error-trust-expired-issuer");
break;
case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
addLabel("cert-error-intro", { hostname });
addLabel("cert-error-trust-self-signed");
break;
case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED":
addLabel("cert-error-intro", { hostname });
addLabel("cert-error-trust-symantec");
break;
default:
addLabel("cert-error-intro", { hostname });
addLabel("cert-error-untrusted-default");
}
addErrorCodeLink();
break;
case "expired-or-not-yet-valid": {
const notBefore = failedCertInfo.validNotBefore;
const notAfter = failedCertInfo.validNotAfter;
if (notBefore && Date.now() < notAfter) {
addLabel("cert-error-not-yet-valid-now", {
hostname,
"not-before-local-time": formatter.format(new Date(notBefore)),
});
} else {
addLabel("cert-error-expired-now", {
hostname,
"not-after-local-time": formatter.format(new Date(notAfter)),
});
}
addErrorCodeLink();
break;
}
case "domain-mismatch":
getSubjectAltNames(failedCertInfo).then(subjectAltNames => {
if (!subjectAltNames.length) {
addLabel("cert-error-domain-mismatch", { hostname });
} else if (subjectAltNames.length > 1) {
const names = subjectAltNames.join(", ");
addLabel("cert-error-domain-mismatch-multiple", {
hostname,
"subject-alt-names": names,
});
} else {
const altName = subjectAltNames[0];
// If the alt name is a wildcard domain ("*.example.com")
// let's use "www" instead. "*.example.com" isn't going to
// get anyone anywhere useful. bug 432491
const okHost = altName.replace(/^\*\./, "www.");
// Let's check if we want to make this a link.
const showLink =
/* case #1:
* example.com uses an invalid security certificate.
*
* The certificate is only valid for www.example.com
*
* Make sure to include the "." ahead of thisHost so that a
* MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
*
* We'd normally just use a RegExp here except that we lack a
* library function to escape them properly (bug 248062), and
* domain names are famous for having '.' characters in them,
* which would allow spurious and possibly hostile matches.
*/
okHost.endsWith("." + HOST_NAME) ||
/* case #2:
* browser.garage.maemo.org uses an invalid security certificate.
*
* The certificate is only valid for garage.maemo.org
*/
HOST_NAME.endsWith("." + okHost);
const l10nArgs = { hostname, "alt-name": altName };
if (showLink) {
// Set the link if we want it.
const proto = document.location.protocol + "//";
addLabel("cert-error-domain-mismatch-single", l10nArgs, {
href: proto + okHost,
"data-l10n-name": "domain-mismatch-link",
id: "cert_domain_link",
});
// If we set a link, meaning there's something helpful for
// the user here, expand the section by default
if (getCSSClass() != "expertBadCert") {
revealAdvancedPanelSlowlyAsync();
}
} else {
addLabel("cert-error-domain-mismatch-single-nolink", l10nArgs);
}
}
addErrorCodeLink();
});
break;
}
getFailedCertificatesAsPEMString().then(pemString => {
const errorText = document.getElementById("certificateErrorText");
errorText.textContent = pemString;
});
}
/* Only focus if we're the toplevel frame; otherwise we
don't want to call attention to ourselves!
*/
function setFocus(selector, position = "afterbegin") {
if (window.top == window) {
var button = document.querySelector(selector);
button.parentNode.insertAdjacentElement(position, button);
// It's possible setFocus was called via the DOMContentLoaded event
// handler and that the button has no frame. Things without a frame cannot
// be focused. We use a requestAnimationFrame to queue up the focus to occur
// once the button has its frame.
requestAnimationFrame(() => {
button.focus({ focusVisible: false });
});
}
}
for (let button of document.querySelectorAll(".try-again")) {
button.addEventListener("click", function () {
retryThis(this);
});
}
initPage();
// Dispatch this event so tests can detect that we finished loading the error page.
document.dispatchEvent(new CustomEvent("AboutNetErrorLoad", { bubbles: true }));