blob: 54984969c74b7ed16c14ff3634ad58bb78225394 [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 "bindings/core/v8/ScriptState.h"
#include "core/dom/DOMArrayBuffer.h"
#include "core/dom/DOMException.h"
#include "core/dom/ExceptionCode.h"
#include "core/dom/ExecutionContext.h"
#include "core/html/HTMLMediaElement.h"
#include "modules/encryptedmedia/EncryptedMediaUtils.h"
#include "modules/encryptedmedia/MediaKeySession.h"
#include "modules/encryptedmedia/SimpleContentDecryptionModuleResultPromise.h"
#include "platform/Timer.h"
#include "public/platform/WebContentDecryptionModule.h"
#include "wtf/RefPtr.h"
#include <memory>
#define MEDIA_KEYS_LOG_LEVEL 3
namespace blink {
// A class holding a pending action.
class MediaKeys::PendingAction final
: public GarbageCollected<MediaKeys::PendingAction> {
public:
const Persistent<ContentDecryptionModuleResult> result() const {
return m_result;
}
DOMArrayBuffer* data() const { return m_data; }
static PendingAction* CreatePendingSetServerCertificate(
ContentDecryptionModuleResult* result,
DOMArrayBuffer* serverCertificate) {
DCHECK(result);
DCHECK(serverCertificate);
return new PendingAction(result, serverCertificate);
}
DEFINE_INLINE_TRACE() {
visitor->trace(m_result);
visitor->trace(m_data);
}
private:
PendingAction(ContentDecryptionModuleResult* result, DOMArrayBuffer* data)
: m_result(result), m_data(data) {}
const Member<ContentDecryptionModuleResult> m_result;
const Member<DOMArrayBuffer> m_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* scriptState)
: ContentDecryptionModuleResultPromise(scriptState) {}
~SetCertificateResultPromise() override {}
// ContentDecryptionModuleResult implementation.
void complete() override { resolve(true); }
void completeWithError(WebContentDecryptionModuleException exceptionCode,
unsigned long systemCode,
const WebString& errorMessage) override {
// 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 (exceptionCode == WebContentDecryptionModuleExceptionNotSupportedError) {
resolve(false);
return;
}
ContentDecryptionModuleResultPromise::completeWithError(
exceptionCode, systemCode, errorMessage);
}
};
MediaKeys* MediaKeys::create(
ExecutionContext* context,
const WebVector<WebEncryptedMediaSessionType>& supportedSessionTypes,
std::unique_ptr<WebContentDecryptionModule> cdm) {
MediaKeys* mediaKeys =
new MediaKeys(context, supportedSessionTypes, std::move(cdm));
mediaKeys->suspendIfNeeded();
return mediaKeys;
}
MediaKeys::MediaKeys(
ExecutionContext* context,
const WebVector<WebEncryptedMediaSessionType>& supportedSessionTypes,
std::unique_ptr<WebContentDecryptionModule> cdm)
: ActiveScriptWrappable(this),
ActiveDOMObject(context),
m_supportedSessionTypes(supportedSessionTypes),
m_cdm(std::move(cdm)),
m_mediaElement(nullptr),
m_reservedForMediaElement(false),
m_timer(this, &MediaKeys::timerFired) {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ")";
}
MediaKeys::~MediaKeys() {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ")";
}
MediaKeySession* MediaKeys::createSession(ScriptState* scriptState,
const String& sessionTypeString,
ExceptionState& exceptionState) {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ") "
<< sessionTypeString;
// 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 sessionType =
EncryptedMediaUtils::convertToSessionType(sessionTypeString);
if (!sessionTypeSupported(sessionType)) {
exceptionState.throwDOMException(NotSupportedError,
"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(scriptState, this, sessionType);
}
ScriptPromise MediaKeys::setServerCertificate(
ScriptState* scriptState,
const DOMArrayPiece& serverCertificate) {
// 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 serverCertificate is an empty array, return a promise rejected
// with a new DOMException whose name is "InvalidAccessError".
if (!serverCertificate.byteLength()) {
return ScriptPromise::rejectWithDOMException(
scriptState,
DOMException::create(InvalidAccessError,
"The serverCertificate parameter is empty."));
}
// 2. If the keySystem does not support server certificates, return a
// promise rejected with a new DOMException whose name is
// "NotSupportedError".
// (Let the CDM decide whether to support this or not.)
// 3. Let certificate be a copy of the contents of the serverCertificate
// parameter.
DOMArrayBuffer* serverCertificateBuffer = DOMArrayBuffer::create(
serverCertificate.data(), serverCertificate.byteLength());
// 4. Let promise be a new promise.
SetCertificateResultPromise* result =
new SetCertificateResultPromise(scriptState);
ScriptPromise promise = result->promise();
// 5. Run the following steps asynchronously (documented in timerFired()).
m_pendingActions.append(PendingAction::CreatePendingSetServerCertificate(
result, serverCertificateBuffer));
if (!m_timer.isActive())
m_timer.startOneShot(0, BLINK_FROM_HERE);
// 6. Return promise.
return promise;
}
bool MediaKeys::reserveForMediaElement(HTMLMediaElement* mediaElement) {
// If some other HtmlMediaElement already has a reference to us, fail.
if (m_mediaElement)
return false;
m_mediaElement = mediaElement;
m_reservedForMediaElement = true;
return true;
}
void MediaKeys::acceptReservation() {
m_reservedForMediaElement = false;
}
void MediaKeys::cancelReservation() {
m_reservedForMediaElement = false;
m_mediaElement.clear();
}
void MediaKeys::clearMediaElement() {
DCHECK(m_mediaElement);
m_mediaElement.clear();
}
bool MediaKeys::sessionTypeSupported(WebEncryptedMediaSessionType sessionType) {
for (size_t i = 0; i < m_supportedSessionTypes.size(); i++) {
if (m_supportedSessionTypes[i] == sessionType)
return true;
}
return false;
}
void MediaKeys::timerFired(TimerBase*) {
DCHECK(m_pendingActions.size());
// Swap the queue to a local copy to avoid problems if resolving promises
// run synchronously.
HeapDeque<Member<PendingAction>> pendingActions;
pendingActions.swap(m_pendingActions);
while (!pendingActions.isEmpty()) {
PendingAction* action = pendingActions.takeFirst();
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ") Certificate";
// 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*>(action->data()->data()),
action->data()->byteLength(), action->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.)
}
}
WebContentDecryptionModule* MediaKeys::contentDecryptionModule() {
return m_cdm.get();
}
DEFINE_TRACE(MediaKeys) {
visitor->trace(m_pendingActions);
visitor->trace(m_mediaElement);
ActiveDOMObject::trace(visitor);
}
void MediaKeys::contextDestroyed() {
ActiveDOMObject::contextDestroyed();
// We don't need the CDM anymore. Only destroyed after all related
// ActiveDOMObjects have been stopped.
m_cdm.reset();
}
bool MediaKeys::hasPendingActivity() const {
// Remain around if there are pending events.
DVLOG(MEDIA_KEYS_LOG_LEVEL)
<< __func__ << "(" << this << ")"
<< (!m_pendingActions.isEmpty() ? " !m_pendingActions.isEmpty()" : "")
<< (m_reservedForMediaElement ? " m_reservedForMediaElement" : "");
return !m_pendingActions.isEmpty() || m_reservedForMediaElement;
}
void MediaKeys::stop() {
ActiveDOMObject::stop();
if (m_timer.isActive())
m_timer.stop();
m_pendingActions.clear();
}
} // namespace blink