/* -*- 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 "mozilla/dom/PushManager.h" #include "mozilla/Base64.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/Unused.h" #include "mozilla/dom/PushManagerBinding.h" #include "mozilla/dom/PushSubscription.h" #include "mozilla/dom/PushSubscriptionOptionsBinding.h" #include "mozilla/dom/PushUtil.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/PromiseWorkerProxy.h" #include "nsIGlobalObject.h" #include "nsIPermissionManager.h" #include "nsIPrincipal.h" #include "nsIPushService.h" #include "nsComponentManagerUtils.h" #include "nsContentUtils.h" #include "WorkerRunnable.h" #include "WorkerPrivate.h" #include "WorkerScope.h" namespace mozilla { namespace dom { using namespace workers; namespace { nsresult GetPermissionState(nsIPrincipal* aPrincipal, PushPermissionState& aState) { nsCOMPtr permManager = mozilla::services::GetPermissionManager(); if (!permManager) { return NS_ERROR_FAILURE; } uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION; nsresult rv = permManager->TestExactPermissionFromPrincipal( aPrincipal, "desktop-notification", &permission); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (permission == nsIPermissionManager::ALLOW_ACTION || Preferences::GetBool("dom.push.testing.ignorePermission", false)) { aState = PushPermissionState::Granted; } else if (permission == nsIPermissionManager::DENY_ACTION) { aState = PushPermissionState::Denied; } else { aState = PushPermissionState::Prompt; } return NS_OK; } // A helper class that frees an `nsIPushSubscription` key buffer when it // goes out of scope. class MOZ_RAII AutoFreeKeyBuffer final { uint8_t** mKeyBuffer; public: explicit AutoFreeKeyBuffer(uint8_t** aKeyBuffer) : mKeyBuffer(aKeyBuffer) { MOZ_ASSERT(mKeyBuffer); } ~AutoFreeKeyBuffer() { NS_Free(*mKeyBuffer); } }; // Copies a subscription key buffer into an array. nsresult CopySubscriptionKeyToArray(nsIPushSubscription* aSubscription, const nsAString& aKeyName, nsTArray& aKey) { uint8_t* keyBuffer = nullptr; AutoFreeKeyBuffer autoFree(&keyBuffer); uint32_t keyLen; nsresult rv = aSubscription->GetKey(aKeyName, &keyLen, &keyBuffer); if (NS_FAILED(rv)) { return rv; } if (!aKey.SetCapacity(keyLen, fallible) || !aKey.InsertElementsAt(0, keyBuffer, keyLen, fallible)) { return NS_ERROR_OUT_OF_MEMORY; } return NS_OK; } nsresult GetSubscriptionParams(nsIPushSubscription* aSubscription, nsAString& aEndpoint, nsTArray& aRawP256dhKey, nsTArray& aAuthSecret, nsTArray& aAppServerKey) { if (!aSubscription) { return NS_OK; } nsresult rv = aSubscription->GetEndpoint(aEndpoint); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = CopySubscriptionKeyToArray(aSubscription, NS_LITERAL_STRING("p256dh"), aRawP256dhKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = CopySubscriptionKeyToArray(aSubscription, NS_LITERAL_STRING("auth"), aAuthSecret); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = CopySubscriptionKeyToArray(aSubscription, NS_LITERAL_STRING("appServer"), aAppServerKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } class GetSubscriptionResultRunnable final : public WorkerRunnable { public: GetSubscriptionResultRunnable(WorkerPrivate* aWorkerPrivate, already_AddRefed&& aProxy, nsresult aStatus, const nsAString& aEndpoint, const nsAString& aScope, nsTArray&& aRawP256dhKey, nsTArray&& aAuthSecret, nsTArray&& aAppServerKey) : WorkerRunnable(aWorkerPrivate) , mProxy(Move(aProxy)) , mStatus(aStatus) , mEndpoint(aEndpoint) , mScope(aScope) , mRawP256dhKey(Move(aRawP256dhKey)) , mAuthSecret(Move(aAuthSecret)) , mAppServerKey(Move(aAppServerKey)) { } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { RefPtr promise = mProxy->WorkerPromise(); if (NS_SUCCEEDED(mStatus)) { if (mEndpoint.IsEmpty()) { promise->MaybeResolve(JS::NullHandleValue); } else { RefPtr sub = new PushSubscription(nullptr, mEndpoint, mScope, Move(mRawP256dhKey), Move(mAuthSecret), Move(mAppServerKey)); promise->MaybeResolve(sub); } } else if (NS_ERROR_GET_MODULE(mStatus) == NS_ERROR_MODULE_DOM_PUSH ) { promise->MaybeReject(mStatus); } else { promise->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR); } mProxy->CleanUp(); return true; } private: ~GetSubscriptionResultRunnable() {} RefPtr mProxy; nsresult mStatus; nsString mEndpoint; nsString mScope; nsTArray mRawP256dhKey; nsTArray mAuthSecret; nsTArray mAppServerKey; }; class GetSubscriptionCallback final : public nsIPushSubscriptionCallback { public: NS_DECL_ISUPPORTS explicit GetSubscriptionCallback(PromiseWorkerProxy* aProxy, const nsAString& aScope) : mProxy(aProxy) , mScope(aScope) {} NS_IMETHOD OnPushSubscription(nsresult aStatus, nsIPushSubscription* aSubscription) override { AssertIsOnMainThread(); MOZ_ASSERT(mProxy, "OnPushSubscription() called twice?"); MutexAutoLock lock(mProxy->Lock()); if (mProxy->CleanedUp()) { return NS_OK; } nsAutoString endpoint; nsTArray rawP256dhKey, authSecret, appServerKey; if (NS_SUCCEEDED(aStatus)) { aStatus = GetSubscriptionParams(aSubscription, endpoint, rawP256dhKey, authSecret, appServerKey); } WorkerPrivate* worker = mProxy->GetWorkerPrivate(); RefPtr r = new GetSubscriptionResultRunnable(worker, mProxy.forget(), aStatus, endpoint, mScope, Move(rawP256dhKey), Move(authSecret), Move(appServerKey)); MOZ_ALWAYS_TRUE(r->Dispatch()); return NS_OK; } // Convenience method for use in this file. void OnPushSubscriptionError(nsresult aStatus) { Unused << NS_WARN_IF(NS_FAILED( OnPushSubscription(aStatus, nullptr))); } protected: ~GetSubscriptionCallback() {} private: RefPtr mProxy; nsString mScope; }; NS_IMPL_ISUPPORTS(GetSubscriptionCallback, nsIPushSubscriptionCallback) class GetSubscriptionRunnable final : public Runnable { public: GetSubscriptionRunnable(PromiseWorkerProxy* aProxy, const nsAString& aScope, PushManager::SubscriptionAction aAction, nsTArray&& aAppServerKey) : mProxy(aProxy) , mScope(aScope) , mAction(aAction) , mAppServerKey(Move(aAppServerKey)) {} NS_IMETHOD Run() override { AssertIsOnMainThread(); nsCOMPtr principal; { // Bug 1228723: If permission is revoked or an error occurs, the // subscription callback will be called synchronously. This causes // `GetSubscriptionCallback::OnPushSubscription` to deadlock when // it tries to acquire the lock. MutexAutoLock lock(mProxy->Lock()); if (mProxy->CleanedUp()) { return NS_OK; } principal = mProxy->GetWorkerPrivate()->GetPrincipal(); } MOZ_ASSERT(principal); RefPtr callback = new GetSubscriptionCallback(mProxy, mScope); PushPermissionState state; nsresult rv = GetPermissionState(principal, state); if (NS_FAILED(rv)) { callback->OnPushSubscriptionError(NS_ERROR_FAILURE); return NS_OK; } if (state != PushPermissionState::Granted) { if (mAction == PushManager::GetSubscriptionAction) { callback->OnPushSubscriptionError(NS_OK); return NS_OK; } callback->OnPushSubscriptionError(NS_ERROR_DOM_PUSH_DENIED_ERR); return NS_OK; } nsCOMPtr service = do_GetService("@mozilla.org/push/Service;1"); if (NS_WARN_IF(!service)) { callback->OnPushSubscriptionError(NS_ERROR_FAILURE); return NS_OK; } if (mAction == PushManager::SubscribeAction) { if (mAppServerKey.IsEmpty()) { rv = service->Subscribe(mScope, principal, callback); } else { rv = service->SubscribeWithKey(mScope, principal, mAppServerKey.Length(), mAppServerKey.Elements(), callback); } } else { MOZ_ASSERT(mAction == PushManager::GetSubscriptionAction); rv = service->GetSubscription(mScope, principal, callback); } if (NS_WARN_IF(NS_FAILED(rv))) { callback->OnPushSubscriptionError(NS_ERROR_FAILURE); return NS_OK; } return NS_OK; } private: ~GetSubscriptionRunnable() {} RefPtr mProxy; nsString mScope; PushManager::SubscriptionAction mAction; nsTArray mAppServerKey; }; class PermissionResultRunnable final : public WorkerRunnable { public: PermissionResultRunnable(PromiseWorkerProxy *aProxy, nsresult aStatus, PushPermissionState aState) : WorkerRunnable(aProxy->GetWorkerPrivate()) , mProxy(aProxy) , mStatus(aStatus) , mState(aState) { AssertIsOnMainThread(); } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); RefPtr promise = mProxy->WorkerPromise(); if (NS_SUCCEEDED(mStatus)) { promise->MaybeResolve(mState); } else { promise->MaybeReject(aCx, JS::UndefinedHandleValue); } mProxy->CleanUp(); return true; } private: ~PermissionResultRunnable() {} RefPtr mProxy; nsresult mStatus; PushPermissionState mState; }; class PermissionStateRunnable final : public Runnable { public: explicit PermissionStateRunnable(PromiseWorkerProxy* aProxy) : mProxy(aProxy) {} NS_IMETHOD Run() override { AssertIsOnMainThread(); MutexAutoLock lock(mProxy->Lock()); if (mProxy->CleanedUp()) { return NS_OK; } PushPermissionState state; nsresult rv = GetPermissionState( mProxy->GetWorkerPrivate()->GetPrincipal(), state ); RefPtr r = new PermissionResultRunnable(mProxy, rv, state); MOZ_ALWAYS_TRUE(r->Dispatch()); return NS_OK; } private: ~PermissionStateRunnable() {} RefPtr mProxy; }; } // anonymous namespace PushManager::PushManager(nsIGlobalObject* aGlobal, PushManagerImpl* aImpl) : mGlobal(aGlobal) , mImpl(aImpl) { AssertIsOnMainThread(); MOZ_ASSERT(aImpl); } PushManager::PushManager(const nsAString& aScope) : mScope(aScope) { #ifdef DEBUG // There's only one global on a worker, so we don't need to pass a global // object to the constructor. WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); worker->AssertIsOnWorkerThread(); #endif } PushManager::~PushManager() {} NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushManager, mGlobal, mImpl) NS_IMPL_CYCLE_COLLECTING_ADDREF(PushManager) NS_IMPL_CYCLE_COLLECTING_RELEASE(PushManager) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushManager) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END JSObject* PushManager::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return PushManagerBinding::Wrap(aCx, this, aGivenProto); } // static already_AddRefed PushManager::Constructor(GlobalObject& aGlobal, const nsAString& aScope, ErrorResult& aRv) { if (!NS_IsMainThread()) { RefPtr ret = new PushManager(aScope); return ret.forget(); } RefPtr impl = PushManagerImpl::Constructor(aGlobal, aGlobal.Context(), aScope, aRv); if (aRv.Failed()) { return nullptr; } nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); RefPtr ret = new PushManager(global, impl); return ret.forget(); } already_AddRefed PushManager::Subscribe(const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv) { if (mImpl) { MOZ_ASSERT(NS_IsMainThread()); return mImpl->Subscribe(aOptions, aRv); } return PerformSubscriptionActionFromWorker(SubscribeAction, aOptions, aRv); } already_AddRefed PushManager::GetSubscription(ErrorResult& aRv) { if (mImpl) { MOZ_ASSERT(NS_IsMainThread()); return mImpl->GetSubscription(aRv); } return PerformSubscriptionActionFromWorker(GetSubscriptionAction, aRv); } already_AddRefed PushManager::PermissionState(const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv) { if (mImpl) { MOZ_ASSERT(NS_IsMainThread()); return mImpl->PermissionState(aOptions, aRv); } WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); worker->AssertIsOnWorkerThread(); nsCOMPtr global = worker->GlobalScope(); RefPtr p = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr proxy = PromiseWorkerProxy::Create(worker, p); if (!proxy) { p->MaybeReject(worker->GetJSContext(), JS::UndefinedHandleValue); return p.forget(); } RefPtr r = new PermissionStateRunnable(proxy); NS_DispatchToMainThread(r); return p.forget(); } already_AddRefed PushManager::PerformSubscriptionActionFromWorker(SubscriptionAction aAction, ErrorResult& aRv) { PushSubscriptionOptionsInit options; return PerformSubscriptionActionFromWorker(aAction, options, aRv); } already_AddRefed PushManager::PerformSubscriptionActionFromWorker(SubscriptionAction aAction, const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv) { WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); worker->AssertIsOnWorkerThread(); nsCOMPtr global = worker->GlobalScope(); RefPtr p = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr proxy = PromiseWorkerProxy::Create(worker, p); if (!proxy) { p->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR); return p.forget(); } nsTArray appServerKey; if (!aOptions.mApplicationServerKey.IsNull()) { nsresult rv = NormalizeAppServerKey(aOptions.mApplicationServerKey.Value(), appServerKey); if (NS_FAILED(rv)) { p->MaybeReject(rv); return p.forget(); } } RefPtr r = new GetSubscriptionRunnable(proxy, mScope, aAction, Move(appServerKey)); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r)); return p.forget(); } nsresult PushManager::NormalizeAppServerKey(const OwningArrayBufferViewOrArrayBufferOrString& aSource, nsTArray& aAppServerKey) { if (aSource.IsString()) { NS_ConvertUTF16toUTF8 base64Key(aSource.GetAsString()); FallibleTArray decodedKey; nsresult rv = Base64URLDecode(base64Key, Base64URLDecodePaddingPolicy::Reject, decodedKey); if (NS_FAILED(rv)) { return NS_ERROR_DOM_INVALID_CHARACTER_ERR; } aAppServerKey = decodedKey; } else if (aSource.IsArrayBuffer()) { if (!PushUtil::CopyArrayBufferToArray(aSource.GetAsArrayBuffer(), aAppServerKey)) { return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR; } } else if (aSource.IsArrayBufferView()) { if (!PushUtil::CopyArrayBufferViewToArray(aSource.GetAsArrayBufferView(), aAppServerKey)) { return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR; } } else { MOZ_CRASH("Uninitialized union: expected string, buffer, or view"); } if (aAppServerKey.IsEmpty()) { return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR; } return NS_OK; } } // namespace dom } // namespace mozilla