summaryrefslogtreecommitdiffstats
path: root/dom/push/PushManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/push/PushManager.cpp')
-rw-r--r--dom/push/PushManager.cpp538
1 files changed, 538 insertions, 0 deletions
diff --git a/dom/push/PushManager.cpp b/dom/push/PushManager.cpp
new file mode 100644
index 0000000000..426f965f0f
--- /dev/null
+++ b/dom/push/PushManager.cpp
@@ -0,0 +1,538 @@
+/* -*- 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/Components.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/PermissionStatusBinding.h"
+#include "mozilla/dom/PushManagerBinding.h"
+#include "mozilla/dom/PushSubscription.h"
+#include "mozilla/dom/PushSubscriptionOptionsBinding.h"
+#include "mozilla/dom/PushUtil.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/ServiceWorker.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerScope.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 "nsServiceManagerUtils.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+nsresult GetPermissionState(nsIPrincipal* aPrincipal, PermissionState& aState) {
+ nsCOMPtr<nsIPermissionManager> permManager =
+ mozilla::components::PermissionManager::Service();
+
+ if (!permManager) {
+ return NS_ERROR_FAILURE;
+ }
+ uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
+ nsresult rv = permManager->TestExactPermissionFromPrincipal(
+ aPrincipal, "desktop-notification"_ns, &permission);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (permission == nsIPermissionManager::ALLOW_ACTION ||
+ Preferences::GetBool("dom.push.testing.ignorePermission", false)) {
+ aState = PermissionState::Granted;
+ } else if (permission == nsIPermissionManager::DENY_ACTION) {
+ aState = PermissionState::Denied;
+ } else {
+ aState = PermissionState::Prompt;
+ }
+
+ return NS_OK;
+}
+
+nsresult GetSubscriptionParams(nsIPushSubscription* aSubscription,
+ nsAString& aEndpoint,
+ nsTArray<uint8_t>& aRawP256dhKey,
+ nsTArray<uint8_t>& aAuthSecret,
+ nsTArray<uint8_t>& aAppServerKey) {
+ if (!aSubscription) {
+ return NS_OK;
+ }
+
+ nsresult rv = aSubscription->GetEndpoint(aEndpoint);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aSubscription->GetKey(u"p256dh"_ns, aRawP256dhKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = aSubscription->GetKey(u"auth"_ns, aAuthSecret);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = aSubscription->GetKey(u"appServer"_ns, aAppServerKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+class GetSubscriptionResultRunnable final : public WorkerRunnable {
+ public:
+ GetSubscriptionResultRunnable(WorkerPrivate* aWorkerPrivate,
+ RefPtr<PromiseWorkerProxy>&& aProxy,
+ nsresult aStatus, const nsAString& aEndpoint,
+ const nsAString& aScope,
+ Nullable<EpochTimeStamp>&& aExpirationTime,
+ nsTArray<uint8_t>&& aRawP256dhKey,
+ nsTArray<uint8_t>&& aAuthSecret,
+ nsTArray<uint8_t>&& aAppServerKey)
+ : WorkerRunnable(aWorkerPrivate),
+ mProxy(std::move(aProxy)),
+ mStatus(aStatus),
+ mEndpoint(aEndpoint),
+ mScope(aScope),
+ mExpirationTime(std::move(aExpirationTime)),
+ mRawP256dhKey(std::move(aRawP256dhKey)),
+ mAuthSecret(std::move(aAuthSecret)),
+ mAppServerKey(std::move(aAppServerKey)) {}
+
+ bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ RefPtr<Promise> promise = mProxy->WorkerPromise();
+ if (NS_SUCCEEDED(mStatus)) {
+ if (mEndpoint.IsEmpty()) {
+ promise->MaybeResolve(JS::NullHandleValue);
+ } else {
+ RefPtr<PushSubscription> sub = new PushSubscription(
+ nullptr, mEndpoint, mScope, std::move(mExpirationTime),
+ std::move(mRawP256dhKey), std::move(mAuthSecret),
+ std::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() = default;
+
+ RefPtr<PromiseWorkerProxy> mProxy;
+ nsresult mStatus;
+ nsString mEndpoint;
+ nsString mScope;
+ Nullable<EpochTimeStamp> mExpirationTime;
+ nsTArray<uint8_t> mRawP256dhKey;
+ nsTArray<uint8_t> mAuthSecret;
+ nsTArray<uint8_t> 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<uint8_t> rawP256dhKey, authSecret, appServerKey;
+ if (NS_SUCCEEDED(aStatus)) {
+ aStatus = GetSubscriptionParams(aSubscription, endpoint, rawP256dhKey,
+ authSecret, appServerKey);
+ }
+
+ WorkerPrivate* worker = mProxy->GetWorkerPrivate();
+ RefPtr<GetSubscriptionResultRunnable> r = new GetSubscriptionResultRunnable(
+ worker, std::move(mProxy), aStatus, endpoint, mScope,
+ std::move(mExpirationTime), std::move(rawP256dhKey),
+ std::move(authSecret), std::move(appServerKey));
+ if (!r->Dispatch()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ 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() = default;
+
+ private:
+ RefPtr<PromiseWorkerProxy> mProxy;
+ nsString mScope;
+ Nullable<EpochTimeStamp> mExpirationTime;
+};
+
+NS_IMPL_ISUPPORTS(GetSubscriptionCallback, nsIPushSubscriptionCallback)
+
+class GetSubscriptionRunnable final : public Runnable {
+ public:
+ GetSubscriptionRunnable(PromiseWorkerProxy* aProxy, const nsAString& aScope,
+ PushManager::SubscriptionAction aAction,
+ nsTArray<uint8_t>&& aAppServerKey)
+ : Runnable("dom::GetSubscriptionRunnable"),
+ mProxy(aProxy),
+ mScope(aScope),
+ mAction(aAction),
+ mAppServerKey(std::move(aAppServerKey)) {}
+
+ NS_IMETHOD
+ Run() override {
+ AssertIsOnMainThread();
+
+ nsCOMPtr<nsIPrincipal> 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<GetSubscriptionCallback> callback =
+ new GetSubscriptionCallback(mProxy, mScope);
+
+ PermissionState state;
+ nsresult rv = GetPermissionState(principal, state);
+ if (NS_FAILED(rv)) {
+ callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ if (state != PermissionState::Granted) {
+ if (mAction == PushManager::GetSubscriptionAction) {
+ callback->OnPushSubscriptionError(NS_OK);
+ return NS_OK;
+ }
+ callback->OnPushSubscriptionError(NS_ERROR_DOM_PUSH_DENIED_ERR);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPushService> 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,
+ 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() = default;
+
+ RefPtr<PromiseWorkerProxy> mProxy;
+ nsString mScope;
+ PushManager::SubscriptionAction mAction;
+ nsTArray<uint8_t> mAppServerKey;
+};
+
+class PermissionResultRunnable final : public WorkerRunnable {
+ public:
+ PermissionResultRunnable(PromiseWorkerProxy* aProxy, nsresult aStatus,
+ PermissionState 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> promise = mProxy->WorkerPromise();
+ if (NS_SUCCEEDED(mStatus)) {
+ promise->MaybeResolve(mState);
+ } else {
+ promise->MaybeRejectWithUndefined();
+ }
+
+ mProxy->CleanUp();
+
+ return true;
+ }
+
+ private:
+ ~PermissionResultRunnable() = default;
+
+ RefPtr<PromiseWorkerProxy> mProxy;
+ nsresult mStatus;
+ PermissionState mState;
+};
+
+class PermissionStateRunnable final : public Runnable {
+ public:
+ explicit PermissionStateRunnable(PromiseWorkerProxy* aProxy)
+ : Runnable("dom::PermissionStateRunnable"), mProxy(aProxy) {}
+
+ NS_IMETHOD
+ Run() override {
+ AssertIsOnMainThread();
+ MutexAutoLock lock(mProxy->Lock());
+ if (mProxy->CleanedUp()) {
+ return NS_OK;
+ }
+
+ PermissionState state;
+ nsresult rv =
+ GetPermissionState(mProxy->GetWorkerPrivate()->GetPrincipal(), state);
+
+ RefPtr<PermissionResultRunnable> r =
+ new PermissionResultRunnable(mProxy, rv, state);
+
+ // This can fail if the worker thread is already shutting down, but there's
+ // nothing we can do in that case.
+ Unused << NS_WARN_IF(!r->Dispatch());
+
+ return NS_OK;
+ }
+
+ private:
+ ~PermissionStateRunnable() = default;
+
+ RefPtr<PromiseWorkerProxy> 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() = default;
+
+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<JSObject*> aGivenProto) {
+ return PushManager_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// static
+already_AddRefed<PushManager> PushManager::Constructor(GlobalObject& aGlobal,
+ const nsAString& aScope,
+ ErrorResult& aRv) {
+ if (!NS_IsMainThread()) {
+ RefPtr<PushManager> ret = new PushManager(aScope);
+ return ret.forget();
+ }
+
+ RefPtr<PushManagerImpl> impl =
+ PushManagerImpl::Constructor(aGlobal, aGlobal.Context(), aScope, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<PushManager> ret = new PushManager(global, impl);
+
+ return ret.forget();
+}
+
+bool PushManager::IsEnabled(JSContext* aCx, JSObject* aGlobal) {
+ return StaticPrefs::dom_push_enabled() && ServiceWorkerVisible(aCx, aGlobal);
+}
+
+already_AddRefed<Promise> 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<Promise> PushManager::GetSubscription(ErrorResult& aRv) {
+ if (mImpl) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mImpl->GetSubscription(aRv);
+ }
+
+ return PerformSubscriptionActionFromWorker(GetSubscriptionAction, aRv);
+}
+
+already_AddRefed<Promise> 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<nsIGlobalObject> global = worker->GlobalScope();
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
+ if (!proxy) {
+ p->MaybeRejectWithUndefined();
+ return p.forget();
+ }
+
+ RefPtr<PermissionStateRunnable> r = new PermissionStateRunnable(proxy);
+ NS_DispatchToMainThread(r);
+
+ return p.forget();
+}
+
+already_AddRefed<Promise> PushManager::PerformSubscriptionActionFromWorker(
+ SubscriptionAction aAction, ErrorResult& aRv) {
+ RootedDictionary<PushSubscriptionOptionsInit> options(RootingCx());
+ return PerformSubscriptionActionFromWorker(aAction, options, aRv);
+}
+
+already_AddRefed<Promise> PushManager::PerformSubscriptionActionFromWorker(
+ SubscriptionAction aAction, const PushSubscriptionOptionsInit& aOptions,
+ ErrorResult& aRv) {
+ WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(worker);
+ worker->AssertIsOnWorkerThread();
+
+ nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope();
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
+ if (!proxy) {
+ p->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR);
+ return p.forget();
+ }
+
+ nsTArray<uint8_t> appServerKey;
+ if (!aOptions.mApplicationServerKey.IsNull()) {
+ nsresult rv = NormalizeAppServerKey(aOptions.mApplicationServerKey.Value(),
+ appServerKey);
+ if (NS_FAILED(rv)) {
+ p->MaybeReject(rv);
+ return p.forget();
+ }
+ }
+
+ RefPtr<GetSubscriptionRunnable> r = new GetSubscriptionRunnable(
+ proxy, mScope, aAction, std::move(appServerKey));
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r));
+
+ return p.forget();
+}
+
+nsresult PushManager::NormalizeAppServerKey(
+ const OwningArrayBufferViewOrArrayBufferOrString& aSource,
+ nsTArray<uint8_t>& aAppServerKey) {
+ if (aSource.IsString()) {
+ NS_ConvertUTF16toUTF8 base64Key(aSource.GetAsString());
+ FallibleTArray<uint8_t> 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 mozilla::dom