1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* eslint-env mozilla/remote-page */
6 /* eslint-disable import/no-unassigned-import */
11 } from "chrome://global/content/certviewer/certDecoder.mjs";
13 const formatter = new Intl.DateTimeFormat();
15 const HOST_NAME = getHostName();
17 function getHostName() {
19 return new URL(RPMGetInnerMostURI(document.location.href)).hostname;
21 console.error("Could not parse URL", error);
26 // Used to check if we have a specific localized message for an error.
27 const KNOWN_ERROR_TITLE_IDS = new Set([
29 "connectionFailure-title",
30 "deniedPortAccess-title",
32 "dns-not-found-trr-only-title2",
34 "fileAccessDenied-title",
36 "captivePortal-title",
41 "contentEncodingError-title",
42 "unsafeContentType-title",
45 "unknownProtocolFound-title",
46 "proxyConnectFailure-title",
47 "proxyResolveFailure-title",
49 "unknownSocketType-title",
51 "csp-xfo-error-title",
52 "corruptedContentError-title",
54 "inadequateSecurityError-title",
55 "blockedByPolicy-title",
56 "clockSkewError-title",
57 "networkProtocolError-title",
59 "nssBadCert-sts-title",
60 "certerror-mitm-title",
63 /* The error message IDs from nsserror.ftl get processed into
64 * aboutNetErrorCodes.js which is loaded before we are: */
65 /* global KNOWN_ERROR_MESSAGE_IDS */
66 const ERROR_MESSAGES_FTL = "toolkit/neterror/nsserrors.ftl";
68 // The following parameters are parsed from the error URL:
70 // s - custom CSS class to allow alternate styling/favicons
71 // d - error description
72 // captive - "true" to indicate we're behind a captive portal.
73 // Any other value is ignored.
75 // Note that this file uses document.documentURI to get
76 // the URL (with the format from above). This is because
77 // document.location.href gets the current URI off the docshell,
78 // which is the URL displayed in the location bar, i.e.
79 // the URI that the user attempted to load.
81 let searchParams = new URLSearchParams(document.documentURI.split("?")[1]);
83 let gErrorCode = searchParams.get("e");
84 let gIsCertError = gErrorCode == "nssBadCert";
85 let gHasSts = gIsCertError && getCSSClass() === "badStsCert";
87 // If the location of the favicon changes, FAVICON_CERTERRORPAGE_URL and/or
88 // FAVICON_ERRORPAGE_URL in toolkit/components/places/nsFaviconService.idl
89 // should also be updated.
90 document.getElementById("favicon").href =
91 gIsCertError || gErrorCode == "nssFailure2"
92 ? "chrome://global/skin/icons/warning.svg"
93 : "chrome://global/skin/icons/info.svg";
95 function getCSSClass() {
96 return searchParams.get("s");
99 function getDescription() {
100 return searchParams.get("d");
103 function isCaptive() {
104 return searchParams.get("captive") == "true";
108 * We don't actually know what the MitM is called (since we don't
109 * maintain a list), so we'll try and display the common name of the
110 * root issuer to the user. In the worst case they are as clueless as
111 * before, in the best case this gives them an actionable hint.
112 * This may be revised in the future.
114 function getMitmName(failedCertInfo) {
115 return failedCertInfo.issuerCommonName;
118 function retryThis(buttonEl) {
119 RPMSendAsyncMessage("Browser:EnableOnlineMode");
120 buttonEl.disabled = true;
123 function showPrefChangeContainer() {
124 const panel = document.getElementById("prefChangeContainer");
125 panel.hidden = false;
126 document.getElementById("netErrorButtonContainer").hidden = true;
128 .getElementById("prefResetButton")
129 .addEventListener("click", function resetPreferences() {
130 RPMSendAsyncMessage("Browser:ResetSSLPreferences");
132 setFocus("#prefResetButton", "beforeend");
135 function toggleCertErrorDebugInfoVisibility(shouldShow) {
136 let debugInfo = document.getElementById("certificateErrorDebugInformation");
137 let copyButton = document.getElementById("copyToClipboardTop");
139 if (shouldShow === undefined) {
140 shouldShow = debugInfo.hidden;
142 debugInfo.hidden = !shouldShow;
144 copyButton.scrollIntoView({ block: "start", behavior: "smooth" });
149 function setupAdvancedButton() {
150 // Get the hostname and add it to the panel
151 var panel = document.getElementById("badCertAdvancedPanel");
153 // Register click handler for the weakCryptoAdvancedPanel
155 .getElementById("advancedButton")
156 .addEventListener("click", togglePanelVisibility);
158 function togglePanelVisibility() {
161 revealAdvancedPanelSlowlyAsync();
163 // send event to trigger telemetry ping
164 document.dispatchEvent(
165 new CustomEvent("AboutNetErrorUIExpanded", { bubbles: true })
173 if (getCSSClass() == "expertBadCert") {
174 revealAdvancedPanelSlowlyAsync();
178 async function revealAdvancedPanelSlowlyAsync() {
179 const badCertAdvancedPanel = document.getElementById("badCertAdvancedPanel");
180 const exceptionDialogButton = document.getElementById(
181 "exceptionDialogButton"
184 // Toggling the advanced panel must ensure that the debugging
185 // information panel is hidden as well, since it's opened by the
186 // error code link in the advanced panel.
187 toggleCertErrorDebugInfoVisibility(false);
189 // Reveal, but disabled (and grayed-out) for 3.0s.
190 badCertAdvancedPanel.hidden = false;
191 exceptionDialogButton.disabled = true;
195 if (exceptionDialogButton.resetReveal) {
196 exceptionDialogButton.resetReveal(); // Reset if previous is pending.
198 let wasReset = false;
199 exceptionDialogButton.resetReveal = () => {
203 // Wait for 10 frames to ensure that the warning text is rendered
204 // and gets all the way to the screen for the user to read it.
205 // This is only ~0.160s at 60Hz, so it's not too much extra time that we're
206 // taking to ensure that we're caught up with rendering, on top of the
207 // (by default) whole second(s) we're going to wait based on the
208 // security.dialog_enable_delay pref.
209 // The catching-up to rendering is the important part, not the
210 // N-frame-delay here.
211 for (let i = 0; i < 10; i++) {
212 await new Promise(requestAnimationFrame);
215 // Wait another Nms (default: 1000) for the user to be very sure. (Sorry speed readers!)
216 const securityDelayMs = RPMGetIntPref("security.dialog_enable_delay", 1000);
217 await new Promise(go => setTimeout(go, securityDelayMs));
223 // Enable and un-gray-out.
224 exceptionDialogButton.disabled = false;
227 function disallowCertOverridesIfNeeded() {
228 // Disallow overrides if this is a Strict-Transport-Security
229 // host and the cert is bad (STS Spec section 7.3) or if the
230 // certerror is in a frame (bug 633691).
231 if (gHasSts || window != top) {
232 document.getElementById("exceptionDialogButton").hidden = true;
235 const stsExplanation = document.getElementById("badStsCertExplanation");
236 document.l10n.setAttributes(
238 "certerror-what-should-i-do-bad-sts-cert-explanation",
239 { hostname: HOST_NAME }
241 stsExplanation.hidden = false;
243 document.l10n.setAttributes(
244 document.getElementById("returnButton"),
245 "neterror-return-to-previous-page-button"
247 document.l10n.setAttributes(
248 document.getElementById("advancedPanelReturnButton"),
249 "neterror-return-to-previous-page-button"
254 function recordTRREventTelemetry(
260 RPMRecordTelemetryEvent(
261 "security.doh.neterror",
267 provider_key: trrDomain,
268 skip_reason: skipReason,
272 const netErrorButtonDiv = document.getElementById("netErrorButtonContainer");
273 const buttons = netErrorButtonDiv.querySelectorAll("button");
274 for (let b of buttons) {
275 b.addEventListener("click", function (e) {
276 let target = e.originalTarget;
277 let telemetryId = target.dataset.telemetryId;
278 RPMRecordTelemetryEvent(
279 "security.doh.neterror",
285 provider_key: trrDomain,
286 skip_reason: skipReason,
293 function initPage() {
294 // We show an offline support page in case of a system-wide error,
295 // when a user cannot connect to the internet and access the SUMO website.
296 // For example, clock error, which causes certerrors across the web or
297 // a security software conflict where the user is unable to connect
299 // The URL that prompts us to show an offline support page should have the following
300 // format: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/supportPageSlug",
301 // so we can extract the support page slug.
302 let baseURL = RPMGetFormatURLPref("app.support.baseURL");
303 if (document.location.href.startsWith(baseURL)) {
304 let supportPageSlug = document.location.pathname.split("/").pop();
305 RPMSendAsyncMessage("DisplayOfflineSupportPage", {
310 const className = getCSSClass();
312 document.body.classList.add(className);
315 const isTRROnlyFailure = gErrorCode == "dnsNotFound" && RPMIsTRROnlyFailure();
317 let isNativeFallbackWarning = false;
318 if (RPMGetBoolPref("network.trr.display_fallback_warning")) {
319 isNativeFallbackWarning =
320 gErrorCode == "dnsNotFound" && RPMIsNativeFallbackFailure();
323 const docTitle = document.querySelector("title");
324 const bodyTitle = document.querySelector(".title-text");
325 const shortDesc = document.getElementById("errorShortDesc");
328 const isStsError = window !== window.top || gHasSts;
329 const errArgs = { hostname: HOST_NAME };
331 document.l10n.setAttributes(
333 "neterror-captive-portal-page-title"
335 document.l10n.setAttributes(bodyTitle, "captivePortal-title");
336 document.l10n.setAttributes(
338 "neterror-captive-portal",
341 initPageCaptivePortal();
344 document.l10n.setAttributes(docTitle, "certerror-sts-page-title");
345 document.l10n.setAttributes(bodyTitle, "nssBadCert-sts-title");
346 document.l10n.setAttributes(shortDesc, "certerror-sts-intro", errArgs);
348 document.l10n.setAttributes(docTitle, "certerror-page-title");
349 document.l10n.setAttributes(bodyTitle, "nssBadCert-title");
350 document.l10n.setAttributes(shortDesc, "certerror-intro", errArgs);
355 initCertErrorPageActions();
356 setTechnicalDetailsOnCertError();
360 document.body.classList.add("neterror");
362 let longDesc = document.getElementById("errorLongDesc");
363 const tryAgain = document.getElementById("netErrorButtonContainer");
364 tryAgain.hidden = false;
365 const learnMore = document.getElementById("learnMoreContainer");
366 const learnMoreLink = document.getElementById("learnMoreLink");
367 learnMoreLink.setAttribute("href", baseURL + "connection-not-secure");
369 let pageTitleId = "neterror-page-title";
370 let bodyTitleId = gErrorCode + "-title";
372 switch (gErrorCode) {
373 case "blockedByPolicy":
374 pageTitleId = "neterror-blocked-by-policy-page-title";
375 document.body.classList.add("blocked");
377 // Remove the "Try again" button from pages that don't need it.
378 // For pages blocked by policy, trying again won't help.
379 tryAgain.hidden = true;
384 bodyTitleId = "csp-xfo-error-title";
386 // Remove the "Try again" button for XFO and CSP violations,
387 // since it's almost certainly useless. (Bug 553180)
388 tryAgain.hidden = true;
390 // Adding a button for opening websites blocked for CSP and XFO violations
391 // in a new window. (Bug 1461195)
392 document.getElementById("errorShortDesc").hidden = true;
394 document.l10n.setAttributes(longDesc, "csp-xfo-blocked-long-desc", {
399 document.getElementById("openInNewWindowContainer").hidden = false;
401 const openInNewWindowButton = document.getElementById(
402 "openInNewWindowButton"
404 openInNewWindowButton.href = document.location.href;
406 // Add a learn more link
407 learnMore.hidden = false;
408 learnMoreLink.setAttribute("href", baseURL + "xframe-neterror-page");
410 setupBlockingReportingUI();
415 pageTitleId = "neterror-dns-not-found-title";
416 if (!isTRROnlyFailure) {
417 RPMCheckAlternateHostAvailable();
421 case "inadequateSecurityError":
422 // Remove the "Try again" button from pages that don't need it.
423 // For HTTP/2 inadequate security, trying again won't help.
424 tryAgain.hidden = true;
428 pageTitleId = "neterror-malformed-uri-page-title";
429 // Remove the "Try again" button from pages that don't need it.
430 tryAgain.hidden = true;
433 // TLS errors and non-overridable certificate errors (e.g. pinning
434 // failures) are of type nssFailure2.
435 case "nssFailure2": {
436 learnMore.hidden = false;
438 const errorCode = document.getNetErrorInfo().errorCodeString;
439 RPMRecordTelemetryEvent(
440 "security.ui.tlserror",
445 is_frame: (window.parent != window).toString(),
449 case "SSL_ERROR_UNSUPPORTED_VERSION":
450 case "SSL_ERROR_PROTOCOL_VERSION_ALERT": {
451 const tlsNotice = document.getElementById("tlsVersionNotice");
452 tlsNotice.hidden = false;
453 document.l10n.setAttributes(tlsNotice, "cert-error-old-tls-version");
457 case "interrupted": // This happens with subresources that are above the max tls
458 case "SSL_ERROR_NO_CIPHERS_SUPPORTED":
459 case "SSL_ERROR_NO_CYPHER_OVERLAP":
460 case "SSL_ERROR_SSL_DISABLED":
461 RPMAddMessageListener("HasChangedCertPrefs", msg => {
462 if (msg.data.hasChangedCertPrefs) {
463 // Configuration overrides might have caused this; offer to reset.
464 showPrefChangeContainer();
467 RPMSendAsyncMessage("GetChangedCertPrefs");
474 learnMore.hidden = false;
475 document.body.className = "certerror";
479 if (!KNOWN_ERROR_TITLE_IDS.has(bodyTitleId)) {
480 console.error("No strings exist for error:", gErrorCode);
481 bodyTitleId = "generic-title";
484 // The TRR errors may present options that direct users to settings only available on Firefox Desktop
485 if (RPMIsFirefox()) {
486 if (isTRROnlyFailure) {
487 document.body.className = "certerror"; // Shows warning icon
488 pageTitleId = "dns-not-found-trr-only-title2";
489 document.l10n.setAttributes(docTitle, pageTitleId);
490 bodyTitleId = "dns-not-found-trr-only-title2";
491 document.l10n.setAttributes(bodyTitle, bodyTitleId);
493 shortDesc.textContent = "";
494 let skipReason = RPMGetTRRSkipReason();
497 let trrExceptionButton = document.getElementById("trrExceptionButton");
498 trrExceptionButton.addEventListener("click", () => {
499 RPMSendQuery("Browser:AddTRRExcludedDomain", {
502 retryThis(trrExceptionButton);
506 let isTrrServerError = true;
507 if (RPMIsSiteSpecificTRRError()) {
508 // Only show the exclude button if the failure is specific to this
509 // domain. If the TRR server is inaccessible we don't want to allow
510 // the user to add an exception just for this domain.
511 trrExceptionButton.hidden = false;
512 isTrrServerError = false;
514 let trrSettingsButton = document.getElementById("trrSettingsButton");
515 trrSettingsButton.addEventListener("click", () => {
516 RPMSendAsyncMessage("OpenTRRPreferences");
518 trrSettingsButton.hidden = false;
519 let message = document.getElementById("trrOnlyMessage");
520 document.l10n.setAttributes(
522 "neterror-dns-not-found-trr-only-reason2",
528 let descriptionTag = "neterror-dns-not-found-trr-unknown-problem";
529 let args = { trrDomain: RPMGetTRRDomain() };
531 skipReason == "TRR_FAILED" ||
532 skipReason == "TRR_CHANNEL_DNS_FAIL" ||
533 skipReason == "TRR_UNKNOWN_CHANNEL_FAILURE" ||
534 skipReason == "TRR_NET_REFUSED" ||
535 skipReason == "TRR_NET_INTERRUPT" ||
536 skipReason == "TRR_NET_INADEQ_SEQURITY"
538 descriptionTag = "neterror-dns-not-found-trr-only-could-not-connect";
539 } else if (skipReason == "TRR_TIMEOUT") {
540 descriptionTag = "neterror-dns-not-found-trr-only-timeout";
542 skipReason == "TRR_IS_OFFLINE" ||
543 skipReason == "TRR_NO_CONNECTIVITY"
545 descriptionTag = "neterror-dns-not-found-trr-offline";
547 skipReason == "TRR_NO_ANSWERS" ||
548 skipReason == "TRR_NXDOMAIN" ||
549 skipReason == "TRR_RCODE_FAIL"
551 descriptionTag = "neterror-dns-not-found-trr-unknown-host2";
553 skipReason == "TRR_DECODE_FAILED" ||
554 skipReason == "TRR_SERVER_RESPONSE_ERR"
556 descriptionTag = "neterror-dns-not-found-trr-server-problem";
557 } else if (skipReason == "TRR_BAD_URL") {
558 descriptionTag = "neterror-dns-not-found-bad-trr-url";
561 let trrMode = RPMGetIntPref("network.trr.mode").toString();
562 recordTRREventTelemetry(
569 let description = document.getElementById("trrOnlyDescription");
570 document.l10n.setAttributes(description, descriptionTag, args);
572 const trrLearnMoreContainer = document.getElementById(
573 "trrLearnMoreContainer"
575 trrLearnMoreContainer.hidden = false;
576 let trrOnlyLearnMoreLink = document.getElementById(
577 "trrOnlylearnMoreLink"
579 if (isTrrServerError) {
580 // Go to DoH settings page
581 trrOnlyLearnMoreLink.href = "about:preferences#privacy-doh";
582 trrOnlyLearnMoreLink.addEventListener("click", event => {
583 event.preventDefault();
584 RPMSendAsyncMessage("OpenTRRPreferences");
585 RPMRecordTelemetryEvent(
586 "security.doh.neterror",
592 provider_key: args.trrDomain,
593 skip_reason: skipReason,
598 // This will be replaced at a later point with a link to an offline support page
599 // https://bugzilla.mozilla.org/show_bug.cgi?id=1806257
600 trrOnlyLearnMoreLink.href =
601 RPMGetFormatURLPref("network.trr_ui.skip_reason_learn_more_url") +
602 skipReason.toLowerCase().replaceAll("_", "-");
605 let div = document.getElementById("trrOnlyContainer");
609 } else if (isNativeFallbackWarning) {
610 showNativeFallbackWarning();
615 document.l10n.setAttributes(docTitle, pageTitleId);
616 document.l10n.setAttributes(bodyTitle, bodyTitleId);
618 shortDesc.textContent = getDescription();
619 setFocus("#netErrorButtonContainer > .try-again");
622 const parts = getNetErrorDescParts();
623 setNetErrorMessageFromParts(longDesc, parts);
626 setNetErrorMessageFromCode();
629 function showNativeFallbackWarning() {
630 const docTitle = document.querySelector("title");
631 const bodyTitle = document.querySelector(".title-text");
632 const shortDesc = document.getElementById("errorShortDesc");
634 let pageTitleId = "neterror-page-title";
635 let bodyTitleId = gErrorCode + "-title";
637 document.body.className = "certerror"; // Shows warning icon
638 pageTitleId = "dns-not-found-native-fallback-title2";
639 document.l10n.setAttributes(docTitle, pageTitleId);
641 bodyTitleId = "dns-not-found-native-fallback-title2";
642 document.l10n.setAttributes(bodyTitle, bodyTitleId);
644 shortDesc.textContent = "";
645 let nativeFallbackIgnoreButton = document.getElementById(
646 "nativeFallbackIgnoreButton"
648 nativeFallbackIgnoreButton.addEventListener("click", () => {
649 RPMSetPref("network.trr.display_fallback_warning", false);
650 retryThis(nativeFallbackIgnoreButton);
653 let continueThisTimeButton = document.getElementById(
654 "nativeFallbackContinueThisTimeButton"
656 continueThisTimeButton.addEventListener("click", () => {
657 RPMSetTRRDisabledLoadFlags();
658 document.location.reload();
660 continueThisTimeButton.hidden = false;
662 nativeFallbackIgnoreButton.hidden = false;
663 let message = document.getElementById("nativeFallbackMessage");
664 document.l10n.setAttributes(
666 "neterror-dns-not-found-native-fallback-reason2",
671 let skipReason = RPMGetTRRSkipReason();
672 let descriptionTag = "neterror-dns-not-found-trr-unknown-problem";
673 let args = { trrDomain: RPMGetTRRDomain() };
675 if (skipReason.includes("HEURISTIC_TRIPPED")) {
676 descriptionTag = "neterror-dns-not-found-native-fallback-heuristic";
677 } else if (skipReason == "TRR_NOT_CONFIRMED") {
678 descriptionTag = "neterror-dns-not-found-native-fallback-not-confirmed2";
681 let description = document.getElementById("nativeFallbackDescription");
682 document.l10n.setAttributes(description, descriptionTag, args);
684 let learnMoreContainer = document.getElementById(
685 "nativeFallbackLearnMoreContainer"
687 learnMoreContainer.hidden = false;
689 let learnMoreLink = document.getElementById("nativeFallbackLearnMoreLink");
691 RPMGetFormatURLPref("network.trr_ui.skip_reason_learn_more_url") +
692 skipReason.toLowerCase().replaceAll("_", "-");
694 let div = document.getElementById("nativeFallbackContainer");
697 recordTRREventTelemetry(
698 "NativeFallbackWarning",
699 RPMGetIntPref("network.trr.mode").toString(),
705 * Builds HTML elements from `parts` and appends them to `parentElement`.
707 * @param {HTMLElement} parentElement
708 * @param {Array<["li" | "p" | "span", string, Record<string, string> | undefined]>} parts
710 function setNetErrorMessageFromParts(parentElement, parts) {
713 for (let [tag, l10nId, l10nArgs] of parts) {
714 const elem = document.createElement(tag);
715 elem.dataset.l10nId = l10nId;
717 elem.dataset.l10nArgs = JSON.stringify(l10nArgs);
722 list = document.createElement("ul");
723 parentElement.appendChild(list);
725 list.appendChild(elem);
730 parentElement.appendChild(elem);
736 * Returns an array of tuples determining the parts of an error message:
739 * - l10n args (optional)
741 * @returns { Array<["li" | "p" | "span", string, Record<string, string> | undefined]> }
743 function getNetErrorDescParts() {
744 switch (gErrorCode) {
745 case "connectionFailure":
750 ["li", "neterror-load-error-try-again"],
751 ["li", "neterror-load-error-connection"],
752 ["li", "neterror-load-error-firewall"],
755 case "blockedByPolicy":
756 case "deniedPortAccess":
760 case "captivePortal":
762 case "contentEncodingError":
763 return [["li", "neterror-content-encoding-error"]];
764 case "corruptedContentErrorv2":
766 ["p", "neterror-corrupted-content-intro"],
767 ["li", "neterror-corrupted-content-contact-website"],
771 ["span", "neterror-dns-not-found-hint-header"],
772 ["li", "neterror-dns-not-found-hint-try-again"],
773 ["li", "neterror-dns-not-found-hint-check-network"],
774 ["li", "neterror-dns-not-found-hint-firewall"],
776 case "fileAccessDenied":
777 return [["li", "neterror-access-denied"]];
780 ["li", "neterror-file-not-found-filename"],
781 ["li", "neterror-file-not-found-moved"],
783 case "inadequateSecurityError":
785 ["p", "neterror-inadequate-security-intro", { hostname: HOST_NAME }],
786 ["p", "neterror-inadequate-security-code"],
789 const failedCertInfo = document.getFailedCertSecurityInfo();
792 mitm: getMitmName(failedCertInfo),
794 return [["span", "certerror-mitm", errArgs]];
797 return [["li", "neterror-net-offline"]];
798 case "networkProtocolError":
800 ["p", "neterror-network-protocol-error-intro"],
801 ["li", "neterror-network-protocol-error-contact-website"],
805 ["p", "neterror-not-cached-intro"],
806 ["li", "neterror-not-cached-sensitive"],
807 ["li", "neterror-not-cached-try-again"],
811 ["li", "neterror-nss-failure-not-verified"],
812 ["li", "neterror-nss-failure-contact-website"],
814 case "proxyConnectFailure":
816 ["li", "neterror-proxy-connect-failure-settings"],
817 ["li", "neterror-proxy-connect-failure-contact-admin"],
819 case "proxyResolveFailure":
821 ["li", "neterror-proxy-resolve-failure-settings"],
822 ["li", "neterror-proxy-resolve-failure-connection"],
823 ["li", "neterror-proxy-resolve-failure-firewall"],
826 return [["li", "neterror-redirect-loop"]];
828 return [["span", "neterror-sslv3-used"]];
829 case "unknownProtocolFound":
830 return [["li", "neterror-unknown-protocol"]];
831 case "unknownSocketType":
833 ["li", "neterror-unknown-socket-type-psm-installed"],
834 ["li", "neterror-unknown-socket-type-server-config"],
836 case "unsafeContentType":
837 return [["li", "neterror-unsafe-content-type"]];
840 return [["p", "neterror-generic-error"]];
844 function setNetErrorMessageFromCode() {
847 errorCode = document.getNetErrorInfo().errorCodeString;
849 // We don't have a securityInfo when this is for example a DNS error.
855 const l10nId = errorCode.replace(/_/g, "-").toLowerCase();
856 if (KNOWN_ERROR_MESSAGE_IDS.has(l10nId)) {
857 const l10n = new Localization([ERROR_MESSAGES_FTL], true);
858 errorMessage = l10n.formatValueSync(l10nId);
861 const shortDesc2 = document.getElementById("errorShortDesc2");
862 document.l10n.setAttributes(shortDesc2, "cert-error-code-prefix", {
866 console.warn("This error page has no error code in its security info");
869 let hostname = HOST_NAME;
870 const { port } = document.location;
871 if (port && port != 443) {
872 hostname += ":" + port;
875 const shortDesc = document.getElementById("errorShortDesc");
876 document.l10n.setAttributes(shortDesc, "cert-error-ssl-connection-error", {
877 errorMessage: errorMessage ?? errorCode ?? "",
882 function setupBlockingReportingUI() {
883 let checkbox = document.getElementById("automaticallyReportBlockingInFuture");
885 let reportingAutomatic = RPMGetBoolPref(
886 "security.xfocsp.errorReporting.automatic"
888 checkbox.checked = !!reportingAutomatic;
890 checkbox.addEventListener("change", function ({ target: { checked } }) {
891 RPMSetPref("security.xfocsp.errorReporting.automatic", checked);
893 // If we're enabling reports, send a report for this failure.
895 reportBlockingError();
899 let reportingEnabled = RPMGetBoolPref(
900 "security.xfocsp.errorReporting.enabled"
903 if (reportingEnabled) {
904 // Display blocking error reporting UI for XFO error and CSP error.
905 document.getElementById("blockingErrorReporting").hidden = false;
907 if (reportingAutomatic) {
908 reportBlockingError();
913 function reportBlockingError() {
914 // We only report if we are in a frame.
915 if (window === window.top) {
919 let err = gErrorCode;
920 // Ensure we only deal with XFO and CSP here.
921 if (!["xfoBlocked", "cspBlocked"].includes(err)) {
925 let xfo_header = RPMGetHttpResponseHeader("X-Frame-Options");
926 let csp_header = RPMGetHttpResponseHeader("Content-Security-Policy");
928 // Extract the 'CSP: frame-ancestors' from the CSP header.
929 let reg = /(?:^|\s)frame-ancestors\s([^;]*)[$]*/i;
930 let match = reg.exec(csp_header);
931 csp_header = match ? match[1] : "";
933 // If it's the csp error page without the CSP: frame-ancestors, this means
934 // this error page is not triggered by CSP: frame-ancestors. So, we bail out
936 if (err === "cspBlocked" && !csp_header) {
940 let xfoAndCspInfo = {
941 error_type: err === "xfoBlocked" ? "xfo" : "csp",
946 // Trimming the tail colon symbol.
947 let scheme = document.location.protocol.slice(0, -1);
949 RPMSendAsyncMessage("ReportBlockingError", {
951 host: document.location.host,
952 port: parseInt(document.location.port) || -1,
953 path: document.location.pathname,
958 function initPageCaptivePortal() {
959 document.body.className = "captiveportal";
960 document.getElementById("returnButton").hidden = true;
961 const openButton = document.getElementById("openPortalLoginPageButton");
962 openButton.hidden = false;
963 openButton.addEventListener("click", () => {
964 RPMSendAsyncMessage("Browser:OpenCaptivePortalPage");
967 setFocus("#openPortalLoginPageButton");
968 setupAdvancedButton();
969 disallowCertOverridesIfNeeded();
971 // When the portal is freed, an event is sent by the parent process
972 // that we can pick up and attempt to reload the original page.
973 RPMAddMessageListener("AboutNetErrorCaptivePortalFreed", () => {
974 document.location.reload();
978 function initPageCertError() {
979 document.body.classList.add("certerror");
981 setFocus("#returnButton");
982 setupAdvancedButton();
983 disallowCertOverridesIfNeeded();
985 const hideAddExceptionButton = RPMGetBoolPref(
986 "security.certerror.hideAddException",
989 if (hideAddExceptionButton) {
990 document.getElementById("exceptionDialogButton").hidden = true;
993 const els = document.querySelectorAll("[data-telemetry-id]");
994 for (let el of els) {
995 el.addEventListener("click", recordClickTelemetry);
998 const failedCertInfo = document.getFailedCertSecurityInfo();
999 // Truncate the error code to avoid going over the allowed
1000 // string size limit for telemetry events.
1001 const errorCode = failedCertInfo.errorCodeString.substring(0, 40);
1002 RPMRecordTelemetryEvent(
1003 "security.ui.certerror",
1008 has_sts: gHasSts.toString(),
1009 is_frame: (window.parent != window).toString(),
1013 setCertErrorDetails();
1016 function recordClickTelemetry(e) {
1017 let target = e.originalTarget;
1018 let telemetryId = target.dataset.telemetryId;
1019 let failedCertInfo = document.getFailedCertSecurityInfo();
1020 // Truncate the error code to avoid going over the allowed
1021 // string size limit for telemetry events.
1022 let errorCode = failedCertInfo.errorCodeString.substring(0, 40);
1023 RPMRecordTelemetryEvent(
1024 "security.ui.certerror",
1029 has_sts: gHasSts.toString(),
1030 is_frame: (window.parent != window).toString(),
1035 function initCertErrorPageActions() {
1036 document.getElementById(
1037 "certErrorAndCaptivePortalButtonContainer"
1040 .getElementById("returnButton")
1041 .addEventListener("click", onReturnButtonClick);
1043 .getElementById("advancedPanelReturnButton")
1044 .addEventListener("click", onReturnButtonClick);
1046 .getElementById("copyToClipboardTop")
1047 .addEventListener("click", copyPEMToClipboard);
1049 .getElementById("copyToClipboardBottom")
1050 .addEventListener("click", copyPEMToClipboard);
1052 .getElementById("exceptionDialogButton")
1053 .addEventListener("click", addCertException);
1056 function addCertException() {
1058 !RPMIsWindowPrivate() &&
1059 RPMGetBoolPref("security.certerrors.permanentOverride");
1060 document.addCertException(!isPermanent).then(
1068 function onReturnButtonClick() {
1069 RPMSendAsyncMessage("Browser:SSLErrorGoBack");
1072 function copyPEMToClipboard() {
1073 const errorText = document.getElementById("certificateErrorText");
1074 navigator.clipboard.writeText(errorText.textContent);
1077 async function getFailedCertificatesAsPEMString() {
1078 let locationUrl = document.location.href;
1079 let failedCertInfo = document.getFailedCertSecurityInfo();
1080 let errorMessage = failedCertInfo.errorMessage;
1081 let hasHSTS = failedCertInfo.hasHSTS.toString();
1082 let hasHPKP = failedCertInfo.hasHPKP.toString();
1083 let [hstsLabel, hpkpLabel, failedChainLabel] =
1084 await document.l10n.formatValues([
1085 { id: "cert-error-details-hsts-label", args: { hasHSTS } },
1086 { id: "cert-error-details-key-pinning-label", args: { hasHPKP } },
1087 { id: "cert-error-details-cert-chain-label" },
1090 let certStrings = failedCertInfo.certChainStrings;
1091 let failedChainCertificates = "";
1092 for (let der64 of certStrings) {
1093 let wrapped = der64.replace(/(\S{64}(?!$))/g, "$1\r\n");
1094 failedChainCertificates +=
1095 "-----BEGIN CERTIFICATE-----\r\n" +
1097 "\r\n-----END CERTIFICATE-----\r\n";
1111 failedChainCertificates;
1115 function setCertErrorDetails() {
1116 // Check if the connection is being man-in-the-middled. When the parent
1117 // detects an intercepted connection, the page may be reloaded with a new
1118 // error code (MOZILLA_PKIX_ERROR_MITM_DETECTED).
1119 const failedCertInfo = document.getFailedCertSecurityInfo();
1120 const mitmPrimingEnabled = RPMGetBoolPref(
1121 "security.certerrors.mitm.priming.enabled"
1124 mitmPrimingEnabled &&
1125 failedCertInfo.errorCodeString == "SEC_ERROR_UNKNOWN_ISSUER" &&
1126 // Only do this check for top-level failures.
1127 window.parent == window
1129 RPMSendAsyncMessage("Browser:PrimeMitm");
1132 document.body.setAttribute("code", failedCertInfo.errorCodeString);
1134 const learnMore = document.getElementById("learnMoreContainer");
1135 learnMore.hidden = false;
1136 const learnMoreLink = document.getElementById("learnMoreLink");
1137 const baseURL = RPMGetFormatURLPref("app.support.baseURL");
1138 learnMoreLink.href = baseURL + "connection-not-secure";
1140 const bodyTitle = document.querySelector(".title-text");
1141 const shortDesc = document.getElementById("errorShortDesc");
1142 const shortDesc2 = document.getElementById("errorShortDesc2");
1144 let whatToDoParts = null;
1146 switch (failedCertInfo.errorCodeString) {
1147 case "SSL_ERROR_BAD_CERT_DOMAIN":
1149 ["p", "certerror-bad-cert-domain-what-can-you-do-about-it"],
1153 case "SEC_ERROR_OCSP_INVALID_SIGNING_CERT": // FIXME - this would have thrown?
1156 case "SEC_ERROR_UNKNOWN_ISSUER":
1158 ["p", "certerror-unknown-issuer-what-can-you-do-about-it-website"],
1161 "certerror-unknown-issuer-what-can-you-do-about-it-contact-admin",
1166 // This error code currently only exists for the Symantec distrust
1167 // in Firefox 63, so we add copy explaining that to the user.
1168 // In case of future distrusts of that scale we might need to add
1169 // additional parameters that allow us to identify the affected party
1170 // without replicating the complex logic from certverifier code.
1171 case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED": {
1172 document.l10n.setAttributes(
1174 "cert-error-symantec-distrust-description",
1175 { hostname: HOST_NAME }
1178 // FIXME - this does nothing
1179 const adminDesc = document.createElement("p");
1180 document.l10n.setAttributes(
1182 "cert-error-symantec-distrust-admin"
1185 learnMoreLink.href = baseURL + "symantec-warning";
1189 case "MOZILLA_PKIX_ERROR_MITM_DETECTED": {
1190 const autoEnabledEnterpriseRoots = RPMGetBoolPref(
1191 "security.enterprise_roots.auto-enabled",
1194 if (mitmPrimingEnabled && autoEnabledEnterpriseRoots) {
1195 RPMSendAsyncMessage("Browser:ResetEnterpriseRootsPref");
1198 learnMoreLink.href = baseURL + "security-error";
1200 document.l10n.setAttributes(bodyTitle, "certerror-mitm-title");
1202 document.l10n.setAttributes(shortDesc, "certerror-mitm", {
1203 hostname: HOST_NAME,
1204 mitm: getMitmName(failedCertInfo),
1208 ? "certerror-mitm-what-can-you-do-about-it-attack-sts"
1209 : "certerror-mitm-what-can-you-do-about-it-attack";
1211 ["li", "certerror-mitm-what-can-you-do-about-it-antivirus"],
1212 ["li", "certerror-mitm-what-can-you-do-about-it-corporate"],
1213 ["li", id3, { mitm: getMitmName(failedCertInfo) }],
1218 case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
1219 learnMoreLink.href = baseURL + "security-error";
1222 // In case the certificate expired we make sure the system clock
1223 // matches the remote-settings service (blocklist via Kinto) ping time
1224 // and is not before the build date.
1225 case "SEC_ERROR_EXPIRED_CERTIFICATE":
1226 case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
1227 case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE":
1228 case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE": {
1229 learnMoreLink.href = baseURL + "time-errors";
1231 // We check against the remote-settings server time first if available, because that allows us
1232 // to give the user an approximation of what the correct time is.
1233 const difference = RPMGetIntPref(
1234 "services.settings.clock_skew_seconds",
1238 RPMGetIntPref("services.settings.last_update_seconds", 0) * 1000;
1240 // This is set to true later if the user's system clock is at fault for this error.
1241 let clockSkew = false;
1243 const now = Date.now();
1245 notBefore: failedCertInfo.certValidityRangeNotBefore,
1246 notAfter: failedCertInfo.certValidityRangeNotAfter,
1248 const approximateDate = now - difference * 1000;
1249 // If the difference is more than a day, we last fetched the date in the last 5 days,
1250 // and adjusting the date per the interval would make the cert valid, warn the user:
1252 Math.abs(difference) > 60 * 60 * 24 &&
1253 now - lastFetched <= 60 * 60 * 24 * 5 * 1000 &&
1254 certRange.notBefore < approximateDate &&
1255 certRange.notAfter > approximateDate
1258 // If there is no clock skew with Kinto servers, check against the build date.
1259 // (The Kinto ping could have happened when the time was still right, or not at all)
1261 const appBuildID = RPMGetAppBuildID();
1262 const year = parseInt(appBuildID.substr(0, 4), 10);
1263 const month = parseInt(appBuildID.substr(4, 2), 10) - 1;
1264 const day = parseInt(appBuildID.substr(6, 2), 10);
1266 const buildDate = new Date(year, month, day);
1268 // We don't check the notBefore of the cert with the build date,
1269 // as it is of course almost certain that it is now later than the build date,
1270 // so we shouldn't exclude the possibility that the cert has become valid
1271 // since the build date.
1272 if (buildDate > now && new Date(certRange.notAfter) > buildDate) {
1278 document.body.classList.add("clockSkewError");
1279 document.l10n.setAttributes(bodyTitle, "clockSkewError-title");
1280 document.l10n.setAttributes(shortDesc, "neterror-clock-skew-error", {
1281 hostname: HOST_NAME,
1284 document.getElementById("returnButton").hidden = true;
1285 document.getElementById("certErrorTryAgainButton").hidden = false;
1286 document.getElementById("advancedButton").hidden = true;
1288 document.getElementById("advancedPanelReturnButton").hidden = true;
1289 document.getElementById("advancedPanelTryAgainButton").hidden = false;
1290 document.getElementById("exceptionDialogButton").hidden = true;
1294 document.l10n.setAttributes(shortDesc, "certerror-expired-cert-intro", {
1295 hostname: HOST_NAME,
1298 // The secondary description mentions expired certificates explicitly
1299 // and should only be shown if the certificate has actually expired
1300 // instead of being not yet valid.
1301 if (failedCertInfo.errorCodeString == "SEC_ERROR_EXPIRED_CERTIFICATE") {
1302 const sd2Id = gHasSts
1303 ? "certerror-expired-cert-sts-second-para"
1304 : "certerror-expired-cert-second-para";
1305 document.l10n.setAttributes(shortDesc2, sd2Id);
1307 Math.abs(difference) <= 60 * 60 * 24 &&
1308 now - lastFetched <= 60 * 60 * 24 * 5 * 1000
1311 ["p", "certerror-bad-cert-domain-what-can-you-do-about-it"],
1319 "certerror-expired-cert-what-can-you-do-about-it-clock",
1320 { hostname: HOST_NAME, now },
1324 "certerror-expired-cert-what-can-you-do-about-it-contact-website",
1331 if (whatToDoParts) {
1332 setNetErrorMessageFromParts(
1333 document.getElementById("errorWhatToDoText"),
1336 document.getElementById("errorWhatToDo").hidden = false;
1340 async function getSubjectAltNames(failedCertInfo) {
1341 const serverCertBase64 = failedCertInfo.certChainStrings[0];
1342 const parsed = await parse(pemToDER(serverCertBase64));
1343 const subjectAltNamesExtension = parsed.ext.san;
1344 const subjectAltNames = [];
1345 if (subjectAltNamesExtension) {
1346 for (let [key, value] of subjectAltNamesExtension.altNames) {
1347 if (key === "DNS Name" && value.length) {
1348 subjectAltNames.push(value);
1352 return subjectAltNames;
1355 // The optional argument is only here for testing purposes.
1356 function setTechnicalDetailsOnCertError(
1357 failedCertInfo = document.getFailedCertSecurityInfo()
1359 let technicalInfo = document.getElementById("badCertTechnicalInfo");
1360 technicalInfo.textContent = "";
1362 function addLabel(l10nId, args = null, attrs = null) {
1363 let elem = document.createElement("label");
1364 technicalInfo.appendChild(elem);
1366 let newLines = document.createTextNode("\n \n");
1367 technicalInfo.appendChild(newLines);
1370 let link = document.createElement("a");
1371 for (let [attr, value] of Object.entries(attrs)) {
1372 link.setAttribute(attr, value);
1374 elem.appendChild(link);
1377 document.l10n.setAttributes(elem, l10nId, args);
1380 function addErrorCodeLink() {
1382 "cert-error-code-prefix-link",
1383 { error: failedCertInfo.errorCodeString },
1385 title: failedCertInfo.errorCodeString,
1387 "data-l10n-name": "error-code-link",
1388 "data-telemetry-id": "error_code_link",
1389 href: "#certificateErrorDebugInformation",
1393 // We're attaching the event listener to the parent element and not on
1394 // the errorCodeLink itself because event listeners cannot be attached
1395 // to fluent DOM overlays.
1396 technicalInfo.addEventListener("click", event => {
1397 if (event.target.id === "errorCode") {
1398 event.preventDefault();
1399 toggleCertErrorDebugInfoVisibility();
1400 recordClickTelemetry(event);
1405 let hostname = HOST_NAME;
1406 const { port } = document.location;
1407 if (port && port != 443) {
1408 hostname += ":" + port;
1411 switch (failedCertInfo.overridableErrorCategory) {
1413 switch (failedCertInfo.errorCodeString) {
1414 case "MOZILLA_PKIX_ERROR_MITM_DETECTED":
1415 addLabel("cert-error-mitm-intro");
1416 addLabel("cert-error-mitm-mozilla");
1417 addLabel("cert-error-mitm-connection");
1419 case "SEC_ERROR_UNKNOWN_ISSUER":
1420 addLabel("cert-error-trust-unknown-issuer-intro");
1421 addLabel("cert-error-trust-unknown-issuer", { hostname });
1423 case "SEC_ERROR_CA_CERT_INVALID":
1424 addLabel("cert-error-intro", { hostname });
1425 addLabel("cert-error-trust-cert-invalid");
1427 case "SEC_ERROR_UNTRUSTED_ISSUER":
1428 addLabel("cert-error-intro", { hostname });
1429 addLabel("cert-error-trust-untrusted-issuer");
1431 case "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED":
1432 addLabel("cert-error-intro", { hostname });
1433 addLabel("cert-error-trust-signature-algorithm-disabled");
1435 case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
1436 addLabel("cert-error-intro", { hostname });
1437 addLabel("cert-error-trust-expired-issuer");
1439 case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
1440 addLabel("cert-error-intro", { hostname });
1441 addLabel("cert-error-trust-self-signed");
1443 case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED":
1444 addLabel("cert-error-intro", { hostname });
1445 addLabel("cert-error-trust-symantec");
1448 addLabel("cert-error-intro", { hostname });
1449 addLabel("cert-error-untrusted-default");
1454 case "expired-or-not-yet-valid": {
1455 const notBefore = failedCertInfo.validNotBefore;
1456 const notAfter = failedCertInfo.validNotAfter;
1457 if (notBefore && Date.now() < notAfter) {
1458 addLabel("cert-error-not-yet-valid-now", {
1460 "not-before-local-time": formatter.format(new Date(notBefore)),
1463 addLabel("cert-error-expired-now", {
1465 "not-after-local-time": formatter.format(new Date(notAfter)),
1472 case "domain-mismatch":
1473 getSubjectAltNames(failedCertInfo).then(subjectAltNames => {
1474 if (!subjectAltNames.length) {
1475 addLabel("cert-error-domain-mismatch", { hostname });
1476 } else if (subjectAltNames.length > 1) {
1477 const names = subjectAltNames.join(", ");
1478 addLabel("cert-error-domain-mismatch-multiple", {
1480 "subject-alt-names": names,
1483 const altName = subjectAltNames[0];
1485 // If the alt name is a wildcard domain ("*.example.com")
1486 // let's use "www" instead. "*.example.com" isn't going to
1487 // get anyone anywhere useful. bug 432491
1488 const okHost = altName.replace(/^\*\./, "www.");
1490 // Let's check if we want to make this a link.
1493 * example.com uses an invalid security certificate.
1495 * The certificate is only valid for www.example.com
1497 * Make sure to include the "." ahead of thisHost so that a
1498 * MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
1500 * We'd normally just use a RegExp here except that we lack a
1501 * library function to escape them properly (bug 248062), and
1502 * domain names are famous for having '.' characters in them,
1503 * which would allow spurious and possibly hostile matches.
1505 okHost.endsWith("." + HOST_NAME) ||
1507 * browser.garage.maemo.org uses an invalid security certificate.
1509 * The certificate is only valid for garage.maemo.org
1511 HOST_NAME.endsWith("." + okHost);
1513 const l10nArgs = { hostname, "alt-name": altName };
1515 // Set the link if we want it.
1516 const proto = document.location.protocol + "//";
1517 addLabel("cert-error-domain-mismatch-single", l10nArgs, {
1518 href: proto + okHost,
1519 "data-l10n-name": "domain-mismatch-link",
1520 id: "cert_domain_link",
1523 // If we set a link, meaning there's something helpful for
1524 // the user here, expand the section by default
1525 if (getCSSClass() != "expertBadCert") {
1526 revealAdvancedPanelSlowlyAsync();
1529 addLabel("cert-error-domain-mismatch-single-nolink", l10nArgs);
1537 getFailedCertificatesAsPEMString().then(pemString => {
1538 const errorText = document.getElementById("certificateErrorText");
1539 errorText.textContent = pemString;
1543 /* Only focus if we're the toplevel frame; otherwise we
1544 don't want to call attention to ourselves!
1546 function setFocus(selector, position = "afterbegin") {
1547 if (window.top == window) {
1548 var button = document.querySelector(selector);
1549 button.parentNode.insertAdjacentElement(position, button);
1550 // It's possible setFocus was called via the DOMContentLoaded event
1551 // handler and that the button has no frame. Things without a frame cannot
1552 // be focused. We use a requestAnimationFrame to queue up the focus to occur
1553 // once the button has its frame.
1554 requestAnimationFrame(() => {
1555 button.focus({ focusVisible: false });
1560 for (let button of document.querySelectorAll(".try-again")) {
1561 button.addEventListener("click", function () {
1568 // Dispatch this event so tests can detect that we finished loading the error page.
1569 document.dispatchEvent(new CustomEvent("AboutNetErrorLoad", { bubbles: true }));