Bug 1885489 - Part 5: Add SnapshotIterator::readInt32(). r=iain
[gecko.git] / toolkit / content / aboutNetError.mjs
blob1c733d5dbba0c20ca07744fb77eadfffb806a370
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 */
8 import {
9   parse,
10   pemToDER,
11 } from "chrome://global/content/certviewer/certDecoder.mjs";
13 const formatter = new Intl.DateTimeFormat();
15 const HOST_NAME = getHostName();
17 function getHostName() {
18   try {
19     return new URL(RPMGetInnerMostURI(document.location.href)).hostname;
20   } catch (error) {
21     console.error("Could not parse URL", error);
22   }
23   return "";
26 // Used to check if we have a specific localized message for an error.
27 const KNOWN_ERROR_TITLE_IDS = new Set([
28   // Error titles:
29   "connectionFailure-title",
30   "deniedPortAccess-title",
31   "dnsNotFound-title",
32   "dns-not-found-trr-only-title2",
33   "fileNotFound-title",
34   "fileAccessDenied-title",
35   "generic-title",
36   "captivePortal-title",
37   "malformedURI-title",
38   "netInterrupt-title",
39   "notCached-title",
40   "netOffline-title",
41   "contentEncodingError-title",
42   "unsafeContentType-title",
43   "netReset-title",
44   "netTimeout-title",
45   "unknownProtocolFound-title",
46   "proxyConnectFailure-title",
47   "proxyResolveFailure-title",
48   "redirectLoop-title",
49   "unknownSocketType-title",
50   "nssFailure2-title",
51   "csp-xfo-error-title",
52   "corruptedContentError-title",
53   "sslv3Used-title",
54   "inadequateSecurityError-title",
55   "blockedByPolicy-title",
56   "clockSkewError-title",
57   "networkProtocolError-title",
58   "nssBadCert-title",
59   "nssBadCert-sts-title",
60   "certerror-mitm-title",
61 ]);
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:
69 //   e - the error code
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.
113  */
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;
127   document
128     .getElementById("prefResetButton")
129     .addEventListener("click", function resetPreferences() {
130       RPMSendAsyncMessage("Browser:ResetSSLPreferences");
131     });
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;
141   }
142   debugInfo.hidden = !shouldShow;
143   if (shouldShow) {
144     copyButton.scrollIntoView({ block: "start", behavior: "smooth" });
145     copyButton.focus();
146   }
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
154   document
155     .getElementById("advancedButton")
156     .addEventListener("click", togglePanelVisibility);
158   function togglePanelVisibility() {
159     if (panel.hidden) {
160       // Reveal
161       revealAdvancedPanelSlowlyAsync();
163       // send event to trigger telemetry ping
164       document.dispatchEvent(
165         new CustomEvent("AboutNetErrorUIExpanded", { bubbles: true })
166       );
167     } else {
168       // Hide
169       panel.hidden = true;
170     }
171   }
173   if (getCSSClass() == "expertBadCert") {
174     revealAdvancedPanelSlowlyAsync();
175   }
178 async function revealAdvancedPanelSlowlyAsync() {
179   const badCertAdvancedPanel = document.getElementById("badCertAdvancedPanel");
180   const exceptionDialogButton = document.getElementById(
181     "exceptionDialogButton"
182   );
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;
193   // -
195   if (exceptionDialogButton.resetReveal) {
196     exceptionDialogButton.resetReveal(); // Reset if previous is pending.
197   }
198   let wasReset = false;
199   exceptionDialogButton.resetReveal = () => {
200     wasReset = true;
201   };
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);
213   }
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));
219   if (wasReset) {
220     return;
221   }
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;
233   }
234   if (gHasSts) {
235     const stsExplanation = document.getElementById("badStsCertExplanation");
236     document.l10n.setAttributes(
237       stsExplanation,
238       "certerror-what-should-i-do-bad-sts-cert-explanation",
239       { hostname: HOST_NAME }
240     );
241     stsExplanation.hidden = false;
243     document.l10n.setAttributes(
244       document.getElementById("returnButton"),
245       "neterror-return-to-previous-page-button"
246     );
247     document.l10n.setAttributes(
248       document.getElementById("advancedPanelReturnButton"),
249       "neterror-return-to-previous-page-button"
250     );
251   }
254 function recordTRREventTelemetry(
255   warningPageType,
256   trrMode,
257   trrDomain,
258   skipReason
259 ) {
260   RPMRecordTelemetryEvent(
261     "security.doh.neterror",
262     "load",
263     "dohwarning",
264     warningPageType,
265     {
266       mode: trrMode,
267       provider_key: trrDomain,
268       skip_reason: skipReason,
269     }
270   );
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",
280         "click",
281         telemetryId,
282         warningPageType,
283         {
284           mode: trrMode,
285           provider_key: trrDomain,
286           skip_reason: skipReason,
287         }
288       );
289     });
290   }
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
298   // to the internet.
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", {
306       supportPageSlug,
307     });
308   }
310   const className = getCSSClass();
311   if (className) {
312     document.body.classList.add(className);
313   }
315   const isTRROnlyFailure = gErrorCode == "dnsNotFound" && RPMIsTRROnlyFailure();
317   let isNativeFallbackWarning = false;
318   if (RPMGetBoolPref("network.trr.display_fallback_warning")) {
319     isNativeFallbackWarning =
320       gErrorCode == "dnsNotFound" && RPMIsNativeFallbackFailure();
321   }
323   const docTitle = document.querySelector("title");
324   const bodyTitle = document.querySelector(".title-text");
325   const shortDesc = document.getElementById("errorShortDesc");
327   if (gIsCertError) {
328     const isStsError = window !== window.top || gHasSts;
329     const errArgs = { hostname: HOST_NAME };
330     if (isCaptive()) {
331       document.l10n.setAttributes(
332         docTitle,
333         "neterror-captive-portal-page-title"
334       );
335       document.l10n.setAttributes(bodyTitle, "captivePortal-title");
336       document.l10n.setAttributes(
337         shortDesc,
338         "neterror-captive-portal",
339         errArgs
340       );
341       initPageCaptivePortal();
342     } else {
343       if (isStsError) {
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);
347       } else {
348         document.l10n.setAttributes(docTitle, "certerror-page-title");
349         document.l10n.setAttributes(bodyTitle, "nssBadCert-title");
350         document.l10n.setAttributes(shortDesc, "certerror-intro", errArgs);
351       }
352       initPageCertError();
353     }
355     initCertErrorPageActions();
356     setTechnicalDetailsOnCertError();
357     return;
358   }
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;
380       break;
382     case "cspBlocked":
383     case "xfoBlocked": {
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", {
395         hostname: HOST_NAME,
396       });
397       longDesc = null;
399       document.getElementById("openInNewWindowContainer").hidden = false;
401       const openInNewWindowButton = document.getElementById(
402         "openInNewWindowButton"
403       );
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();
411       break;
412     }
414     case "dnsNotFound":
415       pageTitleId = "neterror-dns-not-found-title";
416       if (!isTRROnlyFailure) {
417         RPMCheckAlternateHostAvailable();
418       }
420       break;
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;
425       break;
427     case "malformedURI":
428       pageTitleId = "neterror-malformed-uri-page-title";
429       // Remove the "Try again" button from pages that don't need it.
430       tryAgain.hidden = true;
431       break;
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",
441         "load",
442         "abouttlserror",
443         errorCode,
444         {
445           is_frame: (window.parent != window).toString(),
446         }
447       );
448       switch (errorCode) {
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");
454         }
455         // fallthrough
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();
465             }
466           });
467           RPMSendAsyncMessage("GetChangedCertPrefs");
468       }
470       break;
471     }
473     case "sslv3Used":
474       learnMore.hidden = false;
475       document.body.className = "certerror";
476       break;
477   }
479   if (!KNOWN_ERROR_TITLE_IDS.has(bodyTitleId)) {
480     console.error("No strings exist for error:", gErrorCode);
481     bodyTitleId = "generic-title";
482   }
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();
496       // enable buttons
497       let trrExceptionButton = document.getElementById("trrExceptionButton");
498       trrExceptionButton.addEventListener("click", () => {
499         RPMSendQuery("Browser:AddTRRExcludedDomain", {
500           hostname: HOST_NAME,
501         }).then(() => {
502           retryThis(trrExceptionButton);
503         });
504       });
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;
513       }
514       let trrSettingsButton = document.getElementById("trrSettingsButton");
515       trrSettingsButton.addEventListener("click", () => {
516         RPMSendAsyncMessage("OpenTRRPreferences");
517       });
518       trrSettingsButton.hidden = false;
519       let message = document.getElementById("trrOnlyMessage");
520       document.l10n.setAttributes(
521         message,
522         "neterror-dns-not-found-trr-only-reason2",
523         {
524           hostname: HOST_NAME,
525         }
526       );
528       let descriptionTag = "neterror-dns-not-found-trr-unknown-problem";
529       let args = { trrDomain: RPMGetTRRDomain() };
530       if (
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"
537       ) {
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";
541       } else if (
542         skipReason == "TRR_IS_OFFLINE" ||
543         skipReason == "TRR_NO_CONNECTIVITY"
544       ) {
545         descriptionTag = "neterror-dns-not-found-trr-offline";
546       } else if (
547         skipReason == "TRR_NO_ANSWERS" ||
548         skipReason == "TRR_NXDOMAIN" ||
549         skipReason == "TRR_RCODE_FAIL"
550       ) {
551         descriptionTag = "neterror-dns-not-found-trr-unknown-host2";
552       } else if (
553         skipReason == "TRR_DECODE_FAILED" ||
554         skipReason == "TRR_SERVER_RESPONSE_ERR"
555       ) {
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";
559       }
561       let trrMode = RPMGetIntPref("network.trr.mode").toString();
562       recordTRREventTelemetry(
563         "TRROnlyFailure",
564         trrMode,
565         args.trrDomain,
566         skipReason
567       );
569       let description = document.getElementById("trrOnlyDescription");
570       document.l10n.setAttributes(description, descriptionTag, args);
572       const trrLearnMoreContainer = document.getElementById(
573         "trrLearnMoreContainer"
574       );
575       trrLearnMoreContainer.hidden = false;
576       let trrOnlyLearnMoreLink = document.getElementById(
577         "trrOnlylearnMoreLink"
578       );
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",
587             "click",
588             "settings_button",
589             "TRROnlyFailure",
590             {
591               mode: trrMode,
592               provider_key: args.trrDomain,
593               skip_reason: skipReason,
594             }
595           );
596         });
597       } else {
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("_", "-");
603       }
605       let div = document.getElementById("trrOnlyContainer");
606       div.hidden = false;
608       return;
609     } else if (isNativeFallbackWarning) {
610       showNativeFallbackWarning();
611       return;
612     }
613   }
615   document.l10n.setAttributes(docTitle, pageTitleId);
616   document.l10n.setAttributes(bodyTitle, bodyTitleId);
618   shortDesc.textContent = getDescription();
619   setFocus("#netErrorButtonContainer > .try-again");
621   if (longDesc) {
622     const parts = getNetErrorDescParts();
623     setNetErrorMessageFromParts(longDesc, parts);
624   }
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"
647   );
648   nativeFallbackIgnoreButton.addEventListener("click", () => {
649     RPMSetPref("network.trr.display_fallback_warning", false);
650     retryThis(nativeFallbackIgnoreButton);
651   });
653   let continueThisTimeButton = document.getElementById(
654     "nativeFallbackContinueThisTimeButton"
655   );
656   continueThisTimeButton.addEventListener("click", () => {
657     RPMSetTRRDisabledLoadFlags();
658     document.location.reload();
659   });
660   continueThisTimeButton.hidden = false;
662   nativeFallbackIgnoreButton.hidden = false;
663   let message = document.getElementById("nativeFallbackMessage");
664   document.l10n.setAttributes(
665     message,
666     "neterror-dns-not-found-native-fallback-reason2",
667     {
668       hostname: HOST_NAME,
669     }
670   );
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";
679   }
681   let description = document.getElementById("nativeFallbackDescription");
682   document.l10n.setAttributes(description, descriptionTag, args);
684   let learnMoreContainer = document.getElementById(
685     "nativeFallbackLearnMoreContainer"
686   );
687   learnMoreContainer.hidden = false;
689   let learnMoreLink = document.getElementById("nativeFallbackLearnMoreLink");
690   learnMoreLink.href =
691     RPMGetFormatURLPref("network.trr_ui.skip_reason_learn_more_url") +
692     skipReason.toLowerCase().replaceAll("_", "-");
694   let div = document.getElementById("nativeFallbackContainer");
695   div.hidden = false;
697   recordTRREventTelemetry(
698     "NativeFallbackWarning",
699     RPMGetIntPref("network.trr.mode").toString(),
700     args.trrDomain,
701     skipReason
702   );
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
709  */
710 function setNetErrorMessageFromParts(parentElement, parts) {
711   let list = null;
713   for (let [tag, l10nId, l10nArgs] of parts) {
714     const elem = document.createElement(tag);
715     elem.dataset.l10nId = l10nId;
716     if (l10nArgs) {
717       elem.dataset.l10nArgs = JSON.stringify(l10nArgs);
718     }
720     if (tag === "li") {
721       if (!list) {
722         list = document.createElement("ul");
723         parentElement.appendChild(list);
724       }
725       list.appendChild(elem);
726     } else {
727       if (list) {
728         list = null;
729       }
730       parentElement.appendChild(elem);
731     }
732   }
736  * Returns an array of tuples determining the parts of an error message:
737  * - HTML tag name
738  * - l10n id
739  * - l10n args (optional)
741  * @returns { Array<["li" | "p" | "span", string, Record<string, string> | undefined]> }
742  */
743 function getNetErrorDescParts() {
744   switch (gErrorCode) {
745     case "connectionFailure":
746     case "netInterrupt":
747     case "netReset":
748     case "netTimeout":
749       return [
750         ["li", "neterror-load-error-try-again"],
751         ["li", "neterror-load-error-connection"],
752         ["li", "neterror-load-error-firewall"],
753       ];
755     case "blockedByPolicy":
756     case "deniedPortAccess":
757     case "malformedURI":
758       return [];
760     case "captivePortal":
761       return [["p", ""]];
762     case "contentEncodingError":
763       return [["li", "neterror-content-encoding-error"]];
764     case "corruptedContentErrorv2":
765       return [
766         ["p", "neterror-corrupted-content-intro"],
767         ["li", "neterror-corrupted-content-contact-website"],
768       ];
769     case "dnsNotFound":
770       return [
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"],
775       ];
776     case "fileAccessDenied":
777       return [["li", "neterror-access-denied"]];
778     case "fileNotFound":
779       return [
780         ["li", "neterror-file-not-found-filename"],
781         ["li", "neterror-file-not-found-moved"],
782       ];
783     case "inadequateSecurityError":
784       return [
785         ["p", "neterror-inadequate-security-intro", { hostname: HOST_NAME }],
786         ["p", "neterror-inadequate-security-code"],
787       ];
788     case "mitm": {
789       const failedCertInfo = document.getFailedCertSecurityInfo();
790       const errArgs = {
791         hostname: HOST_NAME,
792         mitm: getMitmName(failedCertInfo),
793       };
794       return [["span", "certerror-mitm", errArgs]];
795     }
796     case "netOffline":
797       return [["li", "neterror-net-offline"]];
798     case "networkProtocolError":
799       return [
800         ["p", "neterror-network-protocol-error-intro"],
801         ["li", "neterror-network-protocol-error-contact-website"],
802       ];
803     case "notCached":
804       return [
805         ["p", "neterror-not-cached-intro"],
806         ["li", "neterror-not-cached-sensitive"],
807         ["li", "neterror-not-cached-try-again"],
808       ];
809     case "nssFailure2":
810       return [
811         ["li", "neterror-nss-failure-not-verified"],
812         ["li", "neterror-nss-failure-contact-website"],
813       ];
814     case "proxyConnectFailure":
815       return [
816         ["li", "neterror-proxy-connect-failure-settings"],
817         ["li", "neterror-proxy-connect-failure-contact-admin"],
818       ];
819     case "proxyResolveFailure":
820       return [
821         ["li", "neterror-proxy-resolve-failure-settings"],
822         ["li", "neterror-proxy-resolve-failure-connection"],
823         ["li", "neterror-proxy-resolve-failure-firewall"],
824       ];
825     case "redirectLoop":
826       return [["li", "neterror-redirect-loop"]];
827     case "sslv3Used":
828       return [["span", "neterror-sslv3-used"]];
829     case "unknownProtocolFound":
830       return [["li", "neterror-unknown-protocol"]];
831     case "unknownSocketType":
832       return [
833         ["li", "neterror-unknown-socket-type-psm-installed"],
834         ["li", "neterror-unknown-socket-type-server-config"],
835       ];
836     case "unsafeContentType":
837       return [["li", "neterror-unsafe-content-type"]];
839     default:
840       return [["p", "neterror-generic-error"]];
841   }
844 function setNetErrorMessageFromCode() {
845   let errorCode;
846   try {
847     errorCode = document.getNetErrorInfo().errorCodeString;
848   } catch (ex) {
849     // We don't have a securityInfo when this is for example a DNS error.
850     return;
851   }
853   let errorMessage;
854   if (errorCode) {
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);
859     }
861     const shortDesc2 = document.getElementById("errorShortDesc2");
862     document.l10n.setAttributes(shortDesc2, "cert-error-code-prefix", {
863       error: errorCode,
864     });
865   } else {
866     console.warn("This error page has no error code in its security info");
867   }
869   let hostname = HOST_NAME;
870   const { port } = document.location;
871   if (port && port != 443) {
872     hostname += ":" + port;
873   }
875   const shortDesc = document.getElementById("errorShortDesc");
876   document.l10n.setAttributes(shortDesc, "cert-error-ssl-connection-error", {
877     errorMessage: errorMessage ?? errorCode ?? "",
878     hostname,
879   });
882 function setupBlockingReportingUI() {
883   let checkbox = document.getElementById("automaticallyReportBlockingInFuture");
885   let reportingAutomatic = RPMGetBoolPref(
886     "security.xfocsp.errorReporting.automatic"
887   );
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.
894     if (checked) {
895       reportBlockingError();
896     }
897   });
899   let reportingEnabled = RPMGetBoolPref(
900     "security.xfocsp.errorReporting.enabled"
901   );
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();
909     }
910   }
913 function reportBlockingError() {
914   // We only report if we are in a frame.
915   if (window === window.top) {
916     return;
917   }
919   let err = gErrorCode;
920   // Ensure we only deal with XFO and CSP here.
921   if (!["xfoBlocked", "cspBlocked"].includes(err)) {
922     return;
923   }
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
935   // early.
936   if (err === "cspBlocked" && !csp_header) {
937     return;
938   }
940   let xfoAndCspInfo = {
941     error_type: err === "xfoBlocked" ? "xfo" : "csp",
942     xfo_header,
943     csp_header,
944   };
946   // Trimming the tail colon symbol.
947   let scheme = document.location.protocol.slice(0, -1);
949   RPMSendAsyncMessage("ReportBlockingError", {
950     scheme,
951     host: document.location.host,
952     port: parseInt(document.location.port) || -1,
953     path: document.location.pathname,
954     xfoAndCspInfo,
955   });
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");
965   });
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();
975   });
978 function initPageCertError() {
979   document.body.classList.add("certerror");
981   setFocus("#returnButton");
982   setupAdvancedButton();
983   disallowCertOverridesIfNeeded();
985   const hideAddExceptionButton = RPMGetBoolPref(
986     "security.certerror.hideAddException",
987     false
988   );
989   if (hideAddExceptionButton) {
990     document.getElementById("exceptionDialogButton").hidden = true;
991   }
993   const els = document.querySelectorAll("[data-telemetry-id]");
994   for (let el of els) {
995     el.addEventListener("click", recordClickTelemetry);
996   }
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",
1004     "load",
1005     "aboutcerterror",
1006     errorCode,
1007     {
1008       has_sts: gHasSts.toString(),
1009       is_frame: (window.parent != window).toString(),
1010     }
1011   );
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",
1025     "click",
1026     telemetryId,
1027     errorCode,
1028     {
1029       has_sts: gHasSts.toString(),
1030       is_frame: (window.parent != window).toString(),
1031     }
1032   );
1035 function initCertErrorPageActions() {
1036   document.getElementById(
1037     "certErrorAndCaptivePortalButtonContainer"
1038   ).hidden = false;
1039   document
1040     .getElementById("returnButton")
1041     .addEventListener("click", onReturnButtonClick);
1042   document
1043     .getElementById("advancedPanelReturnButton")
1044     .addEventListener("click", onReturnButtonClick);
1045   document
1046     .getElementById("copyToClipboardTop")
1047     .addEventListener("click", copyPEMToClipboard);
1048   document
1049     .getElementById("copyToClipboardBottom")
1050     .addEventListener("click", copyPEMToClipboard);
1051   document
1052     .getElementById("exceptionDialogButton")
1053     .addEventListener("click", addCertException);
1056 function addCertException() {
1057   const isPermanent =
1058     !RPMIsWindowPrivate() &&
1059     RPMGetBoolPref("security.certerrors.permanentOverride");
1060   document.addCertException(!isPermanent).then(
1061     () => {
1062       location.reload();
1063     },
1064     () => {}
1065   );
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" },
1088     ]);
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" +
1096       wrapped +
1097       "\r\n-----END CERTIFICATE-----\r\n";
1098   }
1100   let details =
1101     locationUrl +
1102     "\r\n\r\n" +
1103     errorMessage +
1104     "\r\n\r\n" +
1105     hstsLabel +
1106     "\r\n" +
1107     hpkpLabel +
1108     "\r\n\r\n" +
1109     failedChainLabel +
1110     "\r\n\r\n" +
1111     failedChainCertificates;
1112   return details;
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"
1122   );
1123   if (
1124     mitmPrimingEnabled &&
1125     failedCertInfo.errorCodeString == "SEC_ERROR_UNKNOWN_ISSUER" &&
1126     // Only do this check for top-level failures.
1127     window.parent == window
1128   ) {
1129     RPMSendAsyncMessage("Browser:PrimeMitm");
1130   }
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":
1148       whatToDoParts = [
1149         ["p", "certerror-bad-cert-domain-what-can-you-do-about-it"],
1150       ];
1151       break;
1153     case "SEC_ERROR_OCSP_INVALID_SIGNING_CERT": // FIXME - this would have thrown?
1154       break;
1156     case "SEC_ERROR_UNKNOWN_ISSUER":
1157       whatToDoParts = [
1158         ["p", "certerror-unknown-issuer-what-can-you-do-about-it-website"],
1159         [
1160           "p",
1161           "certerror-unknown-issuer-what-can-you-do-about-it-contact-admin",
1162         ],
1163       ];
1164       break;
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(
1173         shortDesc2,
1174         "cert-error-symantec-distrust-description",
1175         { hostname: HOST_NAME }
1176       );
1178       // FIXME - this does nothing
1179       const adminDesc = document.createElement("p");
1180       document.l10n.setAttributes(
1181         adminDesc,
1182         "cert-error-symantec-distrust-admin"
1183       );
1185       learnMoreLink.href = baseURL + "symantec-warning";
1186       break;
1187     }
1189     case "MOZILLA_PKIX_ERROR_MITM_DETECTED": {
1190       const autoEnabledEnterpriseRoots = RPMGetBoolPref(
1191         "security.enterprise_roots.auto-enabled",
1192         false
1193       );
1194       if (mitmPrimingEnabled && autoEnabledEnterpriseRoots) {
1195         RPMSendAsyncMessage("Browser:ResetEnterpriseRootsPref");
1196       }
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),
1205       });
1207       const id3 = gHasSts
1208         ? "certerror-mitm-what-can-you-do-about-it-attack-sts"
1209         : "certerror-mitm-what-can-you-do-about-it-attack";
1210       whatToDoParts = [
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) }],
1214       ];
1215       break;
1216     }
1218     case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
1219       learnMoreLink.href = baseURL + "security-error";
1220       break;
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",
1235         0
1236       );
1237       const lastFetched =
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();
1244       const certRange = {
1245         notBefore: failedCertInfo.certValidityRangeNotBefore,
1246         notAfter: failedCertInfo.certValidityRangeNotAfter,
1247       };
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:
1251       if (
1252         Math.abs(difference) > 60 * 60 * 24 &&
1253         now - lastFetched <= 60 * 60 * 24 * 5 * 1000 &&
1254         certRange.notBefore < approximateDate &&
1255         certRange.notAfter > approximateDate
1256       ) {
1257         clockSkew = true;
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)
1260       } else {
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) {
1273           clockSkew = true;
1274         }
1275       }
1277       if (clockSkew) {
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,
1282           now,
1283         });
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;
1291         break;
1292       }
1294       document.l10n.setAttributes(shortDesc, "certerror-expired-cert-intro", {
1295         hostname: HOST_NAME,
1296       });
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);
1306         if (
1307           Math.abs(difference) <= 60 * 60 * 24 &&
1308           now - lastFetched <= 60 * 60 * 24 * 5 * 1000
1309         ) {
1310           whatToDoParts = [
1311             ["p", "certerror-bad-cert-domain-what-can-you-do-about-it"],
1312           ];
1313         }
1314       }
1316       whatToDoParts ??= [
1317         [
1318           "p",
1319           "certerror-expired-cert-what-can-you-do-about-it-clock",
1320           { hostname: HOST_NAME, now },
1321         ],
1322         [
1323           "p",
1324           "certerror-expired-cert-what-can-you-do-about-it-contact-website",
1325         ],
1326       ];
1327       break;
1328     }
1329   }
1331   if (whatToDoParts) {
1332     setNetErrorMessageFromParts(
1333       document.getElementById("errorWhatToDoText"),
1334       whatToDoParts
1335     );
1336     document.getElementById("errorWhatToDo").hidden = false;
1337   }
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);
1349       }
1350     }
1351   }
1352   return subjectAltNames;
1355 // The optional argument is only here for testing purposes.
1356 function setTechnicalDetailsOnCertError(
1357   failedCertInfo = document.getFailedCertSecurityInfo()
1358 ) {
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);
1369     if (attrs) {
1370       let link = document.createElement("a");
1371       for (let [attr, value] of Object.entries(attrs)) {
1372         link.setAttribute(attr, value);
1373       }
1374       elem.appendChild(link);
1375     }
1377     document.l10n.setAttributes(elem, l10nId, args);
1378   }
1380   function addErrorCodeLink() {
1381     addLabel(
1382       "cert-error-code-prefix-link",
1383       { error: failedCertInfo.errorCodeString },
1384       {
1385         title: failedCertInfo.errorCodeString,
1386         id: "errorCode",
1387         "data-l10n-name": "error-code-link",
1388         "data-telemetry-id": "error_code_link",
1389         href: "#certificateErrorDebugInformation",
1390       }
1391     );
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);
1401       }
1402     });
1403   }
1405   let hostname = HOST_NAME;
1406   const { port } = document.location;
1407   if (port && port != 443) {
1408     hostname += ":" + port;
1409   }
1411   switch (failedCertInfo.overridableErrorCategory) {
1412     case "trust-error":
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");
1418           break;
1419         case "SEC_ERROR_UNKNOWN_ISSUER":
1420           addLabel("cert-error-trust-unknown-issuer-intro");
1421           addLabel("cert-error-trust-unknown-issuer", { hostname });
1422           break;
1423         case "SEC_ERROR_CA_CERT_INVALID":
1424           addLabel("cert-error-intro", { hostname });
1425           addLabel("cert-error-trust-cert-invalid");
1426           break;
1427         case "SEC_ERROR_UNTRUSTED_ISSUER":
1428           addLabel("cert-error-intro", { hostname });
1429           addLabel("cert-error-trust-untrusted-issuer");
1430           break;
1431         case "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED":
1432           addLabel("cert-error-intro", { hostname });
1433           addLabel("cert-error-trust-signature-algorithm-disabled");
1434           break;
1435         case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
1436           addLabel("cert-error-intro", { hostname });
1437           addLabel("cert-error-trust-expired-issuer");
1438           break;
1439         case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
1440           addLabel("cert-error-intro", { hostname });
1441           addLabel("cert-error-trust-self-signed");
1442           break;
1443         case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED":
1444           addLabel("cert-error-intro", { hostname });
1445           addLabel("cert-error-trust-symantec");
1446           break;
1447         default:
1448           addLabel("cert-error-intro", { hostname });
1449           addLabel("cert-error-untrusted-default");
1450       }
1451       addErrorCodeLink();
1452       break;
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", {
1459           hostname,
1460           "not-before-local-time": formatter.format(new Date(notBefore)),
1461         });
1462       } else {
1463         addLabel("cert-error-expired-now", {
1464           hostname,
1465           "not-after-local-time": formatter.format(new Date(notAfter)),
1466         });
1467       }
1468       addErrorCodeLink();
1469       break;
1470     }
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", {
1479             hostname,
1480             "subject-alt-names": names,
1481           });
1482         } else {
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.
1491           const showLink =
1492             /* case #1:
1493              * example.com uses an invalid security certificate.
1494              *
1495              * The certificate is only valid for www.example.com
1496              *
1497              * Make sure to include the "." ahead of thisHost so that a
1498              * MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
1499              *
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.
1504              */
1505             okHost.endsWith("." + HOST_NAME) ||
1506             /* case #2:
1507              * browser.garage.maemo.org uses an invalid security certificate.
1508              *
1509              * The certificate is only valid for garage.maemo.org
1510              */
1511             HOST_NAME.endsWith("." + okHost);
1513           const l10nArgs = { hostname, "alt-name": altName };
1514           if (showLink) {
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",
1521             });
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();
1527             }
1528           } else {
1529             addLabel("cert-error-domain-mismatch-single-nolink", l10nArgs);
1530           }
1531         }
1532         addErrorCodeLink();
1533       });
1534       break;
1535   }
1537   getFailedCertificatesAsPEMString().then(pemString => {
1538     const errorText = document.getElementById("certificateErrorText");
1539     errorText.textContent = pemString;
1540   });
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 });
1556     });
1557   }
1560 for (let button of document.querySelectorAll(".try-again")) {
1561   button.addEventListener("click", function () {
1562     retryThis(this);
1563   });
1566 initPage();
1568 // Dispatch this event so tests can detect that we finished loading the error page.
1569 document.dispatchEvent(new CustomEvent("AboutNetErrorLoad", { bubbles: true }));