Bug 1247089 - Log Web Push decryption errors. r=bkelly
MozReview-Commit-ID: HEEq8IPlwBx
--- 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) {