Bug 1247089 - Log Web Push decryption errors. r=bkelly
authorKit Cambridge <kcambridge@mozilla.com>
Mon, 28 Mar 2016 13:33:20 -0700
changeset 290954 ba673d44b388e60ebc820d14fc4664340aeb70cc
parent 290953 ca8b229e4f20c34f05b41955c2128a4388f87e9b
child 290955 c658510738da8197c3d0d9efcf23622bf8e715a0
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbkelly
bugs1247089
milestone48.0a1
Bug 1247089 - Log Web Push decryption errors. r=bkelly MozReview-Commit-ID: HEEq8IPlwBx
dom/interfaces/push/nsIPushNotifier.idl
dom/ipc/ContentChild.cpp
dom/ipc/ContentChild.h
dom/ipc/PContent.ipdl
dom/locales/en-US/chrome/dom/dom.properties
dom/push/PushNotifier.cpp
dom/push/PushNotifier.h
dom/push/PushService.jsm
dom/push/test/test_error_reporting.html
dom/push/test/test_utils.js
--- a/dom/interfaces/push/nsIPushNotifier.idl
+++ b/dom/interfaces/push/nsIPushNotifier.idl
@@ -19,16 +19,19 @@ interface nsIPushNotifier : nsISupports
                   in DOMString messageId);
 
   void notifyPushWithData(in ACString scope, in nsIPrincipal principal,
                           in DOMString messageId,
                           [optional] in uint32_t dataLen,
                           [array, size_is(dataLen)] in uint8_t data);
 
   void notifySubscriptionChange(in ACString scope, in nsIPrincipal principal);
+
+  void notifyError(in ACString scope, in nsIPrincipal principal,
+                   in DOMString message, in uint32_t flags);
 };
 
 /**
  * A push message sent to a system subscription, used as the subject of a
  * `push-message` observer notification. System subscriptions are created by
  * the system principal, and do not use worker events.
  *
  * This interface resembles the `PushMessageData` WebIDL interface.
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -3326,10 +3326,27 @@ ContentChild::RecvPushSubscriptionChange
     static_cast<PushNotifier*>(pushNotifierIface.get());
   nsresult rv = pushNotifier->NotifySubscriptionChangeWorkers(aScope,
                                                               aPrincipal);
   Unused << NS_WARN_IF(NS_FAILED(rv));
 #endif
   return true;
 }
 
+bool
+ContentChild::RecvPushError(const nsCString& aScope, const nsString& aMessage,
+                            const uint32_t& aFlags)
+{
+#ifndef MOZ_SIMPLEPUSH
+  nsCOMPtr<nsIPushNotifier> pushNotifierIface =
+      do_GetService("@mozilla.org/push/Notifier;1");
+  if (NS_WARN_IF(!pushNotifierIface)) {
+      return true;
+  }
+  PushNotifier* pushNotifier =
+    static_cast<PushNotifier*>(pushNotifierIface.get());
+  pushNotifier->NotifyErrorWorkers(aScope, aMessage, aFlags);
+#endif
+  return true;
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -539,16 +539,20 @@ public:
                    const IPC::Principal& aPrincipal,
                    const nsString& aMessageId,
                    InfallibleTArray<uint8_t>&& aData) override;
 
   virtual bool
   RecvPushSubscriptionChange(const nsCString& aScope,
                              const IPC::Principal& aPrincipal) override;
 
+  virtual bool
+  RecvPushError(const nsCString& aScope, const nsString& aMessage,
+                const uint32_t& aFlags) override;
+
   // Get the directory for IndexedDB files. We query the parent for this and
   // cache the value
   nsString &GetIndexedDBPath();
 
   ContentParentId GetID() const { return mID; }
 
   bool IsForApp() const { return mIsForApp; }
   bool IsForBrowser() const { return mIsForBrowser; }
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -739,16 +739,21 @@ child:
                        nsString messageId, uint8_t[] data);
 
     /**
      * Send a `pushsubscriptionchange` event to a service worker in the child.
      */
     async PushSubscriptionChange(nsCString scope, Principal principal);
 
     /**
+     * Send a Push error message to all service worker clients in the child.
+     */
+    async PushError(nsCString scope, nsString message, uint32_t flags);
+
+    /**
      * Windows specific: associate this content process with the browsers
      * audio session.
      */
     async SetAudioSessionData(nsID aID,
                               nsString aDisplayName,
                               nsString aIconPath);
 parent:
     /**
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -200,8 +200,10 @@ ManifestInvalidType=Expected the %1$S's 
 ManifestInvalidCSSColor=%1$S: %2$S is not a valid CSS color.
 PatternAttributeCompileFailure=Unable to check <input pattern='%S'> because the pattern is not a valid regexp: %S
 # LOCALIZATION NOTE: Do not translate "postMessage" or DOMWindow. %S values are origins, like https://domain.com:port
 TargetPrincipalDoesNotMatch=Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('%S') does not match the recipient window's origin ('%S').
 # LOCALIZATION NOTE: Do not translate 'youtube'. %S values are origins, like https://domain.com:port
 RewriteYoutubeEmbed=Rewriting old-style Youtube Flash embed (%S) to iframe embed (%S). Please update page to use iframe instead of embed/object, if possible.
 # LOCALIZATION NOTE: Do not translate 'youtube'. %S values are origins, like https://domain.com:port
 RewriteYoutubeEmbedInvalidQuery=Rewriting old-style Youtube Flash embed (%S) to iframe embed (%S). Query was invalid and removed from URL. Please update page to use iframe instead of embed/object, if possible.
+# LOCALIZATION NOTE: Do not translate "ServiceWorker". %1$S is the ServiceWorker scope URL. %2$S is an error string.
+PushMessageDecryptionFailure=The ServiceWorker for scope '%1$S' encountered an error decrypting a push message: '%2$S'. For help with encryption, please see https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Using_the_Push_API#Encryption
--- a/dom/push/PushNotifier.cpp
+++ b/dom/push/PushNotifier.cpp
@@ -1,30 +1,32 @@
 /* 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/. */
 
 #include "PushNotifier.h"
 
 #include "nsContentUtils.h"
 #include "nsCOMPtr.h"
-#include "nsXPCOM.h"
+#include "nsICategoryManager.h"
 #include "nsIXULRuntime.h"
+#include "nsNetUtil.h"
+#include "nsXPCOM.h"
 #include "ServiceWorkerManager.h"
-#include "nsICategoryManager.h"
 
 #include "mozilla/Services.h"
 #include "mozilla/unused.h"
 
 #include "mozilla/dom/BodyUtil.h"
 #include "mozilla/dom/ContentParent.h"
 
 namespace mozilla {
 namespace dom {
 
+using workers::AssertIsOnMainThread;
 using workers::ServiceWorkerManager;
 
 PushNotifier::PushNotifier()
 {}
 
 PushNotifier::~PushNotifier()
 {}
 
@@ -76,16 +78,37 @@ PushNotifier::NotifySubscriptionChange(c
     rv = NotifySubscriptionChangeWorkers(aScope, aPrincipal);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
   return NS_OK;
 }
 
+NS_IMETHODIMP
+PushNotifier::NotifyError(const nsACString& aScope, nsIPrincipal* aPrincipal,
+                          const nsAString& aMessage, uint32_t aFlags)
+{
+  if (ShouldNotifyWorkers(aPrincipal)) {
+    // For service worker subscriptions, report the error to all clients.
+    NotifyErrorWorkers(aScope, aMessage, aFlags);
+    return NS_OK;
+  }
+  // For system subscriptions, log the error directly to the browser console.
+  return nsContentUtils::ReportToConsoleNonLocalized(aMessage,
+                                                     aFlags,
+                                                     NS_LITERAL_CSTRING("Push"),
+                                                     nullptr, /* aDocument */
+                                                     nullptr, /* aURI */
+                                                     EmptyString(), /* aLine */
+                                                     0, /* aLineNumber */
+                                                     0, /* aColumnNumber */
+                                                     nsContentUtils::eOMIT_LOCATION);
+}
+
 nsresult
 PushNotifier::NotifyPush(const nsACString& aScope, nsIPrincipal* aPrincipal,
                          const nsAString& aMessageId,
                          const Maybe<nsTArray<uint8_t>>& aData)
 {
   nsresult rv;
   if (ShouldNotifyObservers(aPrincipal)) {
     rv = NotifyPushObservers(aScope, aData);
@@ -103,16 +126,17 @@ PushNotifier::NotifyPush(const nsACStrin
 }
 
 nsresult
 PushNotifier::NotifyPushWorkers(const nsACString& aScope,
                                 nsIPrincipal* aPrincipal,
                                 const nsAString& aMessageId,
                                 const Maybe<nsTArray<uint8_t>>& aData)
 {
+  AssertIsOnMainThread();
   if (!aPrincipal) {
     return NS_ERROR_INVALID_ARG;
   }
 
   if (XRE_IsContentProcess() || !BrowserTabsRemoteAutostart()) {
     // Notify the worker from the current process. Either we're running in
     // the content process and received a message from the parent, or e10s
     // is disabled.
@@ -144,16 +168,17 @@ PushNotifier::NotifyPushWorkers(const ns
   }
   return ok ? NS_OK : NS_ERROR_FAILURE;
 }
 
 nsresult
 PushNotifier::NotifySubscriptionChangeWorkers(const nsACString& aScope,
                                               nsIPrincipal* aPrincipal)
 {
+  AssertIsOnMainThread();
   if (!aPrincipal) {
     return NS_ERROR_INVALID_ARG;
   }
 
   if (XRE_IsContentProcess() || !BrowserTabsRemoteAutostart()) {
     // Content process or e10s disabled.
     RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
     if (!swm) {
@@ -173,16 +198,68 @@ PushNotifier::NotifySubscriptionChangeWo
   ContentParent::GetAll(contentActors);
   for (uint32_t i = 0; i < contentActors.Length(); ++i) {
     ok &= contentActors[i]->SendPushSubscriptionChange(
       PromiseFlatCString(aScope), IPC::Principal(aPrincipal));
   }
   return ok ? NS_OK : NS_ERROR_FAILURE;
 }
 
+void
+PushNotifier::NotifyErrorWorkers(const nsACString& aScope,
+                                 const nsAString& aMessage,
+                                 uint32_t aFlags)
+{
+  AssertIsOnMainThread();
+
+  if (XRE_IsContentProcess() || !BrowserTabsRemoteAutostart()) {
+    // Content process or e10s disabled.
+    RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+    if (swm) {
+      swm->ReportToAllClients(PromiseFlatCString(aScope),
+                              PromiseFlatString(aMessage),
+                              NS_ConvertUTF8toUTF16(aScope), /* aFilename */
+                              EmptyString(), /* aLine */
+                              0, /* aLineNumber */
+                              0, /* aColumnNumber */
+                              aFlags);
+    }
+    return;
+  }
+
+  // Parent process, e10s enabled.
+  nsTArray<ContentParent*> contentActors;
+  ContentParent::GetAll(contentActors);
+  if (!contentActors.IsEmpty()) {
+    // At least one content process active.
+    for (uint32_t i = 0; i < contentActors.Length(); ++i) {
+      Unused << NS_WARN_IF(
+        !contentActors[i]->SendPushError(PromiseFlatCString(aScope),
+          PromiseFlatString(aMessage), aFlags));
+    }
+    return;
+  }
+  // Report to the console if no content processes are active.
+  nsCOMPtr<nsIURI> scopeURI;
+  nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return;
+  }
+  Unused << NS_WARN_IF(NS_FAILED(
+    nsContentUtils::ReportToConsoleNonLocalized(aMessage,
+                                                aFlags,
+                                                NS_LITERAL_CSTRING("Push"),
+                                                nullptr, /* aDocument */
+                                                scopeURI, /* aURI */
+                                                EmptyString(), /* aLine */
+                                                0, /* aLineNumber */
+                                                0, /* aColumnNumber */
+                                                nsContentUtils::eOMIT_LOCATION)));
+}
+
 nsresult
 PushNotifier::NotifyPushObservers(const nsACString& aScope,
                                   const Maybe<nsTArray<uint8_t>>& aData)
 {
   nsCOMPtr<nsIPushMessage> message = nullptr;
   if (aData) {
     message = new PushMessage(aData.ref());
   }
--- a/dom/push/PushNotifier.h
+++ b/dom/push/PushNotifier.h
@@ -45,16 +45,18 @@ public:
                       const nsAString& aMessageId,
                       const Maybe<nsTArray<uint8_t>>& aData);
   nsresult NotifyPushWorkers(const nsACString& aScope,
                              nsIPrincipal* aPrincipal,
                              const nsAString& aMessageId,
                              const Maybe<nsTArray<uint8_t>>& aData);
   nsresult NotifySubscriptionChangeWorkers(const nsACString& aScope,
                                            nsIPrincipal* aPrincipal);
+  void NotifyErrorWorkers(const nsACString& aScope, const nsAString& aMessage,
+                          uint32_t aFlags);
 
 protected:
   virtual ~PushNotifier();
 
 private:
   nsresult NotifyPushObservers(const nsACString& aScope,
                                const Maybe<nsTArray<uint8_t>>& aData);
   nsresult NotifySubscriptionChangeObservers(const nsACString& aScope);
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -34,16 +34,19 @@ const CONNECTION_PROTOCOLS = (function()
 XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager",
                                    "@mozilla.org/contentsecuritymanager;1",
                                    "nsIContentSecurityManager");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gPushNotifier",
                                    "@mozilla.org/push/Notifier;1",
                                    "nsIPushNotifier");
 
+XPCOMUtils.defineLazyGetter(this, "gDOMBundle", () =>
+  Services.strings.createBundle("chrome://global/locale/dom/dom.properties"));
+
 this.EXPORTED_SYMBOLS = ["PushService"];
 
 XPCOMUtils.defineLazyGetter(this, "console", () => {
   let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
   return new ConsoleAPI({
     maxLogLevelPref: "dom.push.loglevel",
     prefix: "PushService",
   });
@@ -857,17 +860,20 @@ this.PushService = {
       record.p256dhPrivateKey,
       record.p256dhPublicKey,
       cryptoParams.dh,
       cryptoParams.salt,
       cryptoParams.rs,
       cryptoParams.auth ? record.authenticationSecret : null,
       cryptoParams.padSize
     ).then(message => this._notifyApp(record, messageID, message), error => {
-      console.error("receivedPushMessage: Error decrypting message", error);
+      let message = gDOMBundle.formatStringFromName(
+        "PushMessageDecryptionFailure", [record.scope, String(error)], 2);
+      gPushNotifier.notifyError(record.scope, record.principal, message,
+                                Ci.nsIScriptError.errorFlag);
       return Ci.nsIPushErrorReporter.ACK_DECRYPTION_ERROR;
     });
   },
 
   _updateQuota: function(keyID) {
     console.debug("updateQuota()");
 
     this._db.update(keyID, record => {
--- a/dom/push/test/test_error_reporting.html
+++ b/dom/push/test/test_error_reporting.html
@@ -60,26 +60,66 @@ http://creativecommons.org/licenses/publ
 
       let messageId = "message-" + (idCounter++);
       reporters.set(messageId, resolve);
       pushNotifier.notifyPushWithData(registration.scope, principal, messageId,
                                       data.length, data);
     });
   }
 
-  add_task(function* reportErrors() {
+  add_task(function* reportDeliveryErrors() {
     var reason = yield waitForDeliveryError({ type: "exception" });
     is(reason, SpecialPowers.Ci.nsIPushErrorReporter.DELIVERY_UNCAUGHT_EXCEPTION,
       "Should report uncaught exceptions");
 
     reason = yield waitForDeliveryError({ type: "rejection" });
     is(reason, SpecialPowers.Ci.nsIPushErrorReporter.DELIVERY_UNHANDLED_REJECTION,
       "Should report unhandled rejections");
   });
 
+  add_task(function* reportDecryptionError() {
+    var message = yield new Promise(resolve => {
+      var consoleService = SpecialPowers.Cc["@mozilla.org/consoleservice;1"]
+        .getService(SpecialPowers.Ci.nsIConsoleService);
+
+      var listener = SpecialPowers.wrapCallbackObject({
+        QueryInterface(iid) {
+          if (!SpecialPowers.Ci.nsISupports.equals(iid) &&
+              !SpecialPowers.Ci.nsIConsoleListener.equals(iid)) {
+            throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+          }
+          return this;
+        },
+
+        observe(message) {
+          let error = message;
+          try {
+            error.QueryInterface(SpecialPowers.Ci.nsIScriptError);
+          } catch (error) {
+            return;
+          }
+          if (message.innerWindowID == controlledFrame.innerWindowId()) {
+            consoleService.unregisterListener(listener);
+            resolve(error);
+          }
+        },
+      });
+      consoleService.registerListener(listener);
+
+      var principal = SpecialPowers.wrap(document).nodePrincipal;
+      pushNotifier.notifyError(registration.scope, principal, "Push error",
+        SpecialPowers.Ci.nsIScriptError.errorFlag);
+    });
+
+    is(message.sourceName, registration.scope,
+      "Should use the qualified scope URL as the source");
+    is(message.errorMessage, "Push error",
+      "Should report the given error string");
+  });
+
   add_task(function* unsubscribe() {
     controlledFrame.remove();
   });
 
   add_task(function* unregister() {
     yield registration.unregister();
   });
 
--- a/dom/push/test/test_utils.js
+++ b/dom/push/test/test_utils.js
@@ -195,16 +195,20 @@ function injectControlledFrame(target = 
       remove() {
         target.removeChild(iframe);
         iframe = null;
       },
       waitOnWorkerMessage(type) {
         return iframe ? iframe.contentWindow.waitOnWorkerMessage(type) :
                Promise.reject(new Error("Frame removed from document"));
       },
+      innerWindowId() {
+        var utils = SpecialPowers.getDOMWindowUtils(iframe.contentWindow);
+        return utils.currentInnerWindowID;
+      },
     };
 
     iframe.onload = () => res(controlledFrame);
     target.appendChild(iframe);
   });
 }
 
 function sendRequestToWorker(request) {