/* 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 { NetErrorCard } from "chrome://global/content/net-error-card.mjs"; import { gIsCertError, gErrorCode, gHasSts, searchParams, getHostName, getSubjectAltNames, getFailedCertificatesAsPEMString, recordSecurityUITelemetry, getCSSClass, } from "chrome://global/content/aboutNetErrorHelpers.mjs"; const formatter = new Intl.DateTimeFormat(); const HOST_NAME = getHostName(); const FELT_PRIVACY_REFRESH = RPMGetBoolPref( "security.certerrors.felt-privacy-v1", false ); // 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", "internet-connection-offline-title", "fileNotFound-title", "fileAccessDenied-title", "generic-title", "captivePortal-title", "malformedURI-title", "netInterrupt-title", "notCached-title", "netOffline-title", "contentEncodingError-title", "unsafeContentType-title", "netReset-title", "netTimeout-title", "httpErrorPage-title", "serverError-title", "unknownProtocolFound-title", "proxyConnectFailure-title", "proxyResolveFailure-title", "redirectLoop-title", "unknownSocketType-title", "nssFailure2-title", "csp-xfo-error-title", "corruptedContentErrorv2-title", "sslv3Used-title", "inadequateSecurityError-title", "blockedByPolicy-title", "blocked-by-corp-headers-title", "clockSkewError-title", "networkProtocolError-title", "nssBadCert-title", "nssBadCert-sts-title", "certerror-mitm-title", "general-body-title", "problem-with-this-site-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"; const MDN_DOCS_HEADERS = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/"; const COOP_MDN_DOCS = MDN_DOCS_HEADERS + "Cross-Origin-Opener-Policy"; const COEP_MDN_DOCS = MDN_DOCS_HEADERS + "Cross-Origin-Embedder-Policy"; const HTTPS_UPGRADES_MDN_DOCS = "https://support.mozilla.org/kb/https-upgrades"; // 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 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(); } 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). const failedCertInfo = document.getFailedCertSecurityInfo(); if (gHasSts || window != top || !failedCertInfo.errorIsOverridable) { 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 ) { RPMRecordGleanEvent("securityDohNeterror", "loadDohwarning", { value: 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; RPMRecordGleanEvent( "securityDohNeterror", "click" + telemetryId .split("_") .map(word => word[0].toUpperCase() + word.slice(1)) .join(""), { value: warningPageType, mode: trrMode, provider_key: trrDomain, skip_reason: skipReason, } ); }); } } function setResponseStatus(shortDesc) { let responseStatus; let responseStatusText; try { const netErrorInfo = document.getNetErrorInfo(); responseStatus = netErrorInfo.responseStatus; responseStatusText = netErrorInfo.responseStatusText; } catch (ex) { return; } if (responseStatus >= 400) { let responseStatusLabel = document.createElement("p"); responseStatusLabel.id = "response-status-label"; // id for testing document.l10n.setAttributes( responseStatusLabel, "neterror-response-status-code", { responsestatus: responseStatus, responsestatustext: responseStatusText ?? "", } ); shortDesc.appendChild(responseStatusLabel); } } // Returns pageTitleId, bodyTitle, bodyTitleId, and longDesc as an object function initTitleAndBodyIds(baseURL, isTRROnlyFailure) { let bodyTitle = document.querySelector(".title-text"); 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 "basicHttpAuthDisabled": bodyTitleId = "general-body-title"; tryAgain.hidden = true; break; 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 "blockedByCOOP": case "blockedByCOEP": { bodyTitleId = "general-body-title"; document.body.classList.add("blocked"); tryAgain.hidden = true; break; } case "invalidHeaderValue": { bodyTitleId = "problem-with-this-site-title"; 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; // Add a learn more link learnMore.hidden = false; learnMoreLink.setAttribute("href", baseURL + "xframe-neterror-page"); 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; // TLS errors and non-overridable certificate errors (e.g. pinning // failures) are of type nssFailure2. case "nssFailure2": { learnMore.hidden = false; const netErrorInfo = document.getNetErrorInfo(); void recordSecurityUITelemetry( "securityUiTlserror", "loadAbouttlserror", netErrorInfo ); const errorCode = netErrorInfo.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 "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; } return { pageTitleId, bodyTitle, bodyTitleId, longDesc }; } 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 noConnectivity = gErrorCode == "dnsNotFound" && !RPMHasConnectivity(); const docTitle = document.querySelector("title"); const shortDesc = document.getElementById("errorShortDesc"); if (gIsCertError) { const bodyTitle = document.querySelector(".title-text"); 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"); const tryAgain = document.getElementById("netErrorButtonContainer"); tryAgain.hidden = false; const learnMoreLink = document.getElementById("learnMoreLink"); learnMoreLink.setAttribute("href", baseURL + "connection-not-secure"); let { pageTitleId, bodyTitle, bodyTitleId, longDesc } = initTitleAndBodyIds( baseURL, isTRROnlyFailure ); // We can handle the offline page separately. if (noConnectivity) { pageTitleId = "neterror-dns-not-found-title"; bodyTitleId = "internet-connection-offline-title"; } // bodyTitle is set to null if it has already been set in initTitleAndBodyIds 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 && !noConnectivity) { pageTitleId = "neterror-dns-not-found-title"; document.l10n.setAttributes(docTitle, pageTitleId); if (bodyTitle) { bodyTitleId = "dnsNotFound-title"; 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(() => { 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-reason2", { 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_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"; } else if (skipReason == "TRR_SYSTEM_SLEEP_MODE") { descriptionTag = "neterror-dns-not-found-system-sleep"; } let trrMode = RPMGetIntPref("network.trr.mode"); 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"); RPMRecordGleanEvent("securityDohNeterror", "clickSettingsButton", { value: "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; } } document.l10n.setAttributes(docTitle, pageTitleId); if (bodyTitle) { document.l10n.setAttributes(bodyTitle, bodyTitleId); } shortDesc.textContent = getDescription(); setFocus("#netErrorButtonContainer > .try-again"); if (longDesc) { const parts = getNetErrorDescParts(noConnectivity); setNetErrorMessageFromParts(longDesc, parts); } setResponseStatus(shortDesc); setNetErrorMessageFromCode(); } /** * Builds HTML elements from `parts` and appends them to `parentElement`. * * @param {HTMLElement} parentElement * @param {Array<["li" | "p" | "span" | "a", string, Record | undefined]>} parts */ function setNetErrorMessageFromParts(parentElement, parts) { let list = null; for (let [tag, l10nId, l10nArgsOrHref] of parts) { const elem = document.createElement(tag); elem.dataset.l10nId = l10nId; if (l10nArgsOrHref) { if (tag === "a") { elem.href = l10nArgsOrHref; } else { elem.dataset.l10nArgs = JSON.stringify(l10nArgsOrHref); } } 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 (if the tag is not "a", optional) * - href (if the tag is "a", optional) * * @param {boolean} noConnectivity - if true, the browser has no active network interfaces * @returns { Array<["li" | "p" | "span" | "a", string, Record | undefined]> } */ function getNetErrorDescParts(noConnectivity) { switch (gErrorCode) { case "connectionFailure": case "netInterrupt": case "netReset": case "netTimeout": { let errorTags = [ ["li", "neterror-load-error-try-again"], ["li", "neterror-load-error-connection"], ["li", "neterror-load-error-firewall"], ]; if (RPMShowOSXLocalNetworkPermissionWarning()) { errorTags.push(["li", "neterror-load-osx-permission"]); } return errorTags; } case "httpErrorPage": // 4xx return [["li", "neterror-http-error-page"]]; case "serverError": // 5xx return [["li", "neterror-load-error-try-again"]]; case "blockedByCOOP": { return [ ["p", "certerror-blocked-by-corp-headers-description"], ["a", "certerror-coop-learn-more", COOP_MDN_DOCS], ]; } case "blockedByCOEP": { return [ ["p", "certerror-blocked-by-corp-headers-description"], ["a", "certerror-coep-learn-more", COEP_MDN_DOCS], ]; } 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": if (noConnectivity) { return [ ["span", "neterror-dns-not-found-offline-hint-header"], ["li", "neterror-dns-not-found-offline-hint-different-device"], ["li", "neterror-dns-not-found-offline-hint-modem"], ["li", "neterror-dns-not-found-offline-hint-reconnect"], ]; } 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 "invalidHeaderValue": { return [["li", "neterror-http-error-page"]]; } 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"]]; case "basicHttpAuthDisabled": return [ ["li", "neterror-basic-http-auth", { hostname: HOST_NAME }], ["a", "neterror-learn-more-link", HTTPS_UPGRADES_MDN_DOCS], ]; default: return [["p", "neterror-generic-error"]]; } } function setNetErrorMessageFromCode() { let errorCode; try { errorCode = document.getNetErrorInfo().errorCodeString; } catch (ex) { return; } if (!errorCode) { 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 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(); void recordSecurityUITelemetry( "securityUiCerterror", "loadAboutcerterror", failedCertInfo ); setCertErrorDetails(); } function recordClickTelemetry(e) { let target = e.originalTarget; let telemetryId = target.dataset.telemetryId; let failedCertInfo = document.getFailedCertSecurityInfo(); void recordSecurityUITelemetry( "securityUiCerterror", "click" + telemetryId .split("_") .map(word => word[0].toUpperCase() + word.slice(1)) .join(""), failedCertInfo ); } 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(); }, () => {} ); } function onReturnButtonClick() { RPMSendAsyncMessage("Browser:SSLErrorGoBack"); } function copyPEMToClipboard() { const errorText = document.getElementById("certificateErrorText"); navigator.clipboard.writeText(errorText.textContent); } 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_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 (errorHasNoUserFix(failedCertInfo.errorCodeString)) { // "cert-error-trust-certificate-transparency-what-can-you-do-about-it" was // originally added for certificate transparency errors, but it's general // enough to apply in many cases. whatToDoParts = [ [ "p", "cert-error-trust-certificate-transparency-what-can-you-do-about-it", ], ]; } if (whatToDoParts) { setNetErrorMessageFromParts( document.getElementById("errorWhatToDoText"), whatToDoParts ); document.getElementById("errorWhatToDo").hidden = false; } } // Returns true if the error identified by the given error code string has no // particular action the user can take to fix it. function errorHasNoUserFix(errorCodeString) { switch (errorCodeString) { case "MOZILLA_PKIX_ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY": case "MOZILLA_PKIX_ERROR_INVALID_INTEGER_ENCODING": case "MOZILLA_PKIX_ERROR_ISSUER_NO_LONGER_TRUSTED": case "MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE": case "MOZILLA_PKIX_ERROR_SIGNATURE_ALGORITHM_MISMATCH": case "SEC_ERROR_BAD_DER": case "SEC_ERROR_BAD_SIGNATURE": case "SEC_ERROR_CERT_NOT_IN_NAME_SPACE": case "SEC_ERROR_EXTENSION_VALUE_INVALID": case "SEC_ERROR_INADEQUATE_CERT_TYPE": case "SEC_ERROR_INADEQUATE_KEY_USAGE": case "SEC_ERROR_INVALID_KEY": case "SEC_ERROR_PATH_LEN_CONSTRAINT_INVALID": case "SEC_ERROR_REVOKED_CERTIFICATE": case "SEC_ERROR_UNKNOWN_CRITICAL_EXTENSION": case "SEC_ERROR_UNSUPPORTED_EC_POINT_FORM": case "SEC_ERROR_UNSUPPORTED_ELLIPTIC_CURVE": case "SEC_ERROR_UNSUPPORTED_KEYALG": case "SEC_ERROR_UNTRUSTED_CERT": case "SEC_ERROR_UNTRUSTED_ISSUER": return true; default: return false; } } // 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; case "MOZILLA_PKIX_ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY": addLabel("cert-error-trust-certificate-transparency", { hostname }); 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; } const nonoverridableErrorCodeToLabelMap = { SEC_ERROR_BAD_DER: "cert-error-bad-der", SEC_ERROR_BAD_SIGNATURE: "cert-error-bad-signature", SEC_ERROR_CERT_NOT_IN_NAME_SPACE: "cert-error-cert-not-in-name-space", SEC_ERROR_EXTENSION_VALUE_INVALID: "cert-error-extension-value-invalid", SEC_ERROR_INADEQUATE_CERT_TYPE: "cert-error-inadequate-cert-type", // NB: SEC_ERROR_INADEQUATE_KEY_USAGE intentionally uses the same error // message as SEC_ERROR_INADEQUATE_CERT_TYPE SEC_ERROR_INADEQUATE_KEY_USAGE: "cert-error-inadequate-cert-type", SEC_ERROR_INVALID_KEY: "cert-error-invalid-key", SEC_ERROR_PATH_LEN_CONSTRAINT_INVALID: "cert-error-path-len-constraint-invalid", SEC_ERROR_REVOKED_CERTIFICATE: "cert-error-revoked-certificate", SEC_ERROR_UNKNOWN_CRITICAL_EXTENSION: "cert-error-unknown-critical-extension", // NB: SEC_ERROR_UNSUPPORTED_EC_POINT_FORM intentionally uses the same // error message as SEC_ERROR_UNSUPPORTED_KEYALG SEC_ERROR_UNSUPPORTED_EC_POINT_FORM: "cert-error-unsupported-keyalg", // NB: SEC_ERROR_UNSUPPORTED_ELLIPTIC_CURVE intentionally uses the same // error message as SEC_ERROR_UNSUPPORTED_KEYALG SEC_ERROR_UNSUPPORTED_ELLIPTIC_CURVE: "cert-error-unsupported-keyalg", SEC_ERROR_UNSUPPORTED_KEYALG: "cert-error-unsupported-keyalg", SEC_ERROR_UNTRUSTED_CERT: "cert-error-untrusted-cert", SEC_ERROR_UNTRUSTED_ISSUER: "cert-error-untrusted-issuer", MOZILLA_PKIX_ERROR_INVALID_INTEGER_ENCODING: "cert-error-invalid-integer-encoding", MOZILLA_PKIX_ERROR_ISSUER_NO_LONGER_TRUSTED: "cert-error-issuer-no-longer-trusted", MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE: "cert-error-key-pinning-failure", MOZILLA_PKIX_ERROR_SIGNATURE_ALGORITHM_MISMATCH: "cert-error-signature-algorithm-mismatch", }; if (failedCertInfo.errorCodeString in nonoverridableErrorCodeToLabelMap) { addLabel( nonoverridableErrorCodeToLabelMap[failedCertInfo.errorCodeString], { hostname } ); addErrorCodeLink(); } 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 }); }); } } function shouldUseFeltPrivacyRefresh() { if (!FELT_PRIVACY_REFRESH) { return false; } let failedCertInfo; try { failedCertInfo = document.getFailedCertSecurityInfo(); } catch { return false; } return NetErrorCard.ERROR_CODES.has(failedCertInfo.errorCodeString); } if (!shouldUseFeltPrivacyRefresh()) { 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 }) ); } else { customElements.define("net-error-card", NetErrorCard); document.body.classList.add("felt-privacy-body"); document.body.replaceChildren(document.createElement("net-error-card")); }