summaryrefslogtreecommitdiffstats
path: root/toolkit/actors/NetErrorParent.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/actors/NetErrorParent.sys.mjs353
1 files changed, 353 insertions, 0 deletions
diff --git a/toolkit/actors/NetErrorParent.sys.mjs b/toolkit/actors/NetErrorParent.sys.mjs
new file mode 100644
index 0000000000..7d1f0f3f0f
--- /dev/null
+++ b/toolkit/actors/NetErrorParent.sys.mjs
@@ -0,0 +1,353 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
+import { TelemetryController } from "resource://gre/modules/TelemetryController.sys.mjs";
+
+const PREF_SSL_IMPACT_ROOTS = [
+ "security.tls.version.",
+ "security.ssl3.",
+ "security.tls13.",
+];
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+});
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "HomePage",
+ "resource:///modules/HomePage.jsm"
+);
+
+class CaptivePortalObserver {
+ constructor(actor) {
+ this.actor = actor;
+ Services.obs.addObserver(this, "captive-portal-login-abort");
+ Services.obs.addObserver(this, "captive-portal-login-success");
+ }
+
+ stop() {
+ Services.obs.removeObserver(this, "captive-portal-login-abort");
+ Services.obs.removeObserver(this, "captive-portal-login-success");
+ }
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "captive-portal-login-abort":
+ case "captive-portal-login-success":
+ // Send a message to the content when a captive portal is freed
+ // so that error pages can refresh themselves.
+ this.actor.sendAsyncMessage("AboutNetErrorCaptivePortalFreed");
+ break;
+ }
+ }
+}
+
+export class NetErrorParent extends JSWindowActorParent {
+ constructor() {
+ super();
+ this.captivePortalObserver = new CaptivePortalObserver(this);
+ }
+
+ didDestroy() {
+ if (this.captivePortalObserver) {
+ this.captivePortalObserver.stop();
+ }
+ }
+
+ get browser() {
+ return this.browsingContext.top.embedderElement;
+ }
+
+ hasChangedCertPrefs() {
+ let prefSSLImpact = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => {
+ return prefs.concat(Services.prefs.getChildList(root));
+ }, []);
+ for (let prefName of prefSSLImpact) {
+ if (Services.prefs.prefHasUserValue(prefName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ async ReportBlockingError(bcID, scheme, host, port, path, xfoAndCspInfo) {
+ // For reporting X-Frame-Options error and CSP: frame-ancestors errors, We
+ // are collecting 4 pieces of information.
+ // 1. The X-Frame-Options in the response header.
+ // 2. The CSP: frame-ancestors in the response header.
+ // 3. The URI of the frame who triggers this error.
+ // 4. The top-level URI which loads the frame.
+ //
+ // We will exclude the query strings from the reporting URIs.
+ //
+ // More details about the data we send can be found in
+ // https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/xfocsp-error-report-ping.html
+ //
+
+ let topBC = BrowsingContext.get(bcID).top;
+ let topURI = topBC.currentWindowGlobal.documentURI;
+
+ // Get the URLs without query strings.
+ let frame_uri = `${scheme}://${host}${port == -1 ? "" : ":" + port}${path}`;
+ let top_uri = `${topURI.scheme}://${topURI.hostPort}${topURI.filePath}`;
+
+ TelemetryController.submitExternalPing(
+ "xfocsp-error-report",
+ {
+ ...xfoAndCspInfo,
+ frame_hostname: host,
+ top_hostname: topURI.host,
+ frame_uri,
+ top_uri,
+ },
+ { addClientId: false, addEnvironment: false }
+ );
+ }
+
+ /**
+ * Return the default start page for the cases when the user's own homepage is
+ * infected, so we can get them somewhere safe.
+ */
+ getDefaultHomePage(win) {
+ let url;
+ if (
+ !PrivateBrowsingUtils.isWindowPrivate(win) &&
+ AppConstants.MOZ_BUILD_APP == "browser"
+ ) {
+ url = lazy.HomePage.getDefault();
+ }
+ url ||= win.BROWSER_NEW_TAB_URL || "about:blank";
+
+ // If url is a pipe-delimited set of pages, just take the first one.
+ if (url.includes("|")) {
+ url = url.split("|")[0];
+ }
+ return url;
+ }
+
+ /**
+ * Re-direct the browser to the previous page or a known-safe page if no
+ * previous page is found in history. This function is used when the user
+ * browses to a secure page with certificate issues and is presented with
+ * about:certerror. The "Go Back" button should take the user to the previous
+ * or a default start page so that even when their own homepage is on a server
+ * that has certificate errors, we can get them somewhere safe.
+ */
+ goBackFromErrorPage(browser) {
+ if (!browser.canGoBack) {
+ // If the unsafe page is the first or the only one in history, go to the
+ // start page.
+ browser.fixupAndLoadURIString(
+ this.getDefaultHomePage(browser.ownerGlobal),
+ {
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ } else {
+ browser.goBack();
+ }
+ }
+
+ /**
+ * This function does a canary request to a reliable, maintained endpoint, in
+ * order to help network code detect a system-wide man-in-the-middle.
+ */
+ primeMitm(browser) {
+ // If we already have a mitm canary issuer stored, then don't bother with the
+ // extra request. This will be cleared on every update ping.
+ if (Services.prefs.getStringPref("security.pki.mitm_canary_issuer", null)) {
+ return;
+ }
+
+ let url = Services.prefs.getStringPref(
+ "security.certerrors.mitm.priming.endpoint"
+ );
+ let request = new XMLHttpRequest({ mozAnon: true });
+ request.open("HEAD", url);
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+
+ request.addEventListener("error", event => {
+ // Make sure the user is still on the cert error page.
+ if (!browser.documentURI.spec.startsWith("about:certerror")) {
+ return;
+ }
+
+ let secInfo = request.channel.securityInfo;
+ if (secInfo.errorCodeString != "SEC_ERROR_UNKNOWN_ISSUER") {
+ return;
+ }
+
+ // When we get to this point there's already something deeply wrong, it's very likely
+ // that there is indeed a system-wide MitM.
+ if (secInfo.serverCert && secInfo.serverCert.issuerName) {
+ // Grab the issuer of the certificate used in the exchange and store it so that our
+ // network-level MitM detection code has a comparison baseline.
+ Services.prefs.setStringPref(
+ "security.pki.mitm_canary_issuer",
+ secInfo.serverCert.issuerName
+ );
+
+ // MitM issues are sometimes caused by software not registering their root certs in the
+ // Firefox root store. We might opt for using third party roots from the system root store.
+ if (
+ Services.prefs.getBoolPref(
+ "security.certerrors.mitm.auto_enable_enterprise_roots"
+ )
+ ) {
+ if (
+ !Services.prefs.getBoolPref("security.enterprise_roots.enabled")
+ ) {
+ // Loading enterprise roots happens on a background thread, so wait for import to finish.
+ lazy.BrowserUtils.promiseObserved(
+ "psm:enterprise-certs-imported"
+ ).then(() => {
+ if (browser.documentURI.spec.startsWith("about:certerror")) {
+ browser.reload();
+ }
+ });
+
+ Services.prefs.setBoolPref(
+ "security.enterprise_roots.enabled",
+ true
+ );
+ // Record that this pref was automatically set.
+ Services.prefs.setBoolPref(
+ "security.enterprise_roots.auto-enabled",
+ true
+ );
+ }
+ } else {
+ // Need to reload the page to make sure network code picks up the canary issuer pref.
+ browser.reload();
+ }
+ }
+ });
+
+ request.send(null);
+ }
+
+ displayOfflineSupportPage(supportPageSlug) {
+ const AVAILABLE_PAGES = ["connection-not-secure", "time-errors"];
+ if (!AVAILABLE_PAGES.includes(supportPageSlug)) {
+ console.log(
+ `[Not supported] Offline support is not yet available for ${supportPageSlug} errors.`
+ );
+ return;
+ }
+
+ let offlinePagePath = `chrome://global/content/neterror/supportpages/${supportPageSlug}.html`;
+ let triggeringPrincipal =
+ Services.scriptSecurityManager.getSystemPrincipal();
+ this.browser.loadURI(Services.io.newURI(offlinePagePath), {
+ triggeringPrincipal,
+ });
+ }
+
+ receiveMessage(message) {
+ switch (message.name) {
+ case "Browser:EnableOnlineMode":
+ // Reset network state and refresh the page.
+ Services.io.offline = false;
+ this.browser.reload();
+ break;
+ case "Browser:OpenCaptivePortalPage":
+ this.browser.ownerGlobal.CaptivePortalWatcher.ensureCaptivePortalTab();
+ break;
+ case "Browser:PrimeMitm":
+ this.primeMitm(this.browser);
+ break;
+ case "Browser:ResetEnterpriseRootsPref":
+ Services.prefs.clearUserPref("security.enterprise_roots.enabled");
+ Services.prefs.clearUserPref("security.enterprise_roots.auto-enabled");
+ break;
+ case "Browser:ResetSSLPreferences":
+ let prefSSLImpact = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => {
+ return prefs.concat(Services.prefs.getChildList(root));
+ }, []);
+ for (let prefName of prefSSLImpact) {
+ Services.prefs.clearUserPref(prefName);
+ }
+ this.browser.reload();
+ break;
+ case "Browser:SSLErrorGoBack":
+ this.goBackFromErrorPage(this.browser);
+ break;
+ case "Browser:SSLErrorReportTelemetry":
+ let reportStatus = message.data.reportStatus;
+ Services.telemetry
+ .getHistogramById("TLS_ERROR_REPORT_UI")
+ .add(reportStatus);
+ break;
+ case "GetChangedCertPrefs":
+ let hasChangedCertPrefs = this.hasChangedCertPrefs();
+ this.sendAsyncMessage("HasChangedCertPrefs", {
+ hasChangedCertPrefs,
+ });
+ break;
+ case "ReportBlockingError":
+ this.ReportBlockingError(
+ this.browsingContext.id,
+ message.data.scheme,
+ message.data.host,
+ message.data.port,
+ message.data.path,
+ message.data.xfoAndCspInfo
+ );
+ break;
+ case "DisplayOfflineSupportPage":
+ this.displayOfflineSupportPage(message.data.supportPageSlug);
+ break;
+ case "Browser:CertExceptionError":
+ switch (message.data.elementId) {
+ case "viewCertificate": {
+ let certs = message.data.failedCertChain.map(certBase64 =>
+ encodeURIComponent(certBase64)
+ );
+ let certsStringURL = certs.map(elem => `cert=${elem}`);
+ certsStringURL = certsStringURL.join("&");
+ let url = `about:certificate?${certsStringURL}`;
+
+ let window = this.browser.ownerGlobal;
+ if (AppConstants.MOZ_BUILD_APP === "browser") {
+ window.switchToTabHavingURI(url, true, {});
+ } else {
+ window.open(url, "_blank");
+ }
+ break;
+ }
+ }
+ break;
+ case "Browser:AddTRRExcludedDomain":
+ let domain = message.data.hostname;
+ let excludedDomains = Services.prefs.getStringPref(
+ "network.trr.excluded-domains"
+ );
+ excludedDomains += `, ${domain}`;
+ Services.prefs.setStringPref(
+ "network.trr.excluded-domains",
+ excludedDomains
+ );
+ break;
+ case "OpenTRRPreferences":
+ let browser = this.browsingContext.top.embedderElement;
+ if (!browser) {
+ break;
+ }
+
+ let win = browser.ownerGlobal;
+ win.openPreferences("privacy-doh");
+ break;
+ }
+ }
+}