/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "GMPCDMProxy.h" #include "mozilla/EMEUtils.h" #include "mozilla/PodOperations.h" #include "mozilla/dom/MediaKeys.h" #include "mozilla/dom/MediaKeySession.h" #include "mozIGeckoMediaPluginService.h" #include "nsContentCID.h" #include "nsIConsoleService.h" #include "nsPrintfCString.h" #include "nsServiceManagerUtils.h" #include "nsString.h" #include "prenv.h" #include "GMPCDMCallbackProxy.h" #include "GMPService.h" #include "MainThreadUtils.h" #include "MediaData.h" namespace mozilla { GMPCDMProxy::GMPCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem, GMPCrashHelper* aCrashHelper, bool aDistinctiveIdentifierRequired, bool aPersistentStateRequired) : CDMProxy(aKeys, aKeySystem, aDistinctiveIdentifierRequired, aPersistentStateRequired) , mCrashHelper(aCrashHelper) , mCDM(nullptr) , mDecryptionJobCount(0) , mShutdownCalled(false) , mDecryptorId(0) , mCreatePromiseId(0) { MOZ_ASSERT(NS_IsMainThread()); MOZ_COUNT_CTOR(GMPCDMProxy); } GMPCDMProxy::~GMPCDMProxy() { MOZ_COUNT_DTOR(GMPCDMProxy); } void GMPCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin, const nsAString& aTopLevelOrigin, const nsAString& aGMPName, bool aInPrivateBrowsing) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); EME_LOG("GMPCDMProxy::Init (%s, %s) %s", NS_ConvertUTF16toUTF8(aOrigin).get(), NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(), (aInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing")); nsCString pluginVersion; if (!mOwnerThread) { nsCOMPtr mps = do_GetService("@mozilla.org/gecko-media-plugin-service;1"); if (!mps) { RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Couldn't get MediaPluginService in GMPCDMProxy::Init")); return; } mps->GetThread(getter_AddRefs(mOwnerThread)); if (!mOwnerThread) { RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Couldn't get GMP thread GMPCDMProxy::Init")); return; } } if (aGMPName.IsEmpty()) { RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, nsPrintfCString("Unknown GMP for keysystem '%s'", NS_ConvertUTF16toUTF8(mKeySystem).get())); return; } nsAutoPtr data(new InitData()); data->mPromiseId = aPromiseId; data->mOrigin = aOrigin; data->mTopLevelOrigin = aTopLevelOrigin; data->mGMPName = aGMPName; data->mInPrivateBrowsing = aInPrivateBrowsing; data->mCrashHelper = mCrashHelper; nsCOMPtr task( NewRunnableMethod>(this, &GMPCDMProxy::gmp_Init, Move(data))); mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); } #ifdef DEBUG bool GMPCDMProxy::IsOnOwnerThread() { return NS_GetCurrentThread() == mOwnerThread; } #endif void GMPCDMProxy::gmp_InitDone(GMPDecryptorProxy* aCDM, nsAutoPtr&& aData) { EME_LOG("GMPCDMProxy::gmp_InitDone"); if (mShutdownCalled) { if (aCDM) { aCDM->Close(); } RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("GMPCDMProxy was shut down before init could complete")); return; } if (!aCDM) { RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("GetGMPDecryptor failed to return a CDM")); return; } mCDM = aCDM; mCallback = new GMPCDMCallbackProxy(this); mCDM->Init(mCallback, mDistinctiveIdentifierRequired, mPersistentStateRequired); // Await the OnSetDecryptorId callback. mCreatePromiseId = aData->mPromiseId; } void GMPCDMProxy::OnSetDecryptorId(uint32_t aId) { MOZ_ASSERT(mCreatePromiseId); mDecryptorId = aId; nsCOMPtr task( NewRunnableMethod(this, &GMPCDMProxy::OnCDMCreated, mCreatePromiseId)); NS_DispatchToMainThread(task); } class gmp_InitDoneCallback : public GetGMPDecryptorCallback { public: gmp_InitDoneCallback(GMPCDMProxy* aGMPCDMProxy, nsAutoPtr&& aData) : mGMPCDMProxy(aGMPCDMProxy), mData(Move(aData)) { } void Done(GMPDecryptorProxy* aCDM) { mGMPCDMProxy->gmp_InitDone(aCDM, Move(mData)); } private: RefPtr mGMPCDMProxy; nsAutoPtr mData; }; class gmp_InitGetGMPDecryptorCallback : public GetNodeIdCallback { public: gmp_InitGetGMPDecryptorCallback(GMPCDMProxy* aGMPCDMProxy, nsAutoPtr&& aData) : mGMPCDMProxy(aGMPCDMProxy), mData(aData) { } void Done(nsresult aResult, const nsACString& aNodeId) { mGMPCDMProxy->gmp_InitGetGMPDecryptor(aResult, aNodeId, Move(mData)); } private: RefPtr mGMPCDMProxy; nsAutoPtr mData; }; void GMPCDMProxy::gmp_Init(nsAutoPtr&& aData) { MOZ_ASSERT(IsOnOwnerThread()); nsCOMPtr mps = do_GetService("@mozilla.org/gecko-media-plugin-service;1"); if (!mps) { RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Couldn't get MediaPluginService in GMPCDMProxy::gmp_Init")); return; } // Make a copy before we transfer ownership of aData to the // gmp_InitGetGMPDecryptorCallback. InitData data(*aData); UniquePtr callback( new gmp_InitGetGMPDecryptorCallback(this, Move(aData))); nsresult rv = mps->GetNodeId(data.mOrigin, data.mTopLevelOrigin, data.mGMPName, data.mInPrivateBrowsing, Move(callback)); if (NS_FAILED(rv)) { RejectPromise(data.mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Call to GetNodeId() failed early")); } } void GMPCDMProxy::gmp_InitGetGMPDecryptor(nsresult aResult, const nsACString& aNodeId, nsAutoPtr&& aData) { uint32_t promiseID = aData->mPromiseId; if (NS_FAILED(aResult)) { RejectPromise(promiseID, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("GetNodeId() called back, but with a failure result")); return; } mNodeId = aNodeId; MOZ_ASSERT(!GetNodeId().IsEmpty()); nsCOMPtr mps = do_GetService("@mozilla.org/gecko-media-plugin-service;1"); if (!mps) { RejectPromise(promiseID, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Couldn't get MediaPluginService in GMPCDMProxy::gmp_InitGetGMPDecryptor")); return; } EME_LOG("GMPCDMProxy::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)); // Note: must capture helper refptr here, before the Move() // when we create the GetGMPDecryptorCallback below. RefPtr crashHelper = Move(aData->mCrashHelper); UniquePtr callback(new gmp_InitDoneCallback(this, Move(aData))); nsresult rv = mps->GetGMPDecryptor(crashHelper, &tags, GetNodeId(), Move(callback)); if (NS_FAILED(rv)) { RejectPromise(promiseID, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Call to GetGMPDecryptor() failed early")); } } void GMPCDMProxy::OnCDMCreated(uint32_t aPromiseId) { MOZ_ASSERT(NS_IsMainThread()); if (mKeys.IsNull()) { return; } MOZ_ASSERT(!GetNodeId().IsEmpty()); if (mCDM) { mKeys->OnCDMCreated(aPromiseId, GetNodeId(), mCDM->GetPluginId()); } else { // No CDM? Just reject the promise. mKeys->RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Null CDM in OnCDMCreated()")); } } void GMPCDMProxy::CreateSession(uint32_t aCreateSessionToken, dom::MediaKeySessionType aSessionType, PromiseId aPromiseId, const nsAString& aInitDataType, nsTArray& aInitData) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mOwnerThread); nsAutoPtr data(new CreateSessionData()); data->mSessionType = aSessionType; data->mCreateSessionToken = aCreateSessionToken; data->mPromiseId = aPromiseId; data->mInitDataType = NS_ConvertUTF16toUTF8(aInitDataType); data->mInitData = Move(aInitData); nsCOMPtr task( NewRunnableMethod>(this, &GMPCDMProxy::gmp_CreateSession, data)); mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); } GMPSessionType ToGMPSessionType(dom::MediaKeySessionType aSessionType) { switch (aSessionType) { case dom::MediaKeySessionType::Temporary: return kGMPTemporySession; case dom::MediaKeySessionType::Persistent_license: return kGMPPersistentSession; default: return kGMPTemporySession; }; }; void GMPCDMProxy::gmp_CreateSession(nsAutoPtr aData) { MOZ_ASSERT(IsOnOwnerThread()); if (!mCDM) { RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Null CDM in gmp_CreateSession")); return; } mCDM->CreateSession(aData->mCreateSessionToken, aData->mPromiseId, aData->mInitDataType, aData->mInitData, ToGMPSessionType(aData->mSessionType)); } void GMPCDMProxy::LoadSession(PromiseId aPromiseId, const nsAString& aSessionId) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mOwnerThread); nsAutoPtr data(new SessionOpData()); data->mPromiseId = aPromiseId; data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); nsCOMPtr task( NewRunnableMethod>(this, &GMPCDMProxy::gmp_LoadSession, data)); mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); } void GMPCDMProxy::gmp_LoadSession(nsAutoPtr aData) { MOZ_ASSERT(IsOnOwnerThread()); if (!mCDM) { RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Null CDM in gmp_LoadSession")); return; } mCDM->LoadSession(aData->mPromiseId, aData->mSessionId); } void GMPCDMProxy::SetServerCertificate(PromiseId aPromiseId, nsTArray& aCert) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mOwnerThread); nsAutoPtr data(new SetServerCertificateData()); data->mPromiseId = aPromiseId; data->mCert = Move(aCert); nsCOMPtr task( NewRunnableMethod>(this, &GMPCDMProxy::gmp_SetServerCertificate, data)); mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); } void GMPCDMProxy::gmp_SetServerCertificate(nsAutoPtr aData) { MOZ_ASSERT(IsOnOwnerThread()); if (!mCDM) { RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Null CDM in gmp_SetServerCertificate")); return; } mCDM->SetServerCertificate(aData->mPromiseId, aData->mCert); } void GMPCDMProxy::UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId, nsTArray& aResponse) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mOwnerThread); NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); nsAutoPtr data(new UpdateSessionData()); data->mPromiseId = aPromiseId; data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); data->mResponse = Move(aResponse); nsCOMPtr task( NewRunnableMethod>(this, &GMPCDMProxy::gmp_UpdateSession, data)); mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); } void GMPCDMProxy::gmp_UpdateSession(nsAutoPtr aData) { MOZ_ASSERT(IsOnOwnerThread()); if (!mCDM) { RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Null CDM in gmp_UpdateSession")); return; } mCDM->UpdateSession(aData->mPromiseId, aData->mSessionId, aData->mResponse); } void GMPCDMProxy::CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); nsAutoPtr data(new SessionOpData()); data->mPromiseId = aPromiseId; data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); nsCOMPtr task( NewRunnableMethod>(this, &GMPCDMProxy::gmp_CloseSession, data)); mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); } void GMPCDMProxy::gmp_CloseSession(nsAutoPtr aData) { MOZ_ASSERT(IsOnOwnerThread()); if (!mCDM) { RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Null CDM in gmp_CloseSession")); return; } mCDM->CloseSession(aData->mPromiseId, aData->mSessionId); } void GMPCDMProxy::RemoveSession(const nsAString& aSessionId, PromiseId aPromiseId) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); nsAutoPtr data(new SessionOpData()); data->mPromiseId = aPromiseId; data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); nsCOMPtr task( NewRunnableMethod>(this, &GMPCDMProxy::gmp_RemoveSession, data)); mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); } void GMPCDMProxy::gmp_RemoveSession(nsAutoPtr aData) { MOZ_ASSERT(IsOnOwnerThread()); if (!mCDM) { RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, NS_LITERAL_CSTRING("Null CDM in gmp_RemoveSession")); return; } mCDM->RemoveSession(aData->mPromiseId, aData->mSessionId); } void GMPCDMProxy::Shutdown() { MOZ_ASSERT(NS_IsMainThread()); mKeys.Clear(); // Note: This may end up being the last owning reference to the GMPCDMProxy. nsCOMPtr task(NewRunnableMethod(this, &GMPCDMProxy::gmp_Shutdown)); if (mOwnerThread) { mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); } } void GMPCDMProxy::gmp_Shutdown() { MOZ_ASSERT(IsOnOwnerThread()); mShutdownCalled = true; // Abort any pending decrypt jobs, to awaken any clients waiting on a job. for (size_t i = 0; i < mDecryptionJobs.Length(); i++) { DecryptJob* job = mDecryptionJobs[i]; job->PostResult(AbortedErr); } mDecryptionJobs.Clear(); if (mCDM) { mCDM->Close(); mCDM = nullptr; } } void GMPCDMProxy::RejectPromise(PromiseId aId, nsresult aCode, const nsCString& aReason) { if (NS_IsMainThread()) { if (!mKeys.IsNull()) { mKeys->RejectPromise(aId, aCode, aReason); } } else { nsCOMPtr task(new RejectPromiseTask(this, aId, aCode, aReason)); NS_DispatchToMainThread(task); } } void GMPCDMProxy::ResolvePromise(PromiseId aId) { if (NS_IsMainThread()) { if (!mKeys.IsNull()) { mKeys->ResolvePromise(aId); } else { NS_WARNING("GMPCDMProxy unable to resolve promise!"); } } else { nsCOMPtr task; task = NewRunnableMethod(this, &GMPCDMProxy::ResolvePromise, aId); NS_DispatchToMainThread(task); } } const nsCString& GMPCDMProxy::GetNodeId() const { return mNodeId; } void GMPCDMProxy::OnSetSessionId(uint32_t aCreateSessionToken, const nsAString& aSessionId) { MOZ_ASSERT(NS_IsMainThread()); if (mKeys.IsNull()) { return; } RefPtr session(mKeys->GetPendingSession(aCreateSessionToken)); if (session) { session->SetSessionId(aSessionId); } } void GMPCDMProxy::OnResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) { MOZ_ASSERT(NS_IsMainThread()); if (mKeys.IsNull()) { return; } mKeys->OnSessionLoaded(aPromiseId, aSuccess); } void GMPCDMProxy::OnSessionMessage(const nsAString& aSessionId, dom::MediaKeyMessageType aMessageType, nsTArray& aMessage) { MOZ_ASSERT(NS_IsMainThread()); if (mKeys.IsNull()) { return; } RefPtr session(mKeys->GetSession(aSessionId)); if (session) { session->DispatchKeyMessage(aMessageType, aMessage); } } void GMPCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) { MOZ_ASSERT(NS_IsMainThread()); if (mKeys.IsNull()) { return; } RefPtr session(mKeys->GetSession(aSessionId)); if (session) { session->DispatchKeyStatusesChange(); } } void GMPCDMProxy::OnExpirationChange(const nsAString& aSessionId, GMPTimestamp aExpiryTime) { MOZ_ASSERT(NS_IsMainThread()); if (mKeys.IsNull()) { return; } RefPtr session(mKeys->GetSession(aSessionId)); if (session) { session->SetExpiration(static_cast(aExpiryTime)); } } void GMPCDMProxy::OnSessionClosed(const nsAString& aSessionId) { MOZ_ASSERT(NS_IsMainThread()); if (mKeys.IsNull()) { return; } RefPtr session(mKeys->GetSession(aSessionId)); if (session) { session->OnClosed(); } } void GMPCDMProxy::OnDecrypted(uint32_t aId, DecryptStatus aResult, const nsTArray& aDecryptedData) { MOZ_ASSERT(IsOnOwnerThread()); gmp_Decrypted(aId, aResult, aDecryptedData); } static void LogToConsole(const nsAString& aMsg) { nsCOMPtr console( do_GetService("@mozilla.org/consoleservice;1")); if (!console) { NS_WARNING("Failed to log message to console."); return; } nsAutoString msg(aMsg); console->LogStringMessage(msg.get()); } void GMPCDMProxy::OnSessionError(const nsAString& aSessionId, nsresult aException, uint32_t aSystemCode, const nsAString& aMsg) { MOZ_ASSERT(NS_IsMainThread()); if (mKeys.IsNull()) { return; } RefPtr session(mKeys->GetSession(aSessionId)); if (session) { session->DispatchKeyError(aSystemCode); } LogToConsole(aMsg); } void GMPCDMProxy::OnRejectPromise(uint32_t aPromiseId, nsresult aDOMException, const nsCString& aMsg) { MOZ_ASSERT(NS_IsMainThread()); RejectPromise(aPromiseId, aDOMException, aMsg); } const nsString& GMPCDMProxy::KeySystem() const { return mKeySystem; } CDMCaps& GMPCDMProxy::Capabilites() { return mCapabilites; } RefPtr GMPCDMProxy::Decrypt(MediaRawData* aSample) { RefPtr job(new DecryptJob(aSample)); RefPtr promise(job->Ensure()); nsCOMPtr task( NewRunnableMethod>(this, &GMPCDMProxy::gmp_Decrypt, job)); mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); return promise; } void GMPCDMProxy::gmp_Decrypt(RefPtr aJob) { MOZ_ASSERT(IsOnOwnerThread()); if (!mCDM) { aJob->PostResult(AbortedErr); return; } aJob->mId = ++mDecryptionJobCount; nsTArray data; data.AppendElements(aJob->mSample->Data(), aJob->mSample->Size()); mCDM->Decrypt(aJob->mId, aJob->mSample->mCrypto, data); mDecryptionJobs.AppendElement(aJob.forget()); } void GMPCDMProxy::gmp_Decrypted(uint32_t aId, DecryptStatus aResult, const nsTArray& aDecryptedData) { MOZ_ASSERT(IsOnOwnerThread()); #ifdef DEBUG bool jobIdFound = false; #endif for (size_t i = 0; i < mDecryptionJobs.Length(); i++) { DecryptJob* job = mDecryptionJobs[i]; if (job->mId == aId) { #ifdef DEBUG jobIdFound = true; #endif job->PostResult(aResult, aDecryptedData); mDecryptionJobs.RemoveElementAt(i); } } #ifdef DEBUG if (!jobIdFound) { NS_WARNING("GMPDecryptorChild returned incorrect job ID"); } #endif } void GMPCDMProxy::DecryptJob::PostResult(DecryptStatus aResult) { nsTArray empty; PostResult(aResult, empty); } void GMPCDMProxy::DecryptJob::PostResult(DecryptStatus aResult, const nsTArray& aDecryptedData) { if (aDecryptedData.Length() != mSample->Size()) { NS_WARNING("CDM returned incorrect number of decrypted bytes"); } if (aResult == Ok) { nsAutoPtr writer(mSample->CreateWriter()); PodCopy(writer->Data(), aDecryptedData.Elements(), std::min(aDecryptedData.Length(), mSample->Size())); } else if (aResult == NoKeyErr) { NS_WARNING("CDM returned NoKeyErr"); // We still have the encrypted sample, so we can re-enqueue it to be // decrypted again once the key is usable again. } else { nsAutoCString str("CDM returned decode failure DecryptStatus="); str.AppendInt(aResult); NS_WARNING(str.get()); } mPromise.Resolve(DecryptResult(aResult, mSample), __func__); } void GMPCDMProxy::GetSessionIdsForKeyId(const nsTArray& aKeyId, nsTArray& aSessionIds) { CDMCaps::AutoLock caps(Capabilites()); caps.GetSessionIdsForKeyId(aKeyId, aSessionIds); } void GMPCDMProxy::Terminated() { MOZ_ASSERT(NS_IsMainThread()); NS_WARNING("CDM terminated"); if (mCreatePromiseId) { RejectPromise(mCreatePromiseId, NS_ERROR_DOM_MEDIA_FATAL_ERR, NS_LITERAL_CSTRING("Crashed waiting for CDM to initialize")); mCreatePromiseId = 0; } if (!mKeys.IsNull()) { mKeys->Terminated(); } } uint32_t GMPCDMProxy::GetDecryptorId() { return mDecryptorId; } } // namespace mozilla