gecko-dev/toolkit/content/aboutNetError.mjs

1557 строки
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"
) {
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";
}
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", () => {
RPMSetBoolPref("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 } }) {
RPMSetBoolPref("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 }));