blob: 991936f0364128a04d7db99fe90acea11697f8d2 [file] [log] [blame]
/*
* Copyright (C) 2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "modules/encryptedmedia/MediaKeys.h"
#include <memory>
#include "bindings/core/v8/ScriptPromise.h"
#include "core/dom/DOMArrayBuffer.h"
#include "core/dom/DOMException.h"
#include "core/dom/ExceptionCode.h"
#include "core/dom/ExecutionContext.h"
#include "core/dom/TaskRunnerHelper.h"
#include "core/html/HTMLMediaElement.h"
#include "modules/encryptedmedia/ContentDecryptionModuleResultPromise.h"
#include "modules/encryptedmedia/EncryptedMediaUtils.h"
#include "modules/encryptedmedia/MediaKeySession.h"
#include "modules/encryptedmedia/MediaKeysPolicy.h"
#include "platform/InstanceCounters.h"
#include "platform/Timer.h"
#include "platform/bindings/ScriptState.h"
#include "platform/bindings/V8ThrowException.h"
#include "platform/wtf/RefPtr.h"
#include "public/platform/WebContentDecryptionModule.h"
#include "public/platform/WebEncryptedMediaKeyInformation.h"
#define MEDIA_KEYS_LOG_LEVEL 3
namespace blink {
// A class holding a pending action.
class MediaKeys::PendingAction final
: public GarbageCollectedFinalized<MediaKeys::PendingAction> {
public:
enum class Type { kSetServerCertificate, kGetStatusForPolicy };
Type GetType() const { return type_; }
const Persistent<ContentDecryptionModuleResult> Result() const {
return result_;
}
DOMArrayBuffer* Data() const {
DCHECK_EQ(Type::kSetServerCertificate, type_);
return data_;
}
const String& StringData() const {
DCHECK_EQ(Type::kGetStatusForPolicy, type_);
return string_data_;
}
static PendingAction* CreatePendingSetServerCertificate(
ContentDecryptionModuleResult* result,
DOMArrayBuffer* server_certificate) {
DCHECK(result);
DCHECK(server_certificate);
return new PendingAction(Type::kSetServerCertificate, result,
server_certificate, String());
}
static PendingAction* CreatePendingGetStatusForPolicy(
ContentDecryptionModuleResult* result,
const String& min_hdcp_version) {
DCHECK(result);
return new PendingAction(Type::kGetStatusForPolicy, result, nullptr,
min_hdcp_version);
}
DEFINE_INLINE_TRACE() {
visitor->Trace(result_);
visitor->Trace(data_);
}
private:
PendingAction(Type type,
ContentDecryptionModuleResult* result,
DOMArrayBuffer* data,
const String& string_data)
: type_(type), result_(result), data_(data), string_data_(string_data) {}
const Type type_;
const Member<ContentDecryptionModuleResult> result_;
const Member<DOMArrayBuffer> data_;
const String string_data_;
};
// This class wraps the promise resolver used when setting the certificate
// and is passed to Chromium to fullfill the promise. This implementation of
// complete() will resolve the promise with true, while completeWithError()
// will reject the promise with an exception. completeWithSession()
// is not expected to be called, and will reject the promise.
class SetCertificateResultPromise
: public ContentDecryptionModuleResultPromise {
public:
SetCertificateResultPromise(ScriptState* script_state, MediaKeys* media_keys)
: ContentDecryptionModuleResultPromise(script_state),
media_keys_(media_keys) {}
~SetCertificateResultPromise() override = default;
// ContentDecryptionModuleResult implementation.
void Complete() override {
if (!IsValidToFulfillPromise())
return;
Resolve(true);
}
void CompleteWithError(WebContentDecryptionModuleException exception_code,
unsigned long system_code,
const WebString& error_message) override {
if (!IsValidToFulfillPromise())
return;
// The EME spec specifies that "If the Key System implementation does
// not support server certificates, return a promise resolved with
// false." So convert any NOTSUPPORTEDERROR into resolving with false.
if (exception_code ==
kWebContentDecryptionModuleExceptionNotSupportedError) {
Resolve(false);
return;
}
ContentDecryptionModuleResultPromise::CompleteWithError(
exception_code, system_code, error_message);
}
DEFINE_INLINE_TRACE() {
visitor->Trace(media_keys_);
ContentDecryptionModuleResultPromise::Trace(visitor);
}
private:
// Keeping a reference to MediaKeys to prevent GC from collecting it while
// the promise is pending.
Member<MediaKeys> media_keys_;
};
// This class wraps the promise resolver used when getting the key status for
// policy and is passed to Chromium to fullfill the promise.
class GetStatusForPolicyResultPromise
: public ContentDecryptionModuleResultPromise {
public:
GetStatusForPolicyResultPromise(ScriptState* script_state,
MediaKeys* media_keys)
: ContentDecryptionModuleResultPromise(script_state),
media_keys_(media_keys) {}
~GetStatusForPolicyResultPromise() override = default;
// ContentDecryptionModuleResult implementation.
void CompleteWithKeyStatus(
WebEncryptedMediaKeyInformation::KeyStatus key_status) override {
if (!IsValidToFulfillPromise())
return;
Resolve(EncryptedMediaUtils::ConvertKeyStatusToString(key_status));
}
DEFINE_INLINE_TRACE() {
visitor->Trace(media_keys_);
ContentDecryptionModuleResultPromise::Trace(visitor);
}
private:
// Keeping a reference to MediaKeys to prevent GC from collecting it while
// the promise is pending.
Member<MediaKeys> media_keys_;
};
MediaKeys* MediaKeys::Create(
ExecutionContext* context,
const WebVector<WebEncryptedMediaSessionType>& supported_session_types,
std::unique_ptr<WebContentDecryptionModule> cdm) {
return new MediaKeys(context, supported_session_types, std::move(cdm));
}
MediaKeys::MediaKeys(
ExecutionContext* context,
const WebVector<WebEncryptedMediaSessionType>& supported_session_types,
std::unique_ptr<WebContentDecryptionModule> cdm)
: ContextLifecycleObserver(context),
supported_session_types_(supported_session_types),
cdm_(std::move(cdm)),
media_element_(nullptr),
reserved_for_media_element_(false),
timer_(TaskRunnerHelper::Get(TaskType::kMiscPlatformAPI, context),
this,
&MediaKeys::TimerFired) {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ")";
InstanceCounters::IncrementCounter(InstanceCounters::kMediaKeysCounter);
}
MediaKeys::~MediaKeys() {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ")";
InstanceCounters::DecrementCounter(InstanceCounters::kMediaKeysCounter);
}
MediaKeySession* MediaKeys::createSession(ScriptState* script_state,
const String& session_type_string,
ExceptionState& exception_state) {
DVLOG(MEDIA_KEYS_LOG_LEVEL)
<< __func__ << "(" << this << ") " << session_type_string;
// From http://w3c.github.io/encrypted-media/#createSession
// When this method is invoked, the user agent must run the following steps:
// 1. If this object's persistent state allowed value is false and
// sessionType is not "temporary", throw a new DOMException whose name is
// NotSupportedError.
// (Chromium ensures that only session types supported by the
// configuration are listed in supportedSessionTypes.)
// 2. If the Key System implementation represented by this object's cdm
// implementation value does not support sessionType, throw a new
// DOMException whose name is NotSupportedError.
WebEncryptedMediaSessionType session_type =
EncryptedMediaUtils::ConvertToSessionType(session_type_string);
if (!SessionTypeSupported(session_type)) {
exception_state.ThrowDOMException(kNotSupportedError,
"Unsupported session type.");
return nullptr;
}
// 3. Let session be a new MediaKeySession object, and initialize it as
// follows:
// (Initialization is performed in the constructor.)
// 4. Return session.
return MediaKeySession::Create(script_state, this, session_type);
}
ScriptPromise MediaKeys::setServerCertificate(
ScriptState* script_state,
const DOMArrayPiece& server_certificate) {
// From https://w3c.github.io/encrypted-media/#setServerCertificate
// The setServerCertificate(serverCertificate) method provides a server
// certificate to be used to encrypt messages to the license server.
// It must run the following steps:
// 1. If the Key System implementation represented by this object's cdm
// implementation value does not support server certificates, return
// a promise resolved with false.
// TODO(jrummell): Provide a way to determine if the CDM supports this.
// http://crbug.com/647816.
//
// 2. If serverCertificate is an empty array, return a promise rejected
// with a new a newly created TypeError.
if (!server_certificate.ByteLength()) {
return ScriptPromise::Reject(
script_state, V8ThrowException::CreateTypeError(
script_state->GetIsolate(),
"The serverCertificate parameter is empty."));
}
// 3. Let certificate be a copy of the contents of the serverCertificate
// parameter.
DOMArrayBuffer* server_certificate_buffer = DOMArrayBuffer::Create(
server_certificate.Data(), server_certificate.ByteLength());
// 4. Let promise be a new promise.
SetCertificateResultPromise* result =
new SetCertificateResultPromise(script_state, this);
ScriptPromise promise = result->Promise();
// 5. Run the following steps asynchronously. See SetServerCertificateTask().
pending_actions_.push_back(PendingAction::CreatePendingSetServerCertificate(
result, server_certificate_buffer));
if (!timer_.IsActive())
timer_.StartOneShot(0, BLINK_FROM_HERE);
// 6. Return promise.
return promise;
}
void MediaKeys::SetServerCertificateTask(
DOMArrayBuffer* server_certificate,
ContentDecryptionModuleResult* result) {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ")";
// 5.1 Let cdm be the cdm during the initialization of this object.
WebContentDecryptionModule* cdm = ContentDecryptionModule();
// 5.2 Use the cdm to process certificate.
cdm->SetServerCertificate(
static_cast<unsigned char*>(server_certificate->Data()),
server_certificate->ByteLength(), result->Result());
// 5.3 If any of the preceding steps failed, reject promise with a
// new DOMException whose name is the appropriate error name.
// 5.4 Resolve promise.
// (These are handled by Chromium and the CDM.)
}
ScriptPromise MediaKeys::getStatusForPolicy(
ScriptState* script_state,
const MediaKeysPolicy& media_keys_policy) {
// TODO(xhwang): Pass MediaKeysPolicy classes all the way to Chromium when
// we have more than one policy to check.
String min_hdcp_version = media_keys_policy.minHdcpVersion();
// Let promise be a new promise.
GetStatusForPolicyResultPromise* result =
new GetStatusForPolicyResultPromise(script_state, this);
ScriptPromise promise = result->Promise();
// Run the following steps asynchronously. See GetStatusForPolicyTask().
pending_actions_.push_back(
PendingAction::CreatePendingGetStatusForPolicy(result, min_hdcp_version));
if (!timer_.IsActive())
timer_.StartOneShot(0, BLINK_FROM_HERE);
// Return promise.
return promise;
}
void MediaKeys::GetStatusForPolicyTask(const String& min_hdcp_version,
ContentDecryptionModuleResult* result) {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << ": " << min_hdcp_version;
WebContentDecryptionModule* cdm = ContentDecryptionModule();
cdm->GetStatusForPolicy(min_hdcp_version, result->Result());
}
bool MediaKeys::ReserveForMediaElement(HTMLMediaElement* media_element) {
// If some other HtmlMediaElement already has a reference to us, fail.
if (media_element_)
return false;
media_element_ = media_element;
reserved_for_media_element_ = true;
return true;
}
void MediaKeys::AcceptReservation() {
reserved_for_media_element_ = false;
}
void MediaKeys::CancelReservation() {
reserved_for_media_element_ = false;
media_element_.Clear();
}
void MediaKeys::ClearMediaElement() {
DCHECK(media_element_);
media_element_.Clear();
}
bool MediaKeys::SessionTypeSupported(
WebEncryptedMediaSessionType session_type) {
for (size_t i = 0; i < supported_session_types_.size(); i++) {
if (supported_session_types_[i] == session_type)
return true;
}
return false;
}
void MediaKeys::TimerFired(TimerBase*) {
DCHECK(pending_actions_.size());
// Swap the queue to a local copy to avoid problems if resolving promises
// run synchronously.
HeapDeque<Member<PendingAction>> pending_actions;
pending_actions.Swap(pending_actions_);
while (!pending_actions.IsEmpty()) {
PendingAction* action = pending_actions.TakeFirst();
switch (action->GetType()) {
case PendingAction::Type::kSetServerCertificate:
SetServerCertificateTask(action->Data(), action->Result());
break;
case PendingAction::Type::kGetStatusForPolicy:
GetStatusForPolicyTask(action->StringData(), action->Result());
break;
}
}
}
WebContentDecryptionModule* MediaKeys::ContentDecryptionModule() {
return cdm_.get();
}
DEFINE_TRACE(MediaKeys) {
visitor->Trace(pending_actions_);
visitor->Trace(media_element_);
ContextLifecycleObserver::Trace(visitor);
}
void MediaKeys::ContextDestroyed(ExecutionContext*) {
timer_.Stop();
pending_actions_.clear();
// We don't need the CDM anymore. Only destroyed after all related
// ContextLifecycleObservers have been stopped.
cdm_.reset();
}
bool MediaKeys::HasPendingActivity() const {
// Remain around if there are pending events.
DVLOG(MEDIA_KEYS_LOG_LEVEL)
<< __func__ << "(" << this << ")"
<< (!pending_actions_.IsEmpty() ? " !pending_actions_.isEmpty()" : "")
<< (reserved_for_media_element_ ? " reserved_for_media_element_" : "");
return !pending_actions_.IsEmpty() || reserved_for_media_element_;
}
} // namespace blink