Bug 989197 - Show alternate UI in cert error pages when a captive portal is active. r=Gijs draft
authorNihanth Subramanya <nhnt11@gmail.com>
Tue, 08 Nov 2016 16:10:12 +0530
changeset 438432 3257bd4010807373e33f085391e423068f4a21fb
parent 438010 b37be3d705d929ee52280051d58cedc70a47626f
child 536910 b3f5efb7f22b7db3cf6ab84469eb14c5e066d162
push id35714
push usernhnt11@gmail.com
push dateMon, 14 Nov 2016 14:47:30 +0000
reviewersGijs
bugs989197
milestone52.0a1
Bug 989197 - Show alternate UI in cert error pages when a captive portal is active. r=Gijs MozReview-Commit-ID: 5Pr7gjaD5hw
browser/base/content/aboutNetError.xhtml
browser/base/content/browser.js
browser/base/content/content.js
browser/base/content/tabbrowser.xml
browser/themes/shared/aboutNetError.css
--- a/browser/base/content/aboutNetError.xhtml
+++ b/browser/base/content/aboutNetError.xhtml
@@ -46,16 +46,18 @@
         var error = url.search(/e\=/);
         var duffUrl = url.search(/\&u\=/);
         return decodeURIComponent(url.slice(error + 2, duffUrl));
       }
 
       // Set to true on init if the error code is nssBadCert.
       var gIsCertError;
 
+      var gCaptivePortalActive;
+
       function getCSSClass()
       {
         var url = document.documentURI;
         var matches = url.match(/s\=([^&]+)\&/);
         // s is optional, if no match just return nothing
         if (!matches || matches.length < 2)
           return "";
 
@@ -119,17 +121,17 @@
         panel.style.display = "block";
         document.getElementById("netErrorButtonContainer").style.display = "none";
         document.getElementById("prefResetButton").addEventListener("click", function resetPreferences(e) {
           const event = new CustomEvent("AboutNetErrorResetPreferences", {bubbles:true});
           document.dispatchEvent(event);
         });
       }
 
-      function showAdvancedButton(allowOverride) {
+      function setupAdvancedButton(allowOverride) {
         // Get the hostname and add it to the panel
         var panelId = gIsCertError ? "badCertAdvancedPanel" : "weakCryptoAdvancedPanel";
         var panel = document.getElementById(panelId);
         for (var span of panel.querySelectorAll("span.hostname")) {
           span.textContent = document.location.hostname;
         }
         if (!gIsCertError) {
           panel.replaceChild(document.getElementById("errorLongDesc"),
@@ -155,107 +157,115 @@
           }
         });
 
         if (allowOverride) {
           document.getElementById("overrideWeakCryptoPanel").style.display = "flex";
           var overrideLink = document.getElementById("overrideWeakCrypto");
           overrideLink.addEventListener("click", () => doOverride(overrideLink), false);
         }
-      }
 
-      function initPageCertError() {
-        document.body.className = "certerror";
-        document.title = document.getElementById("certErrorPageTitle").textContent;
-        for (let host of document.querySelectorAll(".hostname")) {
-          host.textContent = document.location.hostname;
+        if (!gIsCertError) {
+          return;
         }
 
-        showAdvancedButton(true);
-
-        var cssClass = getCSSClass();
-        if (cssClass == "expertBadCert") {
+        if (getCSSClass() == "expertBadCert") {
           toggleDisplay(document.getElementById("badCertAdvancedPanel"));
           // Toggling the advanced panel must ensure that the debugging
           // information panel is hidden as well, since it's opened by the
           // error code link in the advanced panel.
           var div = document.getElementById("certificateErrorDebugInformation");
           div.style.display = "none";
         }
 
-        document.getElementById("learnMoreContainer").style.display = "block";
-
-        var checkbox = document.getElementById("automaticallyReportInFuture");
-        checkbox.addEventListener("change", function ({target: {checked}}) {
-          document.dispatchEvent(new CustomEvent("AboutNetErrorSetAutomatic", {
-            detail: checked,
-            bubbles: true
-          }));
-        });
+        disallowCertOverridesIfNeeded();
 
-        addEventListener("AboutNetErrorOptions", function (event) {
-          var options = JSON.parse(event.detail);
-          if (options && options.enabled) {
-            // Display error reporting UI
-            document.getElementById("certificateErrorReporting").style.display = "block";
+        document.getElementById("badCertTechnicalInfo").textContent = getDescription();
+      }
 
-            // set the checkbox
-            checkbox.checked = !!options.automatic;
-          }
-        }, true, true);
-
+      function disallowCertOverridesIfNeeded() {
+        var cssClass = getCSSClass();
         // Disallow overrides if this is a Strict-Transport-Security
         // host and the cert is bad (STS Spec section 7.3) or if the
         // certerror is in a frame (bug 633691).
         if (cssClass == "badStsCert" || window != top) {
           document.getElementById("exceptionDialogButton").setAttribute("hidden", "true");
         }
         if (cssClass == "badStsCert") {
           document.getElementById("badStsCertExplanation").removeAttribute("hidden");
         }
-
-        document.getElementById("badCertTechnicalInfo").textContent = getDescription();
+      }
 
-        var event = new CustomEvent("AboutNetErrorLoad", {bubbles:true});
-        document.getElementById("advancedButton").dispatchEvent(event);
-
-        addDomainErrorLinks();
+      // Before we actually initialize the page, find out whether there is an
+      // active captive portal on the network. If there is one, we will display
+      // different text. We request the captive portal state via an event, and
+      // the response is sent through an event as well.
+      function preInit() {
+        // Start listening for the response...
+        window.addEventListener("AboutNetErrorCaptivePortalState", function listen(e) {
+          window.removeEventListener("AboutNetErrorCaptivePortalState", listen);
+          // e.detail is true if there is an active, locked portal.
+          gCaptivePortalActive = e.detail;
+          initPage();
+        });
+        // ...then send the request.
+        var event = new CustomEvent("AboutNetErrorRequestCaptivePortalState",
+                                    {bubbles:true});
+        document.dispatchEvent(event);
       }
 
       function initPage()
       {
         var err = getErrorCode();
         gIsCertError = (err == "nssBadCert");
+        // Only worry about captive portals if this is a cert error.
+        gCaptivePortalActive = gCaptivePortalActive && gIsCertError;
 
         // if it's an unknown error or there's no title or description
         // defined, get the generic message
-        var errTitle = document.getElementById("et_" + err);
-        var errDesc  = document.getElementById("ed_" + err);
+        var errTitle;
+        var errDesc;
+        if (gCaptivePortalActive) {
+          errTitle = document.getElementById("et_captivePortal");
+          errDesc = document.getElementById("ed_captivePortal");
+        } else {
+          errTitle = document.getElementById("et_" + err);
+          errDesc  = document.getElementById("ed_" + err);
+        }
         if (!errTitle || !errDesc)
         {
           errTitle = document.getElementById("et_generic");
           errDesc  = document.getElementById("ed_generic");
         }
 
         document.querySelector(".title-text").innerHTML = errTitle.innerHTML;
 
         var sd = document.getElementById("errorShortDescText");
         if (sd) {
-          if (gIsCertError) {
+          if (gCaptivePortalActive) {
+            sd.innerHTML = document.getElementById("ed_captivePortal").innerHTML;
+          }
+          else if (gIsCertError) {
             sd.innerHTML = document.getElementById("ed_nssBadCert").innerHTML;
           }
           else {
             sd.textContent = getDescription();
           }
         }
+        if (gCaptivePortalActive) {
+          initPageCaptivePortal();
+          return;
+        }
         if (gIsCertError) {
           initPageCertError();
           return;
         }
 
+        body.className = "neterror";
+
         var ld = document.getElementById("errorLongDesc");
         if (ld)
         {
           ld.innerHTML = errDesc.innerHTML;
         }
 
         if (err == "sslv3Used") {
           document.getElementById("learnMoreContainer").style.display = "block";
@@ -338,17 +348,17 @@
               "SSL_ERROR_NO_CIPHERS_SUPPORTED"
             ].some((substring) => getDescription().includes(substring));
             // If it looks like an error that is user config based
             if (getErrorCode() == "nssFailure2" && hasPrefStyleError && options && options.changedCertPrefs) {
               showPrefChangeContainer();
             }
           }
           if (getErrorCode() == "weakCryptoUsed" || getErrorCode() == "sslv3Used") {
-            showAdvancedButton(getErrorCode() == "weakCryptoUsed");
+            setupAdvancedButton(getErrorCode() == "weakCryptoUsed");
           }
         }.bind(this), true, true);
 
         var event = new CustomEvent("AboutNetErrorLoad", {bubbles:true});
         document.dispatchEvent(event);
 
         if (err == "inadequateSecurityError") {
           // Remove the "Try again" button for HTTP/2 inadequate security as it
@@ -359,16 +369,77 @@
           for (var span of container.querySelectorAll("span.hostname")) {
             span.textContent = document.location.hostname;
           }
         }
 
         addDomainErrorLinks();
       }
 
+      function initPageCaptivePortal()
+      {
+        document.body.className = "captiveportal";
+        document.title = document.getElementById("captivePortalPageTitle").textContent;
+        for (let host of document.querySelectorAll(".hostname")) {
+          host.textContent = document.location.hostname;
+        }
+
+        document.getElementById("openPortalLoginPageButton")
+                .addEventListener("click", () => {
+          let event = new CustomEvent("AboutNetErrorOpenCaptivePortal", {bubbles:true});
+          document.dispatchEvent(event);
+        });
+
+        setupAdvancedButton(true);
+
+        addDomainErrorLinks();
+
+        // When the portal is freed, an event is generated by the frame script
+        // that we can pick up and attempt to reload the original page.
+        window.addEventListener("AboutNetErrorCaptivePortalFreed", () => {
+          document.location.reload();
+        });
+      }
+
+      function initPageCertError() {
+        document.body.className = "certerror";
+        document.title = document.getElementById("certErrorPageTitle").textContent;
+        for (let host of document.querySelectorAll(".hostname")) {
+          host.textContent = document.location.hostname;
+        }
+
+        setupAdvancedButton(true);
+
+        document.getElementById("learnMoreContainer").style.display = "block";
+
+        let checkbox = document.getElementById("automaticallyReportInFuture");
+        checkbox.addEventListener("change", function ({target: {checked}}) {
+          document.dispatchEvent(new CustomEvent("AboutNetErrorSetAutomatic", {
+            detail: checked,
+            bubbles: true
+          }));
+        });
+
+        addEventListener("AboutNetErrorOptions", function (event) {
+          var options = JSON.parse(event.detail);
+          if (options && options.enabled) {
+            // Display error reporting UI
+            document.getElementById("certificateErrorReporting").style.display = "block";
+
+            // set the checkbox
+            checkbox.checked = !!options.automatic;
+          }
+        }, true, true);
+
+        let event = new CustomEvent("AboutNetErrorLoad", {bubbles:true});
+        document.getElementById("advancedButton").dispatchEvent(event);
+
+        addDomainErrorLinks();
+      }
+
       /* Try to preserve the links contained in the error description, like
          the error code.
 
          Also, in the case of SSL error pages about domain mismatch, see if
          we can hyperlink the user to the correct site.  We don't want
          to do this generically since it allows MitM attacks to redirect
          users to a site under attacker control, but in certain cases
          it is safe (and helpful!) to do so.  Bug 402210
@@ -489,21 +560,23 @@
         el.appendChild(anchorEl);
       }
     ]]></script>
   </head>
 
   <body dir="&locale.dir;">
     <!-- Contains an alternate page title set on page init for cert errors. -->
     <div id="certErrorPageTitle" style="display: none;">&certerror.pagetitle1;</div>
+    <div id="captivePortalPageTitle" style="display: none;">&captivePortal.title;</div>
 
     <!-- ERROR ITEM CONTAINER (removed during loading to avoid bug 39098) -->
     <div id="errorContainer">
       <div id="errorTitlesContainer">
         <h1 id="et_generic">&generic.title;</h1>
+        <h1 id="et_captivePortal">&captivePortal.title;</h1>
         <h1 id="et_dnsNotFound">&dnsNotFound.title;</h1>
         <h1 id="et_fileNotFound">&fileNotFound.title;</h1>
         <h1 id="et_fileAccessDenied">&fileAccessDenied.title;</h1>
         <h1 id="et_malformedURI">&malformedURI.title;</h1>
         <h1 id="et_unknownProtocolFound">&unknownProtocolFound.title;</h1>
         <h1 id="et_connectionFailure">&connectionFailure.title;</h1>
         <h1 id="et_netTimeout">&netTimeout.title;</h1>
         <h1 id="et_redirectLoop">&redirectLoop.title;</h1>
@@ -523,16 +596,17 @@
         <h1 id="et_remoteXUL">&remoteXUL.title;</h1>
         <h1 id="et_corruptedContentErrorv2">&corruptedContentErrorv2.title;</h1>
         <h1 id="et_sslv3Used">&sslv3Used.title;</h1>
         <h1 id="et_weakCryptoUsed">&weakCryptoUsed.title;</h1>
         <h1 id="et_inadequateSecurityError">&inadequateSecurityError.title;</h1>
       </div>
       <div id="errorDescriptionsContainer">
         <div id="ed_generic">&generic.longDesc;</div>
+        <div id="ed_captivePortal">&captivePortal.longDesc;</div>
         <div id="ed_dnsNotFound">&dnsNotFound.longDesc;</div>
         <div id="ed_fileNotFound">&fileNotFound.longDesc;</div>
         <div id="ed_fileAccessDenied">&fileAccessDenied.longDesc;</div>
         <div id="ed_malformedURI">&malformedURI.longDesc;</div>
         <div id="ed_unknownProtocolFound">&unknownProtocolFound.longDesc;</div>
         <div id="ed_connectionFailure">&connectionFailure.longDesc;</div>
         <div id="ed_netTimeout">&netTimeout.longDesc;</div>
         <div id="ed_redirectLoop">&redirectLoop.longDesc;</div>
@@ -585,18 +659,19 @@
           <p><a href="https://support.mozilla.org/kb/what-does-your-connection-is-not-secure-mean" id="learnMoreLink" target="new">&errorReporting.learnMore;</a></p>
         </div>
 
         <div id="prefChangeContainer" class="button-container">
           <p>&prefReset.longDesc;</p>
           <button id="prefResetButton" class="primary" autocomplete="off">&prefReset.label;</button>
         </div>
 
-        <div id="certErrorButtonContainer" class="button-container">
+        <div id="certErrorAndCaptivePortalButtonContainer" class="button-container">
           <button id="returnButton" class="primary" autocomplete="off" autofocus="true">&returnToPreviousPage.label;</button>
+          <button id="openPortalLoginPageButton" class="primary" autocomplete="off" autofocus="true">&openPortalLoginPage.label;</button>
           <div class="button-spacer"></div>
           <button id="advancedButton" autocomplete="off" autofocus="true">&advanced.label;</button>
         </div>
       </div>
 
       <div id="netErrorButtonContainer" class="button-container">
         <button id="errorTryAgain" class="primary" autocomplete="off" onclick="retryThis(this);">&retry.label;</button>
       </div>
@@ -651,13 +726,13 @@
     </div>
 
     <!--
     - Note: It is important to run the script this way, instead of using
     - an onload handler. This is because error pages are loaded as
     - LOAD_BACKGROUND, which means that onload handlers will not be executed.
     -->
     <script type="application/javascript">
-      initPage();
+      preInit();
     </script>
 
   </body>
 </html>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -146,16 +146,19 @@ XPCOMUtils.defineLazyGetter(this, "Win7F
       onCloseWindow: function () {
         AeroPeek.onCloseWindow(window);
       }
     };
   }
   return null;
 });
 
+XPCOMUtils.defineLazyServiceGetter(this, "gCaptivePortalService",
+                                   "@mozilla.org/network/captive-portal-service;1",
+                                   "nsICaptivePortalService");
 
 const nsIWebNavigation = Ci.nsIWebNavigation;
 
 var gLastBrowserCharset = null;
 var gLastValidURLStr = "";
 var gInPrintPreviewMode = false;
 var gContextMenu = null; // nsContextMenu instance
 var gMultiProcessBrowser =
@@ -2738,37 +2741,56 @@ const PREF_SSL_IMPACT = PREF_SSL_IMPACT_
  * Handle command events bubbling up from error page content
  * or from about:newtab or from remote error pages that invoke
  * us via async messaging.
  */
 var BrowserOnClick = {
   init: function () {
     let mm = window.messageManager;
     mm.addMessageListener("Browser:CertExceptionError", this);
+    mm.addMessageListener("Browser:RequestCaptivePortalState", this);
+    mm.addMessageListener("Browser:OpenCaptivePortalPage", this);
     mm.addMessageListener("Browser:SiteBlockedError", this);
     mm.addMessageListener("Browser:EnableOnlineMode", this);
     mm.addMessageListener("Browser:SendSSLErrorReport", this);
     mm.addMessageListener("Browser:SetSSLErrorReportAuto", this);
     mm.addMessageListener("Browser:ResetSSLPreferences", this);
     mm.addMessageListener("Browser:SSLErrorReportTelemetry", this);
     mm.addMessageListener("Browser:OverrideWeakCrypto", this);
     mm.addMessageListener("Browser:SSLErrorGoBack", this);
+
+    Services.obs.addObserver(this, "captive-portal-login-abort", false);
+    Services.obs.addObserver(this, "captive-portal-login-success", false);
   },
 
   uninit: function () {
     let mm = window.messageManager;
     mm.removeMessageListener("Browser:CertExceptionError", this);
     mm.removeMessageListener("Browser:SiteBlockedError", this);
     mm.removeMessageListener("Browser:EnableOnlineMode", this);
     mm.removeMessageListener("Browser:SendSSLErrorReport", this);
     mm.removeMessageListener("Browser:SetSSLErrorReportAuto", this);
     mm.removeMessageListener("Browser:ResetSSLPreferences", this);
     mm.removeMessageListener("Browser:SSLErrorReportTelemetry", this);
     mm.removeMessageListener("Browser:OverrideWeakCrypto", this);
     mm.removeMessageListener("Browser:SSLErrorGoBack", this);
+
+    Services.obs.removeObserver(this, "captive-portal-login-abort");
+    Services.obs.removeObserver(this, "captive-portal-login-success");
+  },
+
+  observe: function (aSubjec, aTopic, aData) {
+    switch (aTopic) {
+      case "captive-portal-login-abort":
+      case "captive-portal-login-success":
+        // Broadcast when a captive portal is freed so that error pages
+        // can refresh themselves.
+        window.messageManager.broadcastAsyncMessage("Browser:CaptivePortalFreed");
+      break;
+    }
   },
 
   handleEvent: function (event) {
     if (!event.isTrusted || // Don't trust synthetic events
         event.button == 2) {
       return;
     }
 
@@ -2786,16 +2808,22 @@ var BrowserOnClick = {
 
   receiveMessage: function (msg) {
     switch (msg.name) {
       case "Browser:CertExceptionError":
         this.onCertError(msg.target, msg.data.elementId,
                          msg.data.isTopFrame, msg.data.location,
                          msg.data.securityInfoAsString);
       break;
+      case "Browser:RequestCaptivePortalState":
+        this.onCaptivePortalStateRequest(msg.target);
+      break;
+      case "Browser:OpenCaptivePortalPage":
+        this.onOpenCaptivePortalPage();
+      break;
       case "Browser:SiteBlockedError":
         this.onAboutBlocked(msg.data.elementId, msg.data.reason,
                             msg.data.isTopFrame, msg.data.location);
       break;
       case "Browser:EnableOnlineMode":
         if (Services.io.offline) {
           // Reset network state and refresh the page.
           Services.io.offline = false;
@@ -2920,16 +2948,65 @@ var BrowserOnClick = {
         let detailedInfo = getDetailedCertErrorInfo(location,
                                                     securityInfo);
         gClipboardHelper.copyString(detailedInfo);
         break;
 
     }
   },
 
+  onCaptivePortalStateRequest: function(browser) {
+    browser.messageManager.sendAsyncMessage("CaptivePortalState", {
+      state: gCaptivePortalService.state,
+      locked: gCaptivePortalService.state == gCaptivePortalService.LOCKED_PORTAL,
+    });
+  },
+
+  onOpenCaptivePortalPage: function() {
+    // Open a new tab with the canonical URL that we use to check for a captive portal.
+    // It will be redirected to the login page.
+    let canonicalURL = Services.prefs.getCharPref("captivedetect.canonicalURL");
+    let tab = gBrowser.addTab(canonicalURL);
+    gBrowser.selectedTab = tab;
+    let portalHost;
+    let progressListener = {
+      onLocationChange(aBrowser) {
+        if (!tab || tab.closing || !tab.parentNode || !tab.linkedBrowser) {
+          // The tab we opened is no longer relevant, stop listening.
+          gBrowser.removeTabsProgressListener(progressListener);
+          return;
+        }
+        if (aBrowser != tab.linkedBrowser) {
+          // This location change was in a different tab.
+          return;
+        }
+        if (!portalHost) {
+          // The tab is initially loaded with the canonical URL. We treat
+          // the host of the first location change as the portal host.
+          portalHost = aBrowser.currentURI.host;
+          return;
+        }
+        if (aBrowser.currentURI.host != portalHost &&
+            aBrowser.lastURI.host == portalHost) {
+          // The browser has navigated away from the portal for the first time.
+          // We can stop listening for location changes.
+          gBrowser.removeTabsProgressListener(progressListener);
+
+          if (aBrowser.currentURI.spec == canonicalURL) {
+            // The browser navigated directly to the canonical URL from the
+            // portal. This means the portal redirected back to the URL
+            // that it first intercepted, so we can close the tab.
+            gBrowser.removeTab(tab);
+          }
+        }
+      }
+    };
+    gBrowser.addTabsProgressListener(progressListener);
+  },
+
   onAboutBlocked: function (elementId, reason, isTopFrame, location) {
     // Depending on what page we are displaying here (malware/phishing/unwanted)
     // use the right strings and links for each.
     let bucketName = "";
     let sendTelemetry = false;
     if (reason === 'malware') {
       sendTelemetry = true;
       bucketName = "WARNING_MALWARE_PAGE_";
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -259,39 +259,49 @@ function getSerializedSecurityInfo(docSh
               .QueryInterface(Ci.nsISerializable);
 
   return serhelper.serializeToString(securityInfo);
 }
 
 var AboutNetAndCertErrorListener = {
   init: function(chromeGlobal) {
     addMessageListener("CertErrorDetails", this);
+    addMessageListener("CaptivePortalState", this);
+    addMessageListener("Browser:CaptivePortalFreed", this);
     chromeGlobal.addEventListener('AboutNetErrorLoad', this, false, true);
+    chromeGlobal.addEventListener('AboutNetErrorRequestCaptivePortalState', this, false, true);
+    chromeGlobal.addEventListener('AboutNetErrorOpenCaptivePortal', this, false, true);
     chromeGlobal.addEventListener('AboutNetErrorSetAutomatic', this, false, true);
     chromeGlobal.addEventListener('AboutNetErrorOverride', this, false, true);
     chromeGlobal.addEventListener('AboutNetErrorResetPreferences', this, false, true);
   },
 
   get isAboutNetError() {
     return content.document.documentURI.startsWith("about:neterror");
   },
 
   get isAboutCertError() {
     return content.document.documentURI.startsWith("about:certerror");
   },
 
   receiveMessage: function(msg) {
-    if (!this.isAboutCertError) {
+    if (!this.isAboutCertError && !this.isAboutNetError) {
       return;
     }
 
     switch (msg.name) {
       case "CertErrorDetails":
         this.onCertErrorDetails(msg);
         break;
+      case "CaptivePortalState":
+        this.onCaptivePortalState(msg);
+        break;
+      case "Browser:CaptivePortalFreed":
+        this.onCaptivePortalFreed(msg);
+        break;
     }
   },
 
   onCertErrorDetails(msg) {
     let div = content.document.getElementById("certificateErrorText");
     div.textContent = msg.data.info;
     let learnMoreLink = content.document.getElementById("learnMoreLink");
     let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
@@ -334,25 +344,41 @@ var AboutNetAndCertErrorListener = {
               .style.display = "block";
           }
         }
         learnMoreLink.href = baseURL  + "time-errors";
         break;
     }
   },
 
+  onCaptivePortalState(msg) {
+    content.dispatchEvent(new content.CustomEvent("AboutNetErrorCaptivePortalState", {
+      detail: msg.data.locked,
+    }));
+  },
+
+  onCaptivePortalFreed(msg) {
+    content.dispatchEvent(new content.CustomEvent("AboutNetErrorCaptivePortalFreed"));
+  },
+
   handleEvent: function(aEvent) {
     if (!this.isAboutNetError && !this.isAboutCertError) {
       return;
     }
 
     switch (aEvent.type) {
     case "AboutNetErrorLoad":
       this.onPageLoad(aEvent);
       break;
+    case "AboutNetErrorRequestCaptivePortalState":
+      this.requestCaptivePortalState(aEvent);
+      break;
+    case "AboutNetErrorOpenCaptivePortal":
+      this.openCaptivePortalPage(aEvent);
+      break;
     case "AboutNetErrorSetAutomatic":
       this.onSetAutomatic(aEvent);
       break;
     case "AboutNetErrorOverride":
       this.onOverride(aEvent);
       break;
     case "AboutNetErrorResetPreferences":
       this.onResetPreferences(aEvent);
@@ -385,16 +411,24 @@ var AboutNetAndCertErrorListener = {
         automatic: automatic
       })
     }));
 
     sendAsyncMessage("Browser:SSLErrorReportTelemetry",
                      {reportStatus: TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN});
   },
 
+  requestCaptivePortalState: function(evt) {
+    sendAsyncMessage("Browser:RequestCaptivePortalState");
+  },
+
+  openCaptivePortalPage: function(evt) {
+    sendAsyncMessage("Browser:OpenCaptivePortalPage");
+  },
+
 
   onResetPreferences: function(evt) {
     sendAsyncMessage("Browser:ResetSSLPreferences");
   },
 
   onSetAutomatic: function(evt) {
     sendAsyncMessage("Browser:SetSSLErrorReportAuto", {
       automatic: evt.detail
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -829,17 +829,17 @@
               }
 
               if (!this.mBlank) {
                 this._callProgressListeners("onLocationChange",
                                             [aWebProgress, aRequest, aLocation,
                                              aFlags]);
               }
 
-              if (topLevel) {
+              if (topLevel && this.mBrowser) {
                 this.mBrowser.lastURI = aLocation;
                 this.mBrowser.lastLocationChange = Date.now();
               }
             },
 
             onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) {
               if (this.mBlank)
                 return;
--- a/browser/themes/shared/aboutNetError.css
+++ b/browser/themes/shared/aboutNetError.css
@@ -34,42 +34,54 @@ button:disabled {
 #prefChangeContainer {
   display: none;
 }
 
 #learnMoreContainer {
   display: none;
 }
 
-#certErrorButtonContainer {
+#certErrorAndCaptivePortalButtonContainer {
   display: none;
 }
 
-body.certerror #certErrorButtonContainer {
+body:not(.neterror) #certErrorAndCaptivePortalButtonContainer {
   display: flex;
 }
 
-body.certerror #netErrorButtonContainer {
+body:not(.neterror) #netErrorButtonContainer {
   display: none;
 }
 
 #errorTryAgain {
   margin-top: 1.2em;
   min-width: 150px;
 }
 
 #returnButton {
   min-width: 250px;
 }
 
 #advancedButton {
   display: none;
 }
 
-body.certerror #advancedButton {
+body.captiveportal #returnButton {
+  display: none;
+}
+
+body:not(.captiveportal) #openPortalLoginPageButton {
+  display: none;
+}
+
+#openPortalLoginPageButton {
+  margin-inline-start: 0;
+}
+
+body:not(.neterror) #advancedButton {
   display: block;
 }
 
 #certificateErrorReporting {
   display: none;
 }
 
 .container {