Bug 1244249, Part 2 - Implement structured cloning for push subscriptions. draft
authorKit Cambridge <kcambridge@mozilla.com>
Fri, 29 Jan 2016 11:23:15 -0800
changeset 327375 96bfc2037ab8f7fbabbedf864a6451eab9450152
parent 327374 c6693d506d6ea3ef02756326b3abf2e58db0dacc
child 513698 0ba9c662d1307a719dd00a50cbffedf26ebec039
push id10237
push userkcambridge@mozilla.com
push dateSun, 31 Jan 2016 22:46:27 +0000
bugs1244249
milestone47.0a1
Bug 1244249, Part 2 - Implement structured cloning for push subscriptions.
dom/base/StructuredCloneHolder.cpp
dom/base/StructuredCloneTags.h
dom/push/Push.js
dom/push/PushManager.cpp
dom/push/PushManager.h
dom/push/test/test_register.html
dom/push/test/worker.js
dom/webidl/PushSubscription.webidl
--- a/dom/base/StructuredCloneHolder.cpp
+++ b/dom/base/StructuredCloneHolder.cpp
@@ -40,16 +40,20 @@
 #ifdef MOZ_NFC
 #include "mozilla/dom/MozNDEFRecord.h"
 #endif // MOZ_NFC
 #ifdef MOZ_WEBRTC
 #include "mozilla/dom/RTCCertificate.h"
 #include "mozilla/dom/RTCCertificateBinding.h"
 #endif
 
+#ifndef MOZ_SIMPLEPUSH
+#include "mozilla/dom/PushManager.h"
+#endif
+
 using namespace mozilla::ipc;
 
 namespace mozilla {
 namespace dom {
 
 namespace {
 
 JSObject*
@@ -410,16 +414,52 @@ StructuredCloneHolder::ReadFullySerializ
         result = nullptr;
       } else {
         result = key->WrapObject(aCx, nullptr);
       }
     }
     return result;
   }
 
+#ifndef MOZ_SIMPLEPUSH
+  if (aTag == SCTAG_DOM_PUSH_SUBSCRIPTION) {
+    JS::Rooted<JSObject*> result(aCx);
+
+    nsTArray<uint8_t> rawKey;
+    nsTArray<uint8_t> authSecret;
+
+    // The PushSubscription interface has two implementations: PushSubscription
+    // for the main thread, and WorkerPushSubscription for workers. Use the
+    // appropriate implementation for the current thread.
+    if (NS_IsMainThread()) {
+      nsIGlobalObject *global = xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx));
+      if (!global) {
+        return nullptr;
+      }
+      RefPtr<PushSubscription> subscription = new PushSubscription(global,
+        EmptyString(), EmptyString(), rawKey, authSecret);
+      if (!subscription->ReadStructuredClone(aCx, aReader)) {
+        result = nullptr;
+      } else {
+        result = subscription->WrapObject(aCx, nullptr);
+      }
+    } else {
+      RefPtr<WorkerPushSubscription> subscription =
+        new WorkerPushSubscription(EmptyString(), EmptyString(), rawKey,
+                                   authSecret);
+      if (!subscription->ReadStructuredClone(aCx, aReader)) {
+        result = nullptr;
+      } else {
+        result = subscription->WrapObject(aCx, nullptr);
+      }
+    }
+    return result;
+  }
+#endif
+
   if (aTag == SCTAG_DOM_NULL_PRINCIPAL ||
       aTag == SCTAG_DOM_SYSTEM_PRINCIPAL ||
       aTag == SCTAG_DOM_CONTENT_PRINCIPAL) {
     JSPrincipals* prin;
     if (!nsJSPrincipals::ReadKnownPrincipalType(aCx, aReader, aTag, &prin)) {
       return nullptr;
     }
     // nsJSPrincipals::ReadKnownPrincipalType addrefs for us, but because of the
@@ -509,16 +549,34 @@ StructuredCloneHolder::WriteFullySeriali
     CryptoKey* key = nullptr;
     if (NS_SUCCEEDED(UNWRAP_OBJECT(CryptoKey, aObj, key))) {
       MOZ_ASSERT(NS_IsMainThread());
       return JS_WriteUint32Pair(aWriter, SCTAG_DOM_WEBCRYPTO_KEY, 0) &&
              key->WriteStructuredClone(aWriter);
     }
   }
 
+#ifndef MOZ_SIMPLEPUSH
+  {
+    if (NS_IsMainThread()) {
+      PushSubscription* subscription = nullptr;
+      if (NS_SUCCEEDED(UNWRAP_OBJECT(PushSubscription, aObj, subscription))) {
+        return JS_WriteUint32Pair(aWriter, SCTAG_DOM_PUSH_SUBSCRIPTION, 0) &&
+               subscription->WriteStructuredClone(aCx, aWriter);
+      }
+    } else {
+      WorkerPushSubscription* subscription = nullptr;
+      if (NS_SUCCEEDED(UNWRAP_WORKER_OBJECT(PushSubscription, aObj, subscription))) {
+        return JS_WriteUint32Pair(aWriter, SCTAG_DOM_PUSH_SUBSCRIPTION, 0) &&
+               subscription->WriteStructuredClone(aCx, aWriter);
+      }
+    }
+  }
+#endif
+
 #ifdef MOZ_WEBRTC
   {
     // Handle WebRTC Certificate cloning
     RTCCertificate* cert = nullptr;
     if (NS_SUCCEEDED(UNWRAP_OBJECT(RTCCertificate, aObj, cert))) {
       MOZ_ASSERT(NS_IsMainThread());
       return JS_WriteUint32Pair(aWriter, SCTAG_DOM_RTC_CERTIFICATE, 0) &&
              cert->WriteStructuredClone(aWriter);
--- a/dom/base/StructuredCloneTags.h
+++ b/dom/base/StructuredCloneTags.h
@@ -46,15 +46,17 @@ enum StructuredCloneTags {
 
   SCTAG_DOM_RTC_CERTIFICATE,
 
   SCTAG_DOM_FORMDATA,
 
   // This tag is for OffscreenCanvas.
   SCTAG_DOM_CANVAS,
 
-  SCTAG_DOM_MAX
+  SCTAG_DOM_MAX,
+
+  SCTAG_DOM_PUSH_SUBSCRIPTION
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // StructuredCloneTags_h__
--- a/dom/push/Push.js
+++ b/dom/push/Push.js
@@ -204,17 +204,16 @@ PushSubscriptionCallback.prototype = {
     }
 
     let publicKey = this._getKey(subscription, "p256dh");
     let authSecret = this._getKey(subscription, "auth");
     let sub = new pushManager._window.PushSubscription(subscription.endpoint,
                                                        pushManager._scope,
                                                        publicKey,
                                                        authSecret);
-    sub.setPrincipal(pushManager._principal);
     this.resolve(sub);
   },
 
   _getKey: function(subscription, name) {
     let outKeyLen = {};
     let rawKey = subscription.getKey(name, outKeyLen);
     if (!outKeyLen.value) {
       return null;
--- a/dom/push/PushManager.cpp
+++ b/dom/push/PushManager.cpp
@@ -11,25 +11,28 @@
 #include "mozilla/Services.h"
 #include "mozilla/unused.h"
 #include "mozilla/dom/PushManagerBinding.h"
 #include "mozilla/dom/PushSubscriptionBinding.h"
 #include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h"
 
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseWorkerProxy.h"
+#include "mozilla/dom/ToJSValue.h"
 
 #include "nsIGlobalObject.h"
 #include "nsIPermissionManager.h"
 #include "nsIPrincipal.h"
 #include "nsIPushService.h"
+#include "nsIScriptObjectPrincipal.h"
 
 #include "nsComponentManagerUtils.h"
 #include "nsFrameMessageManager.h"
 #include "nsContentCID.h"
+#include "nsJSUtils.h"
 
 #include "WorkerRunnable.h"
 #include "WorkerPrivate.h"
 #include "WorkerScope.h"
 
 namespace mozilla {
 namespace dom {
 
@@ -81,16 +84,88 @@ SubscriptionToJSON(PushSubscriptionJSON&
   Unused << NS_WARN_IF(NS_FAILED(rv));
 
   aJSON.mKeys.mAuth.Construct();
   rv = Base64URLEncode(aAuthSecret.Length(), aAuthSecret.Elements(),
                        aJSON.mKeys.mAuth.Value());
   Unused << NS_WARN_IF(NS_FAILED(rv));
 }
 
+/* Structured clone helpers for subscriptions. */
+
+bool
+ReadByteArray(JSContext* aCx, JSStructuredCloneReader* aReader,
+              nsTArray<uint8_t>& aArray)
+{
+  uint32_t length, zero;
+  if (!JS_ReadUint32Pair(aReader, &length, &zero)) {
+    return false;
+  }
+  if (length > 0) {
+    return aArray.SetLength(length, fallible) &&
+           JS_ReadBytes(aReader, aArray.Elements(), aArray.Length());
+  }
+  return true;
+}
+
+bool
+WriteByteArray(JSContext* aCx, JSStructuredCloneWriter* aWriter,
+               const nsTArray<uint8_t>& aArray)
+{
+  uint32_t length = aArray.Length();
+  if (JS_WriteUint32Pair(aWriter, length, 0)) {
+    if (length > 0) {
+      return JS_WriteBytes(aWriter, aArray.Elements(), length);
+    }
+    return true;
+  }
+  return false;
+}
+
+bool
+WriteSubscriptionClone(JSContext* aCx, JSStructuredCloneWriter* aWriter,
+                       const nsAString& aEndpoint,
+                       const nsTArray<uint8_t>& aRawP256dhKey,
+                       const nsTArray<uint8_t>& aAuthSecret)
+{
+  JS::Rooted<JS::Value> endpointValue(aCx);
+  if (!ToJSValue(aCx, aEndpoint, &endpointValue)) {
+    return false;
+  }
+  JS::Rooted<JSString*> endpoint(aCx, endpointValue.toString());
+  if (!endpoint ||
+      !JS_WriteString(aWriter, endpoint) ||
+      !WriteByteArray(aCx, aWriter, aRawP256dhKey) ||
+      !WriteByteArray(aCx, aWriter, aAuthSecret)) {
+    return false;
+  }
+  return true;
+}
+
+bool
+ReadSubscriptionClone(JSContext* aCx, JSStructuredCloneReader* aReader,
+                      nsAString& aEndpoint, nsTArray<uint8_t>& aRawP256dhKey,
+                      nsTArray<uint8_t>& aAuthSecret)
+{
+  JS::Rooted<JS::Value> endpointValue(aCx);
+  if (!JS_ReadString(aReader, &endpointValue)) {
+    return false;
+  }
+  nsAutoJSString endpoint;
+  if (!endpoint.init(aCx, endpointValue)) {
+    return false;
+  }
+  aEndpoint = endpoint;
+  if (!ReadByteArray(aCx, aReader, aRawP256dhKey) ||
+      !ReadByteArray(aCx, aReader, aAuthSecret)) {
+    return false;
+  }
+  return true;
+}
+
 } // anonymous namespace
 
 class UnsubscribeResultCallback final : public nsIUnsubscribeResultCallback
 {
 public:
   NS_DECL_ISUPPORTS
 
   explicit UnsubscribeResultCallback(Promise* aPromise)
@@ -118,37 +193,45 @@ private:
   RefPtr<Promise> mPromise;
 };
 
 NS_IMPL_ISUPPORTS(UnsubscribeResultCallback, nsIUnsubscribeResultCallback)
 
 already_AddRefed<Promise>
 PushSubscription::Unsubscribe(ErrorResult& aRv)
 {
-  MOZ_ASSERT(mPrincipal);
-
   nsCOMPtr<nsIPushService> service =
     do_GetService("@mozilla.org/push/Service;1");
   if (NS_WARN_IF(!service)) {
     aRv = NS_ERROR_FAILURE;
     return nullptr;
   }
 
   RefPtr<Promise> p = Promise::Create(mGlobal, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   RefPtr<UnsubscribeResultCallback> callback =
     new UnsubscribeResultCallback(p);
   Unused << NS_WARN_IF(NS_FAILED(
-    service->Unsubscribe(mScope, mPrincipal, callback)));
+    service->Unsubscribe(mScope, GetPrincipal(), callback)));
   return p.forget();
 }
 
+nsIPrincipal*
+PushSubscription::GetPrincipal()
+{
+  nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(GetParentObject());
+  if (!sop) {
+    return nullptr;
+  }
+  return sop->GetPrincipal();
+}
+
 void
 PushSubscription::ToJSON(PushSubscriptionJSON& aJSON)
 {
   SubscriptionToJSON(aJSON, mEndpoint, mRawP256dhKey, mAuthSecret);
 }
 
 PushSubscription::PushSubscription(nsIGlobalObject* aGlobal,
                                    const nsAString& aEndpoint,
@@ -186,21 +269,31 @@ PushSubscription::GetKey(JSContext* aCx,
     aKey.set(ArrayBuffer::Create(aCx,
                                  mAuthSecret.Length(),
                                  mAuthSecret.Elements()));
   } else {
     aKey.set(nullptr);
   }
 }
 
-void
-PushSubscription::SetPrincipal(nsIPrincipal* aPrincipal)
+bool
+PushSubscription::WriteStructuredClone(JSContext* aCx,
+                                       JSStructuredCloneWriter* aWriter) const
 {
-  MOZ_ASSERT(!mPrincipal);
-  mPrincipal = aPrincipal;
+  return WriteSubscriptionClone(aCx, aWriter, mEndpoint, mRawP256dhKey,
+                                mAuthSecret);
+}
+
+bool
+PushSubscription::ReadStructuredClone(JSContext* aCx,
+                                      JSStructuredCloneReader* aReader)
+{
+  nsCOMPtr<nsIPrincipal> principal;
+  return ReadSubscriptionClone(aCx, aReader, mEndpoint, mRawP256dhKey,
+                               mAuthSecret);
 }
 
 // static
 already_AddRefed<PushSubscription>
 PushSubscription::Constructor(GlobalObject& aGlobal,
                               const nsAString& aEndpoint,
                               const nsAString& aScope,
                               const Nullable<ArrayBuffer>& aP256dhKey,
@@ -229,17 +322,17 @@ PushSubscription::Constructor(GlobalObje
                                                       aEndpoint,
                                                       aScope,
                                                       rawKey,
                                                       authSecret);
 
   return sub.forget();
 }
 
-NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushSubscription, mGlobal, mPrincipal)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushSubscription, mGlobal)
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(PushSubscription)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(PushSubscription)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushSubscription)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
@@ -303,20 +396,17 @@ NS_INTERFACE_MAP_END
 WorkerPushSubscription::WorkerPushSubscription(const nsAString& aEndpoint,
                                                const nsAString& aScope,
                                                const nsTArray<uint8_t>& aRawP256dhKey,
                                                const nsTArray<uint8_t>& aAuthSecret)
   : mEndpoint(aEndpoint)
   , mScope(aScope)
   , mRawP256dhKey(aRawP256dhKey)
   , mAuthSecret(aAuthSecret)
-{
-  MOZ_ASSERT(!aScope.IsEmpty());
-  MOZ_ASSERT(!aEndpoint.IsEmpty());
-}
+{}
 
 WorkerPushSubscription::~WorkerPushSubscription()
 {}
 
 JSObject*
 WorkerPushSubscription::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return PushSubscriptionBinding_workers::Wrap(aCx, this, aGivenProto);
@@ -374,16 +464,33 @@ WorkerPushSubscription::GetKey(JSContext
     aKey.set(ArrayBuffer::Create(aCx,
                                  mAuthSecret.Length(),
                                  mAuthSecret.Elements()));
   } else {
     aKey.set(nullptr);
   }
 }
 
+bool
+WorkerPushSubscription::WriteStructuredClone(JSContext* aCx,
+  JSStructuredCloneWriter* aWriter) const
+{
+  return WriteSubscriptionClone(aCx, aWriter, mEndpoint, mRawP256dhKey,
+                                mAuthSecret);
+}
+
+bool
+WorkerPushSubscription::ReadStructuredClone(JSContext* aCx,
+  JSStructuredCloneReader* aReader)
+{
+  nsCOMPtr<nsIPrincipal> principal;
+  return ReadSubscriptionClone(aCx, aReader, mEndpoint, mRawP256dhKey,
+                               mAuthSecret);
+}
+
 class UnsubscribeResultRunnable final : public WorkerRunnable
 {
 public:
   UnsubscribeResultRunnable(PromiseWorkerProxy* aProxy,
                             nsresult aStatus,
                             bool aSuccess)
     : WorkerRunnable(aProxy->GetWorkerPrivate(), WorkerThreadModifyBusyCount)
     , mProxy(aProxy)
--- a/dom/push/PushManager.h
+++ b/dom/push/PushManager.h
@@ -28,16 +28,18 @@
  * verbose to implement in C++ compared to JS.
  */
 
 #ifndef mozilla_dom_PushManager_h
 #define mozilla_dom_PushManager_h
 
 #include "nsWrapperCache.h"
 
+#include "js/StructuredClone.h"
+
 #include "mozilla/AlreadyAddRefed.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/TypedArray.h"
 
 #include "nsCOMPtr.h"
 #include "mozilla/RefPtr.h"
 #include "jsapi.h"
@@ -93,25 +95,31 @@ public:
   static already_AddRefed<PushSubscription>
   Constructor(GlobalObject& aGlobal,
               const nsAString& aEndpoint,
               const nsAString& aScope,
               const Nullable<ArrayBuffer>& aP256dhKey,
               const Nullable<ArrayBuffer>& aAuthSecret,
               ErrorResult& aRv);
 
-  void
-  SetPrincipal(nsIPrincipal* aPrincipal);
+  nsIPrincipal*
+  GetPrincipal();
 
   already_AddRefed<Promise>
   Unsubscribe(ErrorResult& aRv);
 
   void
   ToJSON(PushSubscriptionJSON& aJSON);
 
+  bool
+  WriteStructuredClone(JSContext* aCx, JSStructuredCloneWriter* aWriter) const;
+
+  bool
+  ReadStructuredClone(JSContext* aCx, JSStructuredCloneReader* aReader);
+
 protected:
   ~PushSubscription();
 
 private:
   nsCOMPtr<nsIGlobalObject> mGlobal;
   nsCOMPtr<nsIPrincipal> mPrincipal;
   nsString mEndpoint;
   nsString mScope;
@@ -198,16 +206,22 @@ public:
          JS::MutableHandle<JSObject*> aP256dhKey);
 
   already_AddRefed<Promise>
   Unsubscribe(ErrorResult& aRv);
 
   void
   ToJSON(PushSubscriptionJSON& aJSON);
 
+  bool
+  WriteStructuredClone(JSContext* aCx, JSStructuredCloneWriter* aWriter) const;
+
+  bool
+  ReadStructuredClone(JSContext* aCx, JSStructuredCloneReader* aReader);
+
 protected:
   ~WorkerPushSubscription();
 
 private:
   nsString mEndpoint;
   nsString mScope;
   nsTArray<uint8_t> mRawP256dhKey;
   nsTArray<uint8_t> mAuthSecret;
--- a/dom/push/test/test_register.html
+++ b/dom/push/test/test_register.html
@@ -51,21 +51,28 @@ http://creativecommons.org/licenses/publ
   var pushSubscription;
   add_task(function* subscribe() {
     pushSubscription = yield registration.pushManager.subscribe();
   });
 
   add_task(function* resubscribe() {
     var data = yield sendRequestToWorker({
       type: "resubscribe",
-      endpoint: pushSubscription.endpoint,
+      subscription: pushSubscription,
     });
     pushSubscription = yield registration.pushManager.getSubscription();
     is(data.endpoint, pushSubscription.endpoint,
        "Subscription endpoints should match after resubscribing in worker");
+    ["p256dh", "auth"].forEach(keyName => {
+      isDeeply(
+        new Uint8Array(data.getKey(keyName)),
+        new Uint8Array(pushSubscription.getKey(keyName)),
+        "Mismatched key after resubscribing in worker: " + keyName
+      );
+    });
   });
 
   add_task(function* waitForPushNotification() {
     yield Promise.all([
       controlledFrame.waitOnWorkerMessage("finished"),
       fetch("http://mochi.test:8888/tests/dom/push/test/push-server.sjs", {
         method: "PUT",
         headers: {
--- a/dom/push/test/worker.js
+++ b/dom/push/test/worker.js
@@ -81,30 +81,26 @@ function handleMessage(event) {
         auth: subscription.getKey("auth"),
       })
     ));
     return;
   }
   if (event.data.type == "resubscribe") {
     reply(event, self.registration.pushManager.getSubscription().then(
       subscription => {
-        assert(subscription.endpoint == event.data.endpoint,
+        assert(subscription.endpoint == event.data.subscription.endpoint,
           "Wrong push endpoint in worker");
         return subscription.unsubscribe();
       }
     ).then(result => {
       assert(result, "Error unsubscribing in worker");
       return self.registration.pushManager.getSubscription();
     }).then(subscription => {
       assert(!subscription, "Subscription not removed in worker");
       return self.registration.pushManager.subscribe();
-    }).then(subscription => {
-      return {
-        endpoint: subscription.endpoint,
-      };
     }));
     return;
   }
   if (event.data.type == "denySubscribe") {
     reply(event, self.registration.pushManager.getSubscription().then(
       subscription => {
         assert(!subscription,
           "Should not return worker subscription with revoked permission");
--- a/dom/webidl/PushSubscription.webidl
+++ b/dom/webidl/PushSubscription.webidl
@@ -34,13 +34,9 @@ interface PushSubscription
 {
     readonly attribute USVString endpoint;
     ArrayBuffer? getKey(PushEncryptionKeyName name);
     [Throws, UseCounter]
     Promise<boolean> unsubscribe();
 
     // Implements the custom serializer specified in Push API, section 9.
     PushSubscriptionJSON toJSON();
-
-    // Used to set the principal from the JS implemented PushManager.
-    [Exposed=Window,ChromeOnly]
-    void setPrincipal(Principal principal);
 };