Bug 497488 - RSS feeds with an invalid certificate fail with a misleading message. r=mkmelin
authoralta88 <alta88>
Thu, 13 Oct 2016 05:20:00 +0200
changeset 20583 9fcd32547793baeb1881ed251e796243e1e964e9
parent 20582 652110fac32ecde08024215d58d45d1c950b69cd
child 20584 141676b80c81e2a3daeeac6ad21da35086ae0240
push id12435
push usermozilla@jorgk.com
push dateWed, 19 Oct 2016 22:01:49 +0000
treeherdercomm-central@324728b47409 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs497488
Bug 497488 - RSS feeds with an invalid certificate fail with a misleading message. r=mkmelin
mail/locales/en-US/chrome/messenger-newsblog/newsblog.properties
mailnews/extensions/newsblog/content/Feed.js
mailnews/extensions/newsblog/content/FeedUtils.jsm
mailnews/extensions/newsblog/content/feed-subscriptions.js
mailnews/extensions/newsblog/content/feed-subscriptions.xul
--- a/mail/locales/en-US/chrome/messenger-newsblog/newsblog.properties
+++ b/mail/locales/en-US/chrome/messenger-newsblog/newsblog.properties
@@ -9,16 +9,17 @@ subscribe-feedAlreadySubscribed=You alre
 subscribe-errorOpeningFile=Could not open the file.
 subscribe-feedAdded=Feed added.
 subscribe-feedUpdated=Feed updated.
 subscribe-feedMoved=Feed subscription moved.
 subscribe-feedCopied=Feed subscription copied.
 subscribe-feedRemoved=Feed unsubscribed.
 subscribe-feedNotValid=The Feed URL is not a valid feed.
 subscribe-networkError=The Feed URL could not be found. Please check the name and try again.
+subscribe-noAuthError=The Feed URL is not authorized.
 subscribe-loading=Loading, please wait…
 
 subscribe-OPMLImportTitle=Select OPML file to import
 ## LOCALIZATION NOTE(subscribe-OPMLExportTitleList):
 ## %S is the name of the feed account folder name.
 subscribe-OPMLExportTitleList=Export %S as an OPML file - Feeds list
 ## LOCALIZATION NOTE(subscribe-OPMLExportTitleStruct):
 ## %S is the name of the feed account folder name.
@@ -61,16 +62,20 @@ subscribe-confirmFeedDeletion=Are you su
 ##  - The second %S is the total number of items
 subscribe-gettingFeedItems=Downloading feed articles (%S of %S)…
 
 newsblog-noNewArticlesForFeed=There are no new articles for this feed.
 ## LOCALIZATION NOTE(newsblog-networkError): %S is the feed URL
 newsblog-networkError=%S could not be found. Please check the name and try again.
 ## LOCALIZATION NOTE(newsblog-feedNotValid): %S is the feed URL
 newsblog-feedNotValid=%S is not a valid feed.
+## LOCALIZATION NOTE(newsblog-badCertError): %S is the feed URL host
+newsblog-badCertError=%S uses an invalid security certificate.
+## LOCALIZATION NOTE(newsblog-noAuthError): %S is the feed URL
+newsblog-noAuthError=%S is not authorized.
 newsblog-getNewMsgsCheck=Checking feeds for new items…
 
 ## LOCALIZATION NOTE(feeds-accountname): This string should be the same as feeds.accountName in am-newsblog.dtd
 feeds-accountname=Blogs & News Feeds
 
 ## LOCALIZATION NOTE(externalAttachmentMsg): Content in the MIME part for external link attachments.
 externalAttachmentMsg=This MIME attachment is stored separately from the message.
 
--- a/mailnews/extensions/newsblog/content/Feed.js
+++ b/mailnews/extensions/newsblog/content/Feed.js
@@ -215,25 +215,37 @@ Feed.prototype =
 
   onDownloadError: function(aEvent)
   {
     let request = aEvent.target;
     let url = request.channel.originalURI.spec;
     let feed = FeedCache.getFeed(url);
     if (feed.downloadCallback) 
     {
+      // Generic network or 'not found' error initially.
       let error = FeedUtils.kNewsBlogRequestFailure;
-      try
-      {
-        if (request.status == 304)
-          // If the http status code is 304, the feed has not been modified
-          // since we last downloaded it and does not need to be parsed.
-          error = FeedUtils.kNewsBlogNoNewItems;
+
+      if (request.status == 304) {
+        // If the http status code is 304, the feed has not been modified
+        // since we last downloaded it and does not need to be parsed.
+        error = FeedUtils.kNewsBlogNoNewItems;
       }
-      catch (ex) {}
+      else {
+        let [errType, errName] = FeedUtils.createTCPErrorFromFailedXHR(request);
+        FeedUtils.log.info("Feed.onDownloaded: request errType:errName:statusCode - " +
+                           errType + ":" + errName + ":" + request.status);
+        if (errType == "SecurityCertificate")
+          // This is the code for nsINSSErrorsService.ERROR_CLASS_BAD_CERT
+          // overrideable security certificate errors.
+          error = FeedUtils.kNewsBlogBadCertError;
+
+        if (request.status == 401 || request.status == 403)
+          // Unauthorized or Forbidden.
+          error = FeedUtils.kNewsBlogNoAuthError;
+      }
 
       feed.downloadCallback.downloaded(feed, error);
     }
 
     FeedCache.removeFeed(url);
   },
 
   onParseError: function(aFeed)
@@ -597,9 +609,8 @@ Feed.prototype =
   },
 
   // nsITimerCallback
   notify: function(aTimer)
   {
     this.storeNextItem();
   }
 };
-
--- a/mailnews/extensions/newsblog/content/FeedUtils.jsm
+++ b/mailnews/extensions/newsblog/content/FeedUtils.jsm
@@ -91,20 +91,24 @@ var FeedUtils = {
 
   kBiffMinutesDefault: 100,
   kNewsBlogSuccess: 0,
   // Usually means there was an error trying to parse the feed.
   kNewsBlogInvalidFeed: 1,
   // Generic networking failure when trying to download the feed.
   kNewsBlogRequestFailure: 2,
   kNewsBlogFeedIsBusy: 3,
-  // There are no new articles for this feed
+  // For 304 Not Modified; There are no new articles for this feed.
   kNewsBlogNoNewItems: 4,
   kNewsBlogCancel: 5,
   kNewsBlogFileError: 6,
+  // Invalid certificate, for overridable user exception errors.
+  kNewsBlogBadCertError: 7,
+  // For 401 Unauthorized or 403 Forbidden.
+  kNewsBlogNoAuthError: 8,
 
   CANCEL_REQUESTED: false,
   AUTOTAG: "~AUTOTAG",
 
 /**
  * Get all rss account servers rootFolders.
  *
  * @return array of nsIMsgIncomingServer (empty array if none).
@@ -1230,16 +1234,137 @@ var FeedUtils = {
         if (validUri)
           break;
       };
     }
 
     return validUri ? uri : null;
   },
 
+  /**
+   * Returns security/certificate/network error details for an XMLHTTPRequest.
+   *
+   * @param  XMLHTTPRequest xhr - The xhr request.
+   * @return array [string errType, string errName] (null if not determined).
+   */
+  createTCPErrorFromFailedXHR: function(xhr) {
+    let status = xhr.channel.QueryInterface(Ci.nsIRequest).status;
+
+    let errType = null;
+    let errName = null;
+    if ((status & 0xff0000) === 0x5a0000) {
+      // Security module.
+      const nsINSSErrorsService = Ci.nsINSSErrorsService;
+      let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"]
+                               .getService(nsINSSErrorsService);
+      let errorClass;
+
+      // getErrorClass()) will throw a generic NS_ERROR_FAILURE if the error
+      // code is somehow not in the set of covered errors.
+      try {
+        errorClass = nssErrorsService.getErrorClass(status);
+      }
+      catch (ex) {
+        // Catch security protocol exception.
+        errorClass = "SecurityProtocol";
+      }
+
+      if (errorClass == nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+        errType = "SecurityCertificate";
+      }
+      else {
+        errType = "SecurityProtocol";
+      }
+
+      // NSS_SEC errors (happen below the base value because of negative vals).
+      if ((status & 0xffff) < Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE)) {
+        // The bases are actually negative, so in our positive numeric space,
+        // we need to subtract the base off our value.
+        let nssErr = Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
+
+        switch (nssErr) {
+          case 11: // SEC_ERROR_EXPIRED_CERTIFICATE, sec(11)
+            errName = "SecurityExpiredCertificateError";
+            break;
+          case 12: // SEC_ERROR_REVOKED_CERTIFICATE, sec(12)
+            errName = "SecurityRevokedCertificateError";
+            break;
+
+          // Per bsmith, we will be unable to tell these errors apart very soon,
+          // so it makes sense to just folder them all together already.
+          case 13: // SEC_ERROR_UNKNOWN_ISSUER, sec(13)
+          case 20: // SEC_ERROR_UNTRUSTED_ISSUER, sec(20)
+          case 21: // SEC_ERROR_UNTRUSTED_CERT, sec(21)
+          case 36: // SEC_ERROR_CA_CERT_INVALID, sec(36)
+            errName = "SecurityUntrustedCertificateIssuerError";
+            break;
+          case 90: // SEC_ERROR_INADEQUATE_KEY_USAGE, sec(90)
+            errName = "SecurityInadequateKeyUsageError";
+            break;
+          case 176: // SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED, sec(176)
+            errName = "SecurityCertificateSignatureAlgorithmDisabledError";
+            break;
+          default:
+            errName = "SecurityError";
+            break;
+        }
+      }
+      else {
+        // Calculating the difference.
+        let sslErr = Math.abs(nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
+
+        switch (sslErr) {
+          case 3: // SSL_ERROR_NO_CERTIFICATE, ssl(3)
+            errName = "SecurityNoCertificateError";
+            break;
+          case 4: // SSL_ERROR_BAD_CERTIFICATE, ssl(4)
+            errName = "SecurityBadCertificateError";
+            break;
+          case 8: // SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE, ssl(8)
+            errName = "SecurityUnsupportedCertificateTypeError";
+            break;
+          case 9: // SSL_ERROR_UNSUPPORTED_VERSION, ssl(9)
+            errName = "SecurityUnsupportedTLSVersionError";
+            break;
+          case 12: // SSL_ERROR_BAD_CERT_DOMAIN, ssl(12)
+            errName = "SecurityCertificateDomainMismatchError";
+            break;
+          default:
+            errName = "SecurityError";
+            break;
+        }
+      }
+    }
+    else {
+      errType = "Network";
+      switch (status) {
+        // Connect to host:port failed.
+        case 0x804B000C: // NS_ERROR_CONNECTION_REFUSED, network(13)
+          errName = "ConnectionRefusedError";
+          break;
+        // network timeout error.
+        case 0x804B000E: // NS_ERROR_NET_TIMEOUT, network(14)
+          errName = "NetworkTimeoutError";
+          break;
+        // Hostname lookup failed.
+        case 0x804B001E: // NS_ERROR_UNKNOWN_HOST, network(30)
+          errName = "DomainNotFoundError";
+          break;
+        case 0x804B0047: // NS_ERROR_NET_INTERRUPT, network(71)
+          errName = "NetworkInterruptError";
+          break;
+        default:
+          errName = "NetworkError";
+          break;
+      }
+    }
+
+    return [errType, errName];
+  },
+
 /**
  * Returns if a uri/url is valid to subscribe.
  *
  * @param  nsIURI aUri or string aUrl  - the Uri/Url.
  * @return boolean                     - true if a valid scheme, false if not.
  */
   _validSchemes: ["http", "https"],
   isValidScheme: function(aUri) {
@@ -1428,16 +1553,25 @@ var FeedUtils = {
         case FeedUtils.kNewsBlogRequestFailure:
           message = FeedUtils.strings.formatStringFromName(
                       "newsblog-networkError", [feed.url], 1);
           break;
         case FeedUtils.kNewsBlogFileError:
           message = FeedUtils.strings.GetStringFromName(
                       "subscribe-errorOpeningFile");
           break;
+        case FeedUtils.kNewsBlogBadCertError:
+          let host = Services.io.newURI(feed.url, null, null).host;
+          message = FeedUtils.strings.formatStringFromName(
+                      "newsblog-badCertError", [host], 1);
+          break;
+        case FeedUtils.kNewsBlogNoAuthError:
+          message = FeedUtils.strings.formatStringFromName(
+                      "newsblog-noAuthError", [feed.url], 1);
+          break;
       }
       if (message)
         FeedUtils.log.info("downloaded: " +
                            (this.mSubscribeMode ? "Subscribe: " : "Update: ") +
                            location + message);
 
       if (this.mStatusFeedback)
       {
--- a/mailnews/extensions/newsblog/content/feed-subscriptions.js
+++ b/mailnews/extensions/newsblog/content/feed-subscriptions.js
@@ -1141,17 +1141,19 @@ var FeedSubscriptions = {
     disable = !item || !isServer ||
               this.mActionMode == this.kImportingOPML;
     document.getElementById("importOPML").disabled = disable;
     document.getElementById("exportOPML").disabled = disable;
   },
 
   onMouseDown: function (aEvent)
   {
-    if (aEvent.button != 0 || aEvent.target.id == "validationText")
+    if (aEvent.button != 0 ||
+        aEvent.target.id == "validationText" ||
+        aEvent.target.id == "addCertException")
       return;
 
     this.clearStatusInfo();
   },
 
   setSummaryFocus: function ()
   {
     let item = this.mView.currentItem;
@@ -1738,16 +1740,24 @@ var FeedSubscriptions = {
           message = FeedUtils.strings.GetStringFromName(
                       "subscribe-feedNotValid");
         if (aErrorCode == FeedUtils.kNewsBlogRequestFailure)
           message = FeedUtils.strings.GetStringFromName(
                       "subscribe-networkError");
         if (aErrorCode == FeedUtils.kNewsBlogFileError)
           message = FeedUtils.strings.GetStringFromName(
                       "subscribe-errorOpeningFile");
+        if (aErrorCode == FeedUtils.kNewsBlogBadCertError) {
+          let host = Services.io.newURI(feed.url, null, null).host;
+          message = FeedUtils.strings.formatStringFromName(
+                      "newsblog-badCertError", [host], 1);
+        }
+        if (aErrorCode == FeedUtils.kNewsBlogNoAuthError)
+          message = FeedUtils.strings.GetStringFromName(
+                      "subscribe-noAuthError");
 
         if (win.mActionMode != win.kUpdateMode)
           // Re-enable the add button if subscribe failed.
           document.getElementById("addFeed").removeAttribute("disabled");
       }
 
       win.mActionMode = null;
       win.clearStatusInfo();
@@ -1791,23 +1801,30 @@ var FeedSubscriptions = {
     else
       el.value = aValue;
 
     el = document.getElementById("validationText");
     if (aErrorCode == FeedUtils.kNewsBlogInvalidFeed)
       el.removeAttribute("collapsed");
     else
       el.setAttribute("collapsed", true);
+
+    el = document.getElementById("addCertException");
+    if (aErrorCode == FeedUtils.kNewsBlogBadCertError)
+      el.removeAttribute("collapsed");
+    else
+      el.setAttribute("collapsed", true);
   },
 
   clearStatusInfo: function()
   {
     document.getElementById("statusText").textContent = "";
     document.getElementById("progressMeter").collapsed = true;
     document.getElementById("validationText").collapsed = true;
+    document.getElementById("addCertException").collapsed = true;
   },
 
   checkValidation: function(aEvent)
   {
     if (aEvent.button != 0)
       return;
 
     let validationSite = "http://validator.w3.org";
@@ -1824,16 +1841,28 @@ var FeedSubscriptions = {
         this.mMainWin.focus();
         this.mMainWin.openContentTab(url, "tab", "^" + validationSite);
         FeedUtils.log.debug("checkValidation: query url - " + url);
       }
     }
     aEvent.stopPropagation();
   },
 
+  addCertExceptionDialog: function()
+  {
+    let feedURL = document.getElementById("locationValue").value.trim();
+    let params = { exceptionAdded : false,
+                   location: feedURL,
+                   prefetchCert: true };
+    window.openDialog("chrome://pippki/content/exceptionDialog.xul",
+                      "", "chrome,centerscreen,modal", params);
+    if (params.exceptionAdded)
+      this.clearStatusInfo();
+  },
+
   // Listener for folder pane changes.
   FolderListener: {
     get feedWindow() {
       let subscriptionsWindow =
         Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
       return subscriptionsWindow ? subscriptionsWindow.FeedSubscriptions : null;
     },
 
--- a/mailnews/extensions/newsblog/content/feed-subscriptions.xul
+++ b/mailnews/extensions/newsblog/content/feed-subscriptions.xul
@@ -5,18 +5,20 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <?xml-stylesheet href="chrome://messenger/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
 <?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
 <?xml-stylesheet href="chrome://messenger-newsblog/skin/feed-subscriptions.css" type="text/css"?>
 
 <!DOCTYPE window [
-<!ENTITY % feedDTD SYSTEM "chrome://messenger-newsblog/locale/feed-subscriptions.dtd">
-  %feedDTD;
+  <!ENTITY % feedDTD SYSTEM "chrome://messenger-newsblog/locale/feed-subscriptions.dtd">
+    %feedDTD;
+  <!ENTITY % certDTD SYSTEM "chrome://pippki/locale/certManager.dtd">
+    %certDTD;
 ]>
 
 <window id="subscriptionsDialog"
         flex="1"
         title="&feedSubscriptions.label;"
         windowtype="Mail:News-BlogSubscriptions"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
@@ -173,16 +175,21 @@
       </vbox>
       <spacer flex="1"/>
       <label id="validationText"
              collapsed="true"
              class="text-link"
              crop="end"
              value="&validateText.label;"
              onclick="FeedSubscriptions.checkValidation(event);"/>
+      <button id="addCertException"
+              collapsed="true"
+              label="&certmgr.addException.label;"
+              accesskey="&certmgr.addException.accesskey;"
+              oncommand="FeedSubscriptions.addCertExceptionDialog();"/>
       <progressmeter id="progressMeter"
                      collapsed="true"
                      mode="determined"
                      value="0"/>
     </hbox>
 
     <hbox align="end">
       <hbox class="actionButtons" flex="1">