| Index: Source/modules/encryptedmedia/MediaKeySession.cpp
|
| diff --git a/Source/modules/encryptedmedia/MediaKeySession.cpp b/Source/modules/encryptedmedia/MediaKeySession.cpp
|
| index 9c779cb435399b4c5c700f0d06ae6793ae017a74..0cb30163c1c6ad5ab4e3a23589442aed9c971008 100644
|
| --- a/Source/modules/encryptedmedia/MediaKeySession.cpp
|
| +++ b/Source/modules/encryptedmedia/MediaKeySession.cpp
|
| @@ -47,6 +47,7 @@
|
| #include "public/platform/WebContentDecryptionModuleSession.h"
|
| #include "public/platform/WebString.h"
|
| #include "public/platform/WebURL.h"
|
| +#include "wtf/ASCIICType.h"
|
| #include "wtf/ArrayBuffer.h"
|
| #include "wtf/ArrayBufferView.h"
|
| #include <cmath>
|
| @@ -54,12 +55,16 @@
|
|
|
| namespace {
|
|
|
| -// The list of possible values for |sessionType| passed to createSession().
|
| -#if ENABLE(ASSERT)
|
| +// The list of possible values for |sessionType|.
|
| const char* kTemporary = "temporary";
|
| -#endif
|
| const char* kPersistent = "persistent";
|
|
|
| +// Minimum and maximum length for session ids.
|
| +enum {
|
| + MinSessionIdLength = 1,
|
| + MaxSessionIdLength = 512
|
| +};
|
| +
|
| } // namespace
|
|
|
| namespace blink {
|
| @@ -81,11 +86,30 @@ static bool isKeySystemSupportedWithInitDataType(const String& keySystem, const
|
| return MIMETypeRegistry::isSupportedEncryptedMediaMIMEType(keySystem, type.type(), type.parameter("codecs"));
|
| }
|
|
|
| +// Checks that |sessionId| looks correct and returns whether all checks pass.
|
| +static bool isValidSessionId(const String& sessionId)
|
| +{
|
| + if ((sessionId.length() < MinSessionIdLength) || (sessionId.length() > MaxSessionIdLength))
|
| + return false;
|
| +
|
| + if (!sessionId.containsOnlyASCII())
|
| + return false;
|
| +
|
| + // Check that the sessionId only contains alphanumeric characters.
|
| + for (unsigned i = 0; i < sessionId.length(); ++i) {
|
| + if (!isASCIIAlphanumeric(sessionId[i]))
|
| + return false;
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| // A class holding a pending action.
|
| class MediaKeySession::PendingAction : public GarbageCollectedFinalized<MediaKeySession::PendingAction> {
|
| public:
|
| enum Type {
|
| GenerateRequest,
|
| + Load,
|
| Update,
|
| Close,
|
| Remove
|
| @@ -107,7 +131,13 @@ public:
|
| const String& initDataType() const
|
| {
|
| ASSERT(m_type == GenerateRequest);
|
| - return m_initDataType;
|
| + return m_stringData;
|
| + }
|
| +
|
| + const String& sessionId() const
|
| + {
|
| + ASSERT(m_type == Load);
|
| + return m_stringData;
|
| }
|
|
|
| static PendingAction* CreatePendingGenerateRequest(ContentDecryptionModuleResult* result, const String& initDataType, PassRefPtr<ArrayBuffer> initData)
|
| @@ -117,6 +147,12 @@ public:
|
| return new PendingAction(GenerateRequest, result, initDataType, initData);
|
| }
|
|
|
| + static PendingAction* CreatePendingLoadRequest(ContentDecryptionModuleResult* result, const String& sessionId)
|
| + {
|
| + ASSERT(result);
|
| + return new PendingAction(Load, result, sessionId, PassRefPtr<ArrayBuffer>());
|
| + }
|
| +
|
| static PendingAction* CreatePendingUpdate(ContentDecryptionModuleResult* result, PassRefPtr<ArrayBuffer> data)
|
| {
|
| ASSERT(result);
|
| @@ -146,17 +182,17 @@ public:
|
| }
|
|
|
| private:
|
| - PendingAction(Type type, ContentDecryptionModuleResult* result, const String& initDataType, PassRefPtr<ArrayBuffer> data)
|
| + PendingAction(Type type, ContentDecryptionModuleResult* result, const String& stringData, PassRefPtr<ArrayBuffer> data)
|
| : m_type(type)
|
| , m_result(result)
|
| - , m_initDataType(initDataType)
|
| + , m_stringData(stringData)
|
| , m_data(data)
|
| {
|
| }
|
|
|
| const Type m_type;
|
| const Member<ContentDecryptionModuleResult> m_result;
|
| - const String m_initDataType;
|
| + const String m_stringData;
|
| const RefPtr<ArrayBuffer> m_data;
|
| };
|
|
|
| @@ -224,6 +260,81 @@ private:
|
| Member<MediaKeySession> m_session;
|
| };
|
|
|
| +// This class wraps the promise resolver used when loading a session
|
| +// and is passed to Chromium to fullfill the promise. This implementation of
|
| +// completeWithSession() will resolve the promise with true/false, while
|
| +// completeWithError() will reject the promise with an exception. complete()
|
| +// is not expected to be called, and will reject the promise.
|
| +class LoadSessionResult : public ContentDecryptionModuleResult {
|
| +public:
|
| + LoadSessionResult(ScriptState* scriptState, MediaKeySession* session)
|
| + : m_resolver(ScriptPromiseResolver::create(scriptState))
|
| + , m_session(session)
|
| + {
|
| + WTF_LOG(Media, "LoadSessionResult(%p)", this);
|
| + }
|
| +
|
| + virtual ~LoadSessionResult()
|
| + {
|
| + WTF_LOG(Media, "~LoadSessionResult(%p)", this);
|
| + }
|
| +
|
| + // ContentDecryptionModuleResult implementation.
|
| + virtual void complete() override
|
| + {
|
| + ASSERT_NOT_REACHED();
|
| + completeWithDOMException(InvalidStateError, "Unexpected completion.");
|
| + }
|
| +
|
| + virtual void completeWithSession(WebContentDecryptionModuleResult::SessionStatus status) override
|
| + {
|
| + bool result = false;
|
| + switch (status) {
|
| + case WebContentDecryptionModuleResult::NewSession:
|
| + result = true;
|
| + break;
|
| +
|
| + case WebContentDecryptionModuleResult::SessionNotFound:
|
| + result = false;
|
| + break;
|
| +
|
| + case WebContentDecryptionModuleResult::SessionAlreadyExists:
|
| + ASSERT_NOT_REACHED();
|
| + completeWithDOMException(InvalidStateError, "Unexpected completion.");
|
| + return;
|
| + }
|
| +
|
| + m_session->finishLoad();
|
| + m_resolver->resolve(result);
|
| + m_resolver.clear();
|
| + }
|
| +
|
| + virtual void completeWithError(WebContentDecryptionModuleException exceptionCode, unsigned long systemCode, const WebString& errorMessage) override
|
| + {
|
| + completeWithDOMException(WebCdmExceptionToExceptionCode(exceptionCode), errorMessage);
|
| + }
|
| +
|
| + // It is only valid to call this before completion.
|
| + ScriptPromise promise() { return m_resolver->promise(); }
|
| +
|
| + void trace(Visitor* visitor)
|
| + {
|
| + visitor->trace(m_session);
|
| + ContentDecryptionModuleResult::trace(visitor);
|
| + }
|
| +
|
| +private:
|
| + // Reject the promise with a DOMException.
|
| + void completeWithDOMException(ExceptionCode code, const String& errorMessage)
|
| + {
|
| + m_resolver->reject(DOMException::create(code, errorMessage));
|
| + m_resolver.clear();
|
| + }
|
| +
|
| + RefPtr<ScriptPromiseResolver> m_resolver;
|
| + Member<MediaKeySession> m_session;
|
| +};
|
| +
|
| MediaKeySession* MediaKeySession::create(ScriptState* scriptState, MediaKeys* mediaKeys, const String& sessionType)
|
| {
|
| ASSERT(sessionType == kTemporary || sessionType == kPersistent);
|
| @@ -232,6 +343,11 @@ MediaKeySession* MediaKeySession::create(ScriptState* scriptState, MediaKeys* me
|
| return session.get();
|
| }
|
|
|
| +bool MediaKeySession::isValidSessionType(const String& sessionType)
|
| +{
|
| + return (sessionType == kTemporary || sessionType == kPersistent);
|
| +}
|
| +
|
| MediaKeySession::MediaKeySession(ScriptState* scriptState, MediaKeys* mediaKeys, const String& sessionType)
|
| : ActiveDOMObject(scriptState->executionContext())
|
| , m_keySystem(mediaKeys->keySystem())
|
| @@ -265,7 +381,7 @@ MediaKeySession::MediaKeySession(ScriptState* scriptState, MediaKeys* mediaKeys,
|
| ASSERT(!closed(scriptState).isUndefinedOrNull());
|
|
|
| // 2.4 Let the session type be sessionType.
|
| - ASSERT(sessionType == m_sessionType);
|
| + ASSERT(isValidSessionType(sessionType));
|
|
|
| // 2.5 Let uninitialized be true.
|
| ASSERT(m_isUninitialized);
|
| @@ -377,6 +493,62 @@ ScriptPromise MediaKeySession::generateRequestInternal(ScriptState* scriptState,
|
| return promise;
|
| }
|
|
|
| +ScriptPromise MediaKeySession::load(ScriptState* scriptState, const String& sessionId)
|
| +{
|
| + WTF_LOG(Media, "MediaKeySession(%p)::load %s", this, sessionId.ascii().data());
|
| +
|
| + // From https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#dom-load:
|
| + // The load(sessionId) method loads the data stored for the sessionId into
|
| + // the session represented by the object. It must run the following steps:
|
| +
|
| + // 1. If this object's uninitialized value is false, return a promise
|
| + // rejected with a new DOMException whose name is "InvalidStateError".
|
| + if (!m_isUninitialized) {
|
| + return ScriptPromise::rejectWithDOMException(
|
| + scriptState, DOMException::create(InvalidStateError, "The session is already initialized."));
|
| + }
|
| +
|
| + // 2. Let this object's uninitialized be false.
|
| + m_isUninitialized = false;
|
| +
|
| + // 3. If sessionId is an empty string, return a promise rejected with a
|
| + // new DOMException whose name is "InvalidAccessError".
|
| + if (sessionId.isEmpty()) {
|
| + return ScriptPromise::rejectWithDOMException(
|
| + scriptState, DOMException::create(InvalidAccessError, "The sessionId parameter is empty."));
|
| + }
|
| +
|
| + // 4. If this object's session type is not "persistent", return a promise
|
| + // rejected with a new DOMException whose name is "InvalidAccessError".
|
| + if (m_sessionType != kPersistent) {
|
| + return ScriptPromise::rejectWithDOMException(
|
| + scriptState, DOMException::create(InvalidAccessError, "The session type is not 'persistent'."));
|
| + }
|
| +
|
| + // 5. Let media keys be the MediaKeys object that created this object.
|
| + // (Done in constructor.)
|
| + ASSERT(m_mediaKeys);
|
| +
|
| + // 6. If the content decryption module corresponding to media keys's
|
| + // keySystem attribute does not support loading previous sessions,
|
| + // return a promise rejected with a new DOMException whose name is
|
| + // "NotSupportedError".
|
| + // (Done by CDM.)
|
| +
|
| + // 7. Let promise be a new promise.
|
| + LoadSessionResult* result = new LoadSessionResult(scriptState, this);
|
| + ScriptPromise promise = result->promise();
|
| +
|
| + // 8. Run the following steps asynchronously (documented in
|
| + // actionTimerFired())
|
| + m_pendingActions.append(PendingAction::CreatePendingLoadRequest(result, sessionId));
|
| + ASSERT(!m_actionTimer.isActive());
|
| + m_actionTimer.startOneShot(0, FROM_HERE);
|
| +
|
| + // 9. Return promise.
|
| + return promise;
|
| +}
|
| +
|
| ScriptPromise MediaKeySession::update(ScriptState* scriptState, ArrayBuffer* response)
|
| {
|
| RefPtr<ArrayBuffer> responseCopy = ArrayBuffer::create(response->data(), response->byteLength());
|
| @@ -540,6 +712,63 @@ void MediaKeySession::actionTimerFired(Timer<MediaKeySession>*)
|
| // when |result| is resolved.
|
| break;
|
|
|
| + case PendingAction::Load:
|
| + // NOTE: Continue step 8 of MediaKeySession::load().
|
| +
|
| + // 8.1 Let sanitized session ID be a validated and/or sanitized
|
| + // version of sessionId. The user agent should thoroughly
|
| + // validate the sessionId value before passing it to the CDM.
|
| + // At a minimum, this should include checking that the length
|
| + // and value (e.g. alphanumeric) are reasonable.
|
| + // 8.2 If the previous step failed, reject promise with a new
|
| + // DOMException whose name is "InvalidAccessError".
|
| + if (!isValidSessionId(action->sessionId())) {
|
| + action->result()->completeWithError(WebContentDecryptionModuleExceptionInvalidAccessError, 0, "Invalid sessionId");
|
| + return;
|
| + }
|
| +
|
| + // 8.3 Let expiration time be NaN.
|
| + // (Done in the constructor.)
|
| + ASSERT(std::isnan(m_expiration));
|
| +
|
| + // 8.4 Let message be null.
|
| + // 8.5 Let message type be null.
|
| + // (Will be provided by the CDM if needed.)
|
| +
|
| + // 8.6 Let origin be the origin of this object's Document.
|
| + // (Obtained previously when CDM created.)
|
| +
|
| + // 8.7 Let cdm be the CDM loaded during the initialization of media
|
| + // keys.
|
| + // 8.8 Use the cdm to execute the following steps:
|
| + // 8.8.1 If there is no data stored for the sanitized session ID in
|
| + // the origin, resolve promise with false.
|
| + // 8.8.2 Let session data be the data stored for the sanitized
|
| + // session ID in the origin. This must not include data from
|
| + // other origin(s) or that is not associated with an origin.
|
| + // 8.8.3 If there is an unclosed "persistent" session in any
|
| + // Document representing the session data, reject promise
|
| + // with a new DOMException whose name is "QuotaExceededError".
|
| + // 8.8.4 In other words, do not create a session if a non-closed
|
| + // persistent session already exists for this sanitized
|
| + // session ID in any browsing context.
|
| + // 8.8.5 Load the session data.
|
| + // 8.8.6 If the session data indicates an expiration time for the
|
| + // session, let expiration time be the expiration time
|
| + // in milliseconds since 01 January 1970 UTC.
|
| + // 8.8.6 If the CDM needs to send a message:
|
| + // 8.8.6.1 Let message be a message generated by the CDM based on
|
| + // the session data.
|
| + // 8.8.6.2 Let message type be the appropriate MediaKeyMessageType
|
| + // for the message.
|
| + // 8.9 If any of the preceding steps failed, reject promise with a
|
| + // new DOMException whose name is the appropriate error name.
|
| + m_session->load(action->sessionId(), action->result()->result());
|
| +
|
| + // Remainder of steps executed in finishLoad(), called
|
| + // when |result| is resolved.
|
| + break;
|
| +
|
| case PendingAction::Update:
|
| WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Update", this);
|
| // NOTE: Continued from step 4 of MediaKeySession::update().
|
| @@ -613,6 +842,31 @@ void MediaKeySession::finishGenerateRequest()
|
| m_isCallable = true;
|
| }
|
|
|
| +void MediaKeySession::finishLoad()
|
| +{
|
| + // 8.10 Set the sessionId attribute to sanitized session ID.
|
| + ASSERT(!sessionId().isEmpty());
|
| +
|
| + // 8.11 Let this object's callable be true.
|
| + m_isCallable = true;
|
| +
|
| + // 8.12 If the loaded session contains usable keys, run the Usable
|
| + // Keys Changed algorithm on the session. The algorithm may
|
| + // also be run later should additional processing be necessary
|
| + // to determine with certainty whether one or more keys is
|
| + // usable.
|
| + // (Done by the CDM.)
|
| +
|
| + // 8.13 Run the Update Expiration algorithm on the session,
|
| + // providing expiration time.
|
| + // (Done by the CDM.)
|
| +
|
| + // 8.14 If message is not null, run the Queue a "message" Event
|
| + // algorithm on the session, providing message type and
|
| + // message.
|
| + // (Done by the CDM.)
|
| +}
|
| +
|
| // Queue a task to fire a simple event named keymessage at the new object.
|
| void MediaKeySession::message(const unsigned char* message, size_t messageLength, const WebURL& destinationURL)
|
| {
|
|
|