1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/dom/PushManager.h"
9 #include "mozilla/Base64.h"
10 #include "mozilla/Preferences.h"
11 #include "mozilla/Components.h"
12 #include "mozilla/Unused.h"
13 #include "mozilla/dom/PermissionStatusBinding.h"
14 #include "mozilla/dom/PushManagerBinding.h"
15 #include "mozilla/dom/PushSubscription.h"
16 #include "mozilla/dom/PushSubscriptionOptionsBinding.h"
17 #include "mozilla/dom/PushUtil.h"
18 #include "mozilla/dom/RootedDictionary.h"
19 #include "mozilla/dom/ServiceWorker.h"
20 #include "mozilla/dom/WorkerRunnable.h"
21 #include "mozilla/dom/WorkerScope.h"
23 #include "mozilla/dom/Promise.h"
24 #include "mozilla/dom/PromiseWorkerProxy.h"
26 #include "nsIGlobalObject.h"
27 #include "nsIPermissionManager.h"
28 #include "nsIPrincipal.h"
29 #include "nsIPushService.h"
31 #include "nsComponentManagerUtils.h"
32 #include "nsContentUtils.h"
33 #include "nsServiceManagerUtils.h"
35 namespace mozilla::dom
{
39 nsresult
GetPermissionState(nsIPrincipal
* aPrincipal
, PermissionState
& aState
) {
40 nsCOMPtr
<nsIPermissionManager
> permManager
=
41 mozilla::components::PermissionManager::Service();
44 return NS_ERROR_FAILURE
;
46 uint32_t permission
= nsIPermissionManager::UNKNOWN_ACTION
;
47 nsresult rv
= permManager
->TestExactPermissionFromPrincipal(
48 aPrincipal
, "desktop-notification"_ns
, &permission
);
49 if (NS_WARN_IF(NS_FAILED(rv
))) {
53 if (permission
== nsIPermissionManager::ALLOW_ACTION
||
54 Preferences::GetBool("dom.push.testing.ignorePermission", false)) {
55 aState
= PermissionState::Granted
;
56 } else if (permission
== nsIPermissionManager::DENY_ACTION
) {
57 aState
= PermissionState::Denied
;
59 aState
= PermissionState::Prompt
;
65 nsresult
GetSubscriptionParams(nsIPushSubscription
* aSubscription
,
67 nsTArray
<uint8_t>& aRawP256dhKey
,
68 nsTArray
<uint8_t>& aAuthSecret
,
69 nsTArray
<uint8_t>& aAppServerKey
) {
74 nsresult rv
= aSubscription
->GetEndpoint(aEndpoint
);
75 if (NS_WARN_IF(NS_FAILED(rv
))) {
79 rv
= aSubscription
->GetKey(u
"p256dh"_ns
, aRawP256dhKey
);
80 if (NS_WARN_IF(NS_FAILED(rv
))) {
83 rv
= aSubscription
->GetKey(u
"auth"_ns
, aAuthSecret
);
84 if (NS_WARN_IF(NS_FAILED(rv
))) {
87 rv
= aSubscription
->GetKey(u
"appServer"_ns
, aAppServerKey
);
88 if (NS_WARN_IF(NS_FAILED(rv
))) {
95 class GetSubscriptionResultRunnable final
: public WorkerRunnable
{
97 GetSubscriptionResultRunnable(WorkerPrivate
* aWorkerPrivate
,
98 RefPtr
<PromiseWorkerProxy
>&& aProxy
,
99 nsresult aStatus
, const nsAString
& aEndpoint
,
100 const nsAString
& aScope
,
101 Nullable
<EpochTimeStamp
>&& aExpirationTime
,
102 nsTArray
<uint8_t>&& aRawP256dhKey
,
103 nsTArray
<uint8_t>&& aAuthSecret
,
104 nsTArray
<uint8_t>&& aAppServerKey
)
105 : WorkerRunnable(aWorkerPrivate
, "GetSubscriptionResultRunnable"),
106 mProxy(std::move(aProxy
)),
108 mEndpoint(aEndpoint
),
110 mExpirationTime(std::move(aExpirationTime
)),
111 mRawP256dhKey(std::move(aRawP256dhKey
)),
112 mAuthSecret(std::move(aAuthSecret
)),
113 mAppServerKey(std::move(aAppServerKey
)) {}
115 bool WorkerRun(JSContext
* aCx
, WorkerPrivate
* aWorkerPrivate
) override
{
116 RefPtr
<Promise
> promise
= mProxy
->GetWorkerPromise();
117 // Once Worker had already started shutdown, workerPromise would be nullptr
121 if (NS_SUCCEEDED(mStatus
)) {
122 if (mEndpoint
.IsEmpty()) {
123 promise
->MaybeResolve(JS::NullHandleValue
);
125 RefPtr
<PushSubscription
> sub
= new PushSubscription(
126 nullptr, mEndpoint
, mScope
, std::move(mExpirationTime
),
127 std::move(mRawP256dhKey
), std::move(mAuthSecret
),
128 std::move(mAppServerKey
));
129 promise
->MaybeResolve(sub
);
131 } else if (NS_ERROR_GET_MODULE(mStatus
) == NS_ERROR_MODULE_DOM_PUSH
) {
132 promise
->MaybeReject(mStatus
);
134 promise
->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR
);
143 ~GetSubscriptionResultRunnable() = default;
145 RefPtr
<PromiseWorkerProxy
> mProxy
;
149 Nullable
<EpochTimeStamp
> mExpirationTime
;
150 nsTArray
<uint8_t> mRawP256dhKey
;
151 nsTArray
<uint8_t> mAuthSecret
;
152 nsTArray
<uint8_t> mAppServerKey
;
155 class GetSubscriptionCallback final
: public nsIPushSubscriptionCallback
{
159 explicit GetSubscriptionCallback(PromiseWorkerProxy
* aProxy
,
160 const nsAString
& aScope
)
161 : mProxy(aProxy
), mScope(aScope
) {}
164 OnPushSubscription(nsresult aStatus
,
165 nsIPushSubscription
* aSubscription
) override
{
166 AssertIsOnMainThread();
167 MOZ_ASSERT(mProxy
, "OnPushSubscription() called twice?");
169 MutexAutoLock
lock(mProxy
->Lock());
170 if (mProxy
->CleanedUp()) {
174 nsAutoString endpoint
;
175 nsTArray
<uint8_t> rawP256dhKey
, authSecret
, appServerKey
;
176 if (NS_SUCCEEDED(aStatus
)) {
177 aStatus
= GetSubscriptionParams(aSubscription
, endpoint
, rawP256dhKey
,
178 authSecret
, appServerKey
);
181 WorkerPrivate
* worker
= mProxy
->GetWorkerPrivate();
182 RefPtr
<GetSubscriptionResultRunnable
> r
= new GetSubscriptionResultRunnable(
183 worker
, std::move(mProxy
), aStatus
, endpoint
, mScope
,
184 std::move(mExpirationTime
), std::move(rawP256dhKey
),
185 std::move(authSecret
), std::move(appServerKey
));
186 if (!r
->Dispatch()) {
187 return NS_ERROR_UNEXPECTED
;
193 // Convenience method for use in this file.
194 void OnPushSubscriptionError(nsresult aStatus
) {
195 Unused
<< NS_WARN_IF(NS_FAILED(OnPushSubscription(aStatus
, nullptr)));
199 ~GetSubscriptionCallback() = default;
202 RefPtr
<PromiseWorkerProxy
> mProxy
;
204 Nullable
<EpochTimeStamp
> mExpirationTime
;
207 NS_IMPL_ISUPPORTS(GetSubscriptionCallback
, nsIPushSubscriptionCallback
)
209 class GetSubscriptionRunnable final
: public Runnable
{
211 GetSubscriptionRunnable(PromiseWorkerProxy
* aProxy
, const nsAString
& aScope
,
212 PushManager::SubscriptionAction aAction
,
213 nsTArray
<uint8_t>&& aAppServerKey
)
214 : Runnable("dom::GetSubscriptionRunnable"),
218 mAppServerKey(std::move(aAppServerKey
)) {}
222 AssertIsOnMainThread();
224 nsCOMPtr
<nsIPrincipal
> principal
;
227 // Bug 1228723: If permission is revoked or an error occurs, the
228 // subscription callback will be called synchronously. This causes
229 // `GetSubscriptionCallback::OnPushSubscription` to deadlock when
230 // it tries to acquire the lock.
231 MutexAutoLock
lock(mProxy
->Lock());
232 if (mProxy
->CleanedUp()) {
235 principal
= mProxy
->GetWorkerPrivate()->GetPrincipal();
238 MOZ_ASSERT(principal
);
240 RefPtr
<GetSubscriptionCallback
> callback
=
241 new GetSubscriptionCallback(mProxy
, mScope
);
243 PermissionState state
;
244 nsresult rv
= GetPermissionState(principal
, state
);
246 callback
->OnPushSubscriptionError(NS_ERROR_FAILURE
);
250 if (state
!= PermissionState::Granted
) {
251 if (mAction
== PushManager::GetSubscriptionAction
) {
252 callback
->OnPushSubscriptionError(NS_OK
);
255 callback
->OnPushSubscriptionError(NS_ERROR_DOM_PUSH_DENIED_ERR
);
259 nsCOMPtr
<nsIPushService
> service
=
260 do_GetService("@mozilla.org/push/Service;1");
261 if (NS_WARN_IF(!service
)) {
262 callback
->OnPushSubscriptionError(NS_ERROR_FAILURE
);
266 if (mAction
== PushManager::SubscribeAction
) {
267 if (mAppServerKey
.IsEmpty()) {
268 rv
= service
->Subscribe(mScope
, principal
, callback
);
270 rv
= service
->SubscribeWithKey(mScope
, principal
, mAppServerKey
,
274 MOZ_ASSERT(mAction
== PushManager::GetSubscriptionAction
);
275 rv
= service
->GetSubscription(mScope
, principal
, callback
);
278 if (NS_WARN_IF(NS_FAILED(rv
))) {
279 callback
->OnPushSubscriptionError(NS_ERROR_FAILURE
);
287 ~GetSubscriptionRunnable() = default;
289 RefPtr
<PromiseWorkerProxy
> mProxy
;
291 PushManager::SubscriptionAction mAction
;
292 nsTArray
<uint8_t> mAppServerKey
;
295 class PermissionResultRunnable final
: public WorkerRunnable
{
297 PermissionResultRunnable(PromiseWorkerProxy
* aProxy
, nsresult aStatus
,
298 PermissionState aState
)
299 : WorkerRunnable(aProxy
->GetWorkerPrivate(), "PermissionResultRunnable"),
303 AssertIsOnMainThread();
306 bool WorkerRun(JSContext
* aCx
, WorkerPrivate
* aWorkerPrivate
) override
{
307 MOZ_ASSERT(aWorkerPrivate
);
308 aWorkerPrivate
->AssertIsOnWorkerThread();
309 RefPtr
<Promise
> promise
= mProxy
->GetWorkerPromise();
313 if (NS_SUCCEEDED(mStatus
)) {
314 promise
->MaybeResolve(mState
);
316 promise
->MaybeRejectWithUndefined();
325 ~PermissionResultRunnable() = default;
327 RefPtr
<PromiseWorkerProxy
> mProxy
;
329 PermissionState mState
;
332 class PermissionStateRunnable final
: public Runnable
{
334 explicit PermissionStateRunnable(PromiseWorkerProxy
* aProxy
)
335 : Runnable("dom::PermissionStateRunnable"), mProxy(aProxy
) {}
339 AssertIsOnMainThread();
340 MutexAutoLock
lock(mProxy
->Lock());
341 if (mProxy
->CleanedUp()) {
345 PermissionState state
;
347 GetPermissionState(mProxy
->GetWorkerPrivate()->GetPrincipal(), state
);
349 RefPtr
<PermissionResultRunnable
> r
=
350 new PermissionResultRunnable(mProxy
, rv
, state
);
352 // This can fail if the worker thread is already shutting down, but there's
353 // nothing we can do in that case.
354 Unused
<< NS_WARN_IF(!r
->Dispatch());
360 ~PermissionStateRunnable() = default;
362 RefPtr
<PromiseWorkerProxy
> mProxy
;
365 } // anonymous namespace
367 PushManager::PushManager(nsIGlobalObject
* aGlobal
, PushManagerImpl
* aImpl
)
368 : mGlobal(aGlobal
), mImpl(aImpl
) {
369 AssertIsOnMainThread();
373 PushManager::PushManager(const nsAString
& aScope
) : mScope(aScope
) {
375 // There's only one global on a worker, so we don't need to pass a global
376 // object to the constructor.
377 WorkerPrivate
* worker
= GetCurrentThreadWorkerPrivate();
379 worker
->AssertIsOnWorkerThread();
383 PushManager::~PushManager() = default;
385 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushManager
, mGlobal
, mImpl
)
386 NS_IMPL_CYCLE_COLLECTING_ADDREF(PushManager
)
387 NS_IMPL_CYCLE_COLLECTING_RELEASE(PushManager
)
388 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushManager
)
389 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
390 NS_INTERFACE_MAP_ENTRY(nsISupports
)
393 JSObject
* PushManager::WrapObject(JSContext
* aCx
,
394 JS::Handle
<JSObject
*> aGivenProto
) {
395 return PushManager_Binding::Wrap(aCx
, this, aGivenProto
);
399 already_AddRefed
<PushManager
> PushManager::Constructor(GlobalObject
& aGlobal
,
400 const nsAString
& aScope
,
402 if (!NS_IsMainThread()) {
403 RefPtr
<PushManager
> ret
= new PushManager(aScope
);
407 RefPtr
<PushManagerImpl
> impl
=
408 PushManagerImpl::Constructor(aGlobal
, aGlobal
.Context(), aScope
, aRv
);
413 nsCOMPtr
<nsIGlobalObject
> global
= do_QueryInterface(aGlobal
.GetAsSupports());
414 RefPtr
<PushManager
> ret
= new PushManager(global
, impl
);
419 bool PushManager::IsEnabled(JSContext
* aCx
, JSObject
* aGlobal
) {
420 return StaticPrefs::dom_push_enabled() && ServiceWorkerVisible(aCx
, aGlobal
);
423 already_AddRefed
<Promise
> PushManager::Subscribe(
424 const PushSubscriptionOptionsInit
& aOptions
, ErrorResult
& aRv
) {
426 MOZ_ASSERT(NS_IsMainThread());
427 return mImpl
->Subscribe(aOptions
, aRv
);
430 return PerformSubscriptionActionFromWorker(SubscribeAction
, aOptions
, aRv
);
433 already_AddRefed
<Promise
> PushManager::GetSubscription(ErrorResult
& aRv
) {
435 MOZ_ASSERT(NS_IsMainThread());
436 return mImpl
->GetSubscription(aRv
);
439 return PerformSubscriptionActionFromWorker(GetSubscriptionAction
, aRv
);
442 already_AddRefed
<Promise
> PushManager::PermissionState(
443 const PushSubscriptionOptionsInit
& aOptions
, ErrorResult
& aRv
) {
445 MOZ_ASSERT(NS_IsMainThread());
446 return mImpl
->PermissionState(aOptions
, aRv
);
449 WorkerPrivate
* worker
= GetCurrentThreadWorkerPrivate();
451 worker
->AssertIsOnWorkerThread();
453 nsCOMPtr
<nsIGlobalObject
> global
= worker
->GlobalScope();
454 RefPtr
<Promise
> p
= Promise::Create(global
, aRv
);
455 if (NS_WARN_IF(aRv
.Failed())) {
459 RefPtr
<PromiseWorkerProxy
> proxy
= PromiseWorkerProxy::Create(worker
, p
);
461 p
->MaybeRejectWithUndefined();
465 RefPtr
<PermissionStateRunnable
> r
= new PermissionStateRunnable(proxy
);
466 NS_DispatchToMainThread(r
);
471 already_AddRefed
<Promise
> PushManager::PerformSubscriptionActionFromWorker(
472 SubscriptionAction aAction
, ErrorResult
& aRv
) {
473 RootedDictionary
<PushSubscriptionOptionsInit
> options(RootingCx());
474 return PerformSubscriptionActionFromWorker(aAction
, options
, aRv
);
477 already_AddRefed
<Promise
> PushManager::PerformSubscriptionActionFromWorker(
478 SubscriptionAction aAction
, const PushSubscriptionOptionsInit
& aOptions
,
480 WorkerPrivate
* worker
= GetCurrentThreadWorkerPrivate();
482 worker
->AssertIsOnWorkerThread();
484 nsCOMPtr
<nsIGlobalObject
> global
= worker
->GlobalScope();
485 RefPtr
<Promise
> p
= Promise::Create(global
, aRv
);
486 if (NS_WARN_IF(aRv
.Failed())) {
490 RefPtr
<PromiseWorkerProxy
> proxy
= PromiseWorkerProxy::Create(worker
, p
);
492 p
->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR
);
496 nsTArray
<uint8_t> appServerKey
;
497 if (!aOptions
.mApplicationServerKey
.IsNull()) {
498 nsresult rv
= NormalizeAppServerKey(aOptions
.mApplicationServerKey
.Value(),
506 RefPtr
<GetSubscriptionRunnable
> r
= new GetSubscriptionRunnable(
507 proxy
, mScope
, aAction
, std::move(appServerKey
));
508 MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r
));
513 nsresult
PushManager::NormalizeAppServerKey(
514 const OwningArrayBufferViewOrArrayBufferOrString
& aSource
,
515 nsTArray
<uint8_t>& aAppServerKey
) {
516 if (aSource
.IsString()) {
517 NS_ConvertUTF16toUTF8
base64Key(aSource
.GetAsString());
518 FallibleTArray
<uint8_t> decodedKey
;
519 nsresult rv
= Base64URLDecode(
520 base64Key
, Base64URLDecodePaddingPolicy::Reject
, decodedKey
);
522 return NS_ERROR_DOM_INVALID_CHARACTER_ERR
;
524 aAppServerKey
= decodedKey
;
526 if (!AppendTypedArrayDataTo(aSource
, aAppServerKey
)) {
527 return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR
;
530 if (aAppServerKey
.IsEmpty()) {
531 return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR
;
536 } // namespace mozilla::dom