diff --git a/content/html/content/public/HTMLMediaElement.h b/content/html/content/public/HTMLMediaElement.h index 8eefc1bf384f..5086bbf3ebe7 100755 --- a/content/html/content/public/HTMLMediaElement.h +++ b/content/html/content/public/HTMLMediaElement.h @@ -545,6 +545,11 @@ public: bool IsEventAttributeName(nsIAtom* aName) MOZ_OVERRIDE; + + // Returns the principal of the "top level" document; the origin displayed + // in the URL bar of the browser window. + already_AddRefed GetTopLevelPrincipal(); + #endif // MOZ_EME bool MozAutoplayEnabled() const diff --git a/content/html/content/src/HTMLMediaElement.cpp b/content/html/content/src/HTMLMediaElement.cpp index a1cd0da764d8..1d125bfd3d8f 100755 --- a/content/html/content/src/HTMLMediaElement.cpp +++ b/content/html/content/src/HTMLMediaElement.cpp @@ -3407,6 +3407,11 @@ void HTMLMediaElement::NotifyDecoderPrincipalChanged() OutputMediaStream* ms = &mOutputStreams[i]; ms->mStream->CombineWithPrincipal(principal); } +#ifdef MOZ_EME + if (mMediaKeys && NS_FAILED(mMediaKeys->CheckPrincipals())) { + mMediaKeys->Shutdown(); + } +#endif } void HTMLMediaElement::UpdateMediaSize(nsIntSize size) @@ -4008,16 +4013,34 @@ HTMLMediaElement::SetMediaKeys(mozilla::dom::MediaKeys* aMediaKeys, aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } - // TODO: Need to shutdown existing MediaKeys instance? bug 1016709. nsRefPtr promise = Promise::Create(global, aRv); if (aRv.Failed()) { return nullptr; } - if (mMediaKeys != aMediaKeys) { - mMediaKeys = aMediaKeys; + if (mMediaKeys == aMediaKeys) { + promise->MaybeResolve(JS::UndefinedHandleValue); + return promise.forget(); } - if (mDecoder) { - mDecoder->SetCDMProxy(mMediaKeys->GetCDMProxy()); + if (aMediaKeys && aMediaKeys->IsBoundToMediaElement()) { + promise->MaybeReject(NS_ERROR_DOM_QUOTA_EXCEEDED_ERR); + return promise.forget(); + } + if (mMediaKeys) { + // Existing MediaKeys object. Shut it down. + mMediaKeys->Shutdown(); + mMediaKeys = nullptr; + } + + mMediaKeys = aMediaKeys; + if (mMediaKeys) { + if (NS_FAILED(mMediaKeys->Bind(this))) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + mMediaKeys = nullptr; + return promise.forget(); + } + if (mDecoder) { + mDecoder->SetCDMProxy(mMediaKeys->GetCDMProxy()); + } } promise->MaybeResolve(JS::UndefinedHandleValue); return promise.forget(); @@ -4063,6 +4086,28 @@ HTMLMediaElement::IsEventAttributeName(nsIAtom* aName) return aName == nsGkAtoms::onencrypted || nsGenericHTMLElement::IsEventAttributeName(aName); } + +already_AddRefed +HTMLMediaElement::GetTopLevelPrincipal() +{ + nsRefPtr principal; + nsCOMPtr window = do_QueryInterface(OwnerDoc()->GetParentObject()); + nsCOMPtr topWindow; + if (!window) { + return nullptr; + } + window->GetTop(getter_AddRefs(topWindow)); + nsCOMPtr top = do_QueryInterface(topWindow); + if (!top) { + return nullptr; + } + nsIDocument* doc = top->GetExtantDoc(); + if (!doc) { + return nullptr; + } + principal = doc->NodePrincipal(); + return principal.forget(); +} #endif // MOZ_EME NS_IMETHODIMP HTMLMediaElement::WindowVolumeChanged() diff --git a/content/media/eme/CDMProxy.cpp b/content/media/eme/CDMProxy.cpp index a9dae6a909d0..7c177e42640b 100644 --- a/content/media/eme/CDMProxy.cpp +++ b/content/media/eme/CDMProxy.cpp @@ -36,17 +36,18 @@ CDMProxy::~CDMProxy() } void -CDMProxy::Init(PromiseId aPromiseId) +CDMProxy::Init(PromiseId aPromiseId, + const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + bool aInPrivateBrowsing) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); - mNodeId = mKeys->GetNodeId(); - if (mNodeId.IsEmpty()) { - RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR); - return; - } - EME_LOG("Creating CDMProxy for origin='%s'", GetNodeId().get()); + EME_LOG("CDMProxy::Init (%s, %s) %s", + NS_ConvertUTF16toUTF8(aOrigin).get(), + NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(), + (aInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing")); if (!mGMPThread) { nsCOMPtr mps = @@ -61,8 +62,15 @@ CDMProxy::Init(PromiseId aPromiseId) return; } } - - nsRefPtr task(NS_NewRunnableMethodWithArg(this, &CDMProxy::gmp_Init, aPromiseId)); + nsAutoPtr data(new InitData()); + data->mPromiseId = aPromiseId; + data->mOrigin = aOrigin; + data->mTopLevelOrigin = aTopLevelOrigin; + data->mInPrivateBrowsing = aInPrivateBrowsing; + nsRefPtr task( + NS_NewRunnableMethodWithArg>(this, + &CDMProxy::gmp_Init, + data)); mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL); } @@ -75,26 +83,45 @@ CDMProxy::IsOnGMPThread() #endif void -CDMProxy::gmp_Init(uint32_t aPromiseId) +CDMProxy::gmp_Init(nsAutoPtr aData) { MOZ_ASSERT(IsOnGMPThread()); nsCOMPtr mps = do_GetService("@mozilla.org/gecko-media-plugin-service;1"); if (!mps) { - RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR); + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR); return; } + nsresult rv = mps->GetNodeId(aData->mOrigin, + aData->mTopLevelOrigin, + aData->mInPrivateBrowsing, + mNodeId); + MOZ_ASSERT(!GetNodeId().IsEmpty()); + if (NS_FAILED(rv)) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + EME_LOG("CDMProxy::gmp_Init (%s, %s) %s NodeId=%s", + NS_ConvertUTF16toUTF8(aData->mOrigin).get(), + NS_ConvertUTF16toUTF8(aData->mTopLevelOrigin).get(), + (aData->mInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing"), + GetNodeId().get()); + nsTArray tags; tags.AppendElement(NS_ConvertUTF16toUTF8(mKeySystem)); - nsresult rv = mps->GetGMPDecryptor(&tags, GetNodeId(), &mCDM); + rv = mps->GetGMPDecryptor(&tags, GetNodeId(), &mCDM); if (NS_FAILED(rv) || !mCDM) { - RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR); + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR); } else { mCallback = new CDMCallbackProxy(this); mCDM->Init(mCallback); - nsRefPtr task(NS_NewRunnableMethodWithArg(this, &CDMProxy::OnCDMCreated, aPromiseId)); + nsRefPtr task( + NS_NewRunnableMethodWithArg(this, + &CDMProxy::OnCDMCreated, + aData->mPromiseId)); NS_DispatchToMainThread(task); } } @@ -106,7 +133,8 @@ CDMProxy::OnCDMCreated(uint32_t aPromiseId) if (mKeys.IsNull()) { return; } - mKeys->OnCDMCreated(aPromiseId); + MOZ_ASSERT(!GetNodeId().IsEmpty()); + mKeys->OnCDMCreated(aPromiseId, GetNodeId()); } void @@ -303,9 +331,7 @@ CDMProxy::Shutdown() mKeys.Clear(); // Note: This may end up being the last owning reference to the CDMProxy. nsRefPtr task(NS_NewRunnableMethod(this, &CDMProxy::gmp_Shutdown)); - if (mGMPThread) { - mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL); - } + mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL); } void diff --git a/content/media/eme/CDMProxy.h b/content/media/eme/CDMProxy.h index 802ff5b058ba..690eca3384da 100644 --- a/content/media/eme/CDMProxy.h +++ b/content/media/eme/CDMProxy.h @@ -48,7 +48,10 @@ public: // Main thread only. // Loads the CDM corresponding to mKeySystem. // Calls MediaKeys::OnCDMCreated() when the CDM is created. - void Init(PromiseId aPromiseId); + void Init(PromiseId aPromiseId, + const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + bool aInPrivateBrowsing); // Main thread only. // Uses the CDM to create a key session. @@ -165,8 +168,15 @@ public: private: + struct InitData { + uint32_t mPromiseId; + nsAutoString mOrigin; + nsAutoString mTopLevelOrigin; + bool mInPrivateBrowsing; + }; + // GMP thread only. - void gmp_Init(uint32_t aPromiseId); + void gmp_Init(nsAutoPtr aData); // GMP thread only. void gmp_Shutdown(); diff --git a/content/media/eme/MediaKeys.cpp b/content/media/eme/MediaKeys.cpp index f9b15da29b2e..9872f5dfa8a1 100644 --- a/content/media/eme/MediaKeys.cpp +++ b/content/media/eme/MediaKeys.cpp @@ -25,6 +25,7 @@ namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeys, + mElement, mParent, mKeySessions, mPromises, @@ -224,23 +225,85 @@ MediaKeys::Create(const GlobalObject& aGlobal, // CDMProxy keeps MediaKeys alive until it resolves the promise and thus // returns the MediaKeys object to JS. nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); - if (!window) { + if (!window || !window->GetExtantDoc()) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } nsRefPtr keys = new MediaKeys(window, aKeySystem); - nsRefPtr promise(keys->MakePromise(aRv)); + return keys->Init(aRv); +} + +already_AddRefed +MediaKeys::Init(ErrorResult& aRv) +{ + nsRefPtr promise(MakePromise(aRv)); if (aRv.Failed()) { return nullptr; } - if (!IsSupportedKeySystem(aKeySystem)) { - aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); - return nullptr; + if (!IsSupportedKeySystem(mKeySystem)) { + promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return promise.forget(); } - keys->mProxy = new CDMProxy(keys, aKeySystem); + mProxy = new CDMProxy(this, mKeySystem); + + // Determine principal (at creation time) of the MediaKeys object. + nsCOMPtr sop = do_QueryInterface(GetParentObject()); + if (!sop) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + mPrincipal = sop->GetPrincipal(); + + // Determine principal of the "top-level" window; the principal of the + // page that will display in the URL bar. + nsCOMPtr window = do_QueryInterface(GetParentObject()); + if (!window) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + nsCOMPtr topWindow; + window->GetTop(getter_AddRefs(topWindow)); + nsCOMPtr top = do_QueryInterface(topWindow); + if (!top || !top->GetExtantDoc()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + + mTopLevelPrincipal = top->GetExtantDoc()->NodePrincipal(); + + if (!mPrincipal || !mTopLevelPrincipal) { + NS_WARNING("Failed to get principals when creating MediaKeys"); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + + nsAutoString origin; + nsresult rv = nsContentUtils::GetUTFOrigin(mPrincipal, origin); + if (NS_FAILED(rv)) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + nsAutoString topLevelOrigin; + rv = nsContentUtils::GetUTFOrigin(mTopLevelPrincipal, topLevelOrigin); + if (NS_FAILED(rv)) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + + if (!window) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + nsIDocument* doc = window->GetExtantDoc(); + const bool inPrivateBrowsing = nsContentUtils::IsInPrivateBrowsing(doc); + + EME_LOG("MediaKeys::Create() (%s, %s), %s", + NS_ConvertUTF16toUTF8(origin).get(), + NS_ConvertUTF16toUTF8(topLevelOrigin).get(), + (inPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing")); // The CDMProxy's initialization is asynchronous. The MediaKeys is // refcounted, and its instance is returned to JS by promise once @@ -250,22 +313,26 @@ MediaKeys::Create(const GlobalObject& aGlobal, // or its creation has failed. Store the id of the promise returned // here, and hold a self-reference until that promise is resolved or // rejected. - MOZ_ASSERT(!keys->mCreatePromiseId, "Should only be created once!"); - keys->mCreatePromiseId = keys->StorePromise(promise); - keys->AddRef(); - keys->mProxy->Init(keys->mCreatePromiseId); + MOZ_ASSERT(!mCreatePromiseId, "Should only be created once!"); + mCreatePromiseId = StorePromise(promise); + AddRef(); + mProxy->Init(mCreatePromiseId, + origin, + topLevelOrigin, + inPrivateBrowsing); return promise.forget(); } void -MediaKeys::OnCDMCreated(PromiseId aId) +MediaKeys::OnCDMCreated(PromiseId aId, const nsACString& aNodeId) { nsRefPtr promise(RetrievePromise(aId)); if (!promise) { NS_WARNING("MediaKeys tried to resolve a non-existent promise"); return; } + mNodeId = aNodeId; nsRefPtr keys(this); promise->MaybeResolve(keys); if (mCreatePromiseId == aId) { @@ -367,32 +434,59 @@ MediaKeys::GetSession(const nsAString& aSessionId) } const nsCString& -MediaKeys::GetNodeId() +MediaKeys::GetNodeId() const { MOZ_ASSERT(NS_IsMainThread()); - - // TODO: Bug 1035637, return a combination of origin and URL bar origin. - - if (!mNodeId.IsEmpty()) { - return mNodeId; - } - - nsIPrincipal* principal = nullptr; - nsCOMPtr pWindow = do_QueryInterface(GetParentObject()); - nsCOMPtr scriptPrincipal = - do_QueryInterface(pWindow); - if (scriptPrincipal) { - principal = scriptPrincipal->GetPrincipal(); - } - nsAutoString id; - if (principal && NS_SUCCEEDED(nsContentUtils::GetUTFOrigin(principal, id))) { - CopyUTF16toUTF8(id, mNodeId); - EME_LOG("EME Origin = '%s'", mNodeId.get()); - } - return mNodeId; } +bool +MediaKeys::IsBoundToMediaElement() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mElement != nullptr; +} + +nsresult +MediaKeys::Bind(HTMLMediaElement* aElement) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (IsBoundToMediaElement()) { + return NS_ERROR_FAILURE; + } + + mElement = aElement; + nsresult rv = CheckPrincipals(); + if (NS_FAILED(rv)) { + mElement = nullptr; + return rv; + } + + return NS_OK; +} + +nsresult +MediaKeys::CheckPrincipals() +{ + MOZ_ASSERT(NS_IsMainThread()); + if (!IsBoundToMediaElement()) { + return NS_ERROR_FAILURE; + } + + nsRefPtr elementPrincipal(mElement->GetCurrentPrincipal()); + nsRefPtr elementTopLevelPrincipal(mElement->GetTopLevelPrincipal()); + if (!elementPrincipal || + !mPrincipal || + !elementPrincipal->Equals(mPrincipal) || + !elementTopLevelPrincipal || + !mTopLevelPrincipal || + !elementTopLevelPrincipal->Equals(mTopLevelPrincipal)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + bool CopyArrayBufferViewOrArrayBufferData(const ArrayBufferViewOrArrayBuffer& aBufferOrView, nsTArray& aOutData) diff --git a/content/media/eme/MediaKeys.h b/content/media/eme/MediaKeys.h index 20b245a2c212..b7bc934d1f10 100644 --- a/content/media/eme/MediaKeys.h +++ b/content/media/eme/MediaKeys.h @@ -18,6 +18,7 @@ #include "mozilla/dom/Promise.h" #include "mozilla/dom/MediaKeysBinding.h" #include "mozilla/dom/UnionTypes.h" +#include "mozIGeckoMediaPluginService.h" namespace mozilla { @@ -26,6 +27,7 @@ class CDMProxy; namespace dom { class MediaKeySession; +class HTMLMediaElement; typedef nsRefPtrHashtable KeySessionHashMap; typedef nsRefPtrHashtable PromiseHashMap; @@ -55,6 +57,8 @@ public: virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE; + nsresult Bind(HTMLMediaElement* aElement); + // Javascript: readonly attribute DOMString keySystem; void GetKeySystem(nsString& retval) const; @@ -82,7 +86,7 @@ public: already_AddRefed GetSession(const nsAString& aSessionId); // Called once a Create() operation succeeds. - void OnCDMCreated(PromiseId aId); + void OnCDMCreated(PromiseId aId, const nsACString& aNodeId); // Called when GenerateRequest or Load have been called on a MediaKeySession // and we are waiting for its initialisation to finish. void OnSessionPending(PromiseId aId, MediaKeySession* aSession); @@ -107,12 +111,22 @@ public: // Resolves promise with "undefined". void ResolvePromise(PromiseId aId); - const nsCString& GetNodeId(); + const nsCString& GetNodeId() const; void Shutdown(); + // Returns true if this MediaKeys has been bound to a media element. + bool IsBoundToMediaElement() const; + + // Return NS_OK if the principals are the same as when the MediaKeys + // was created, failure otherwise. + nsresult CheckPrincipals(); + private: + bool IsInPrivateBrowsing(); + already_AddRefed Init(ErrorResult& aRv); + // Removes promise from mPromises, and returns it. already_AddRefed RetrievePromise(PromiseId aId); @@ -120,6 +134,8 @@ private: // and the MediaKeys destructor clears the proxy's reference to the MediaKeys. nsRefPtr mProxy; + nsRefPtr mElement; + nsCOMPtr mParent; nsString mKeySystem; nsCString mNodeId; @@ -127,6 +143,10 @@ private: PromiseHashMap mPromises; PendingKeySessionsHashMap mPendingSessions; PromiseId mCreatePromiseId; + + nsRefPtr mPrincipal; + nsRefPtr mTopLevelPrincipal; + }; } // namespace dom diff --git a/content/media/gmp/GMPService.cpp b/content/media/gmp/GMPService.cpp index 84be732287de..2a35af292860 100644 --- a/content/media/gmp/GMPService.cpp +++ b/content/media/gmp/GMPService.cpp @@ -22,6 +22,7 @@ #include "nsComponentManagerUtils.h" #include "mozilla/Preferences.h" #include "runnable_utils.h" +#include "VideoUtils.h" #if defined(XP_LINUX) && defined(MOZ_GMP_SANDBOX) #include "mozilla/Sandbox.h" #endif @@ -854,5 +855,28 @@ GeckoMediaPluginService::ReAddOnGMPThread(nsRefPtr& aOld) NS_DispatchToCurrentThread(WrapRunnableNM(&Dummy, aOld)); } +NS_IMETHODIMP +GeckoMediaPluginService::GetNodeId(const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + bool aInPrivateBrowsing, + nsACString& aOutId) +{ + MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); + LOGD(("%s::%s: (%s, %s), %s", __CLASS__, __FUNCTION__, + NS_ConvertUTF16toUTF8(aOrigin).get(), + NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(), + (aInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing"))); + + nsAutoCString salt; + nsresult rv = GenerateRandomPathName(salt, 32); + NS_ENSURE_SUCCESS(rv, rv); + + aOutId = salt; + + // TODO: Store salt, so it can be retrieved in subsequent sessions. + + return NS_OK; +} + } // namespace gmp } // namespace mozilla diff --git a/content/media/gmp/GMPStorageParent.cpp b/content/media/gmp/GMPStorageParent.cpp index 3539d9714e68..e54537d3c024 100644 --- a/content/media/gmp/GMPStorageParent.cpp +++ b/content/media/gmp/GMPStorageParent.cpp @@ -78,12 +78,7 @@ GetGMPStorageDir(nsIFile** aTempDir, const nsCString& aNodeId) return rv; } - // TODO: When aOrigin is the same node-id as the GMP sees in the child - // process (a UUID or somesuch), we can just append it un-hashed here. - // This should reduce the chance of hash collsions exposing data. - nsAutoString nodeIdHash; - nodeIdHash.AppendInt(HashString(aNodeId)); - rv = tmpFile->Append(nodeIdHash); + rv = tmpFile->AppendNative(aNodeId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } diff --git a/content/media/gmp/mozIGeckoMediaPluginService.idl b/content/media/gmp/mozIGeckoMediaPluginService.idl index 8c1c812d4dc0..a61735ce84e9 100644 --- a/content/media/gmp/mozIGeckoMediaPluginService.idl +++ b/content/media/gmp/mozIGeckoMediaPluginService.idl @@ -25,7 +25,7 @@ class GMPVideoHost; [ptr] native GMPDecryptorProxy(GMPDecryptorProxy); [ptr] native GMPAudioDecoderProxy(GMPAudioDecoderProxy); -[scriptable, uuid(88ade941-a423-48f9-aa3d-a383af8de4b8)] +[scriptable, uuid(3d811f9f-e1f8-48a5-a385-3657a641ee76)] interface mozIGeckoMediaPluginService : nsISupports { @@ -89,4 +89,11 @@ interface mozIGeckoMediaPluginService : nsISupports * @note Main-thread API. */ void removePluginDirectory(in AString directory); + + /** + * Gets the NodeId for a (origin, urlbarOrigin, isInprivateBrowsing) tuple. + */ + ACString getNodeId(in AString origin, + in AString topLevelOrigin, + in bool inPrivateBrowsingMode); };