PushManager.cpp (17715B)
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/. */ 6 7 #include "mozilla/dom/PushManager.h" 8 9 #include "mozilla/Base64.h" 10 #include "mozilla/Components.h" 11 #include "mozilla/Preferences.h" 12 #include "mozilla/dom/PermissionStatusBinding.h" 13 #include "mozilla/dom/Promise.h" 14 #include "mozilla/dom/PromiseWorkerProxy.h" 15 #include "mozilla/dom/PushManagerBinding.h" 16 #include "mozilla/dom/PushSubscription.h" 17 #include "mozilla/dom/PushSubscriptionOptionsBinding.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" 22 #include "nsComponentManagerUtils.h" 23 #include "nsContentUtils.h" 24 #include "nsIGlobalObject.h" 25 #include "nsIPermissionManager.h" 26 #include "nsIPrincipal.h" 27 #include "nsIPushService.h" 28 #include "nsServiceManagerUtils.h" 29 30 namespace mozilla::dom { 31 32 nsresult GetSubscriptionParams(nsIPushSubscription* aSubscription, 33 nsAString& aEndpoint, 34 nsTArray<uint8_t>& aRawP256dhKey, 35 nsTArray<uint8_t>& aAuthSecret, 36 nsTArray<uint8_t>& aAppServerKey) { 37 if (!aSubscription) { 38 return NS_OK; 39 } 40 41 nsresult rv = aSubscription->GetEndpoint(aEndpoint); 42 if (NS_WARN_IF(NS_FAILED(rv))) { 43 return rv; 44 } 45 46 rv = aSubscription->GetKey(u"p256dh"_ns, aRawP256dhKey); 47 if (NS_WARN_IF(NS_FAILED(rv))) { 48 return rv; 49 } 50 rv = aSubscription->GetKey(u"auth"_ns, aAuthSecret); 51 if (NS_WARN_IF(NS_FAILED(rv))) { 52 return rv; 53 } 54 rv = aSubscription->GetKey(u"appServer"_ns, aAppServerKey); 55 if (NS_WARN_IF(NS_FAILED(rv))) { 56 return rv; 57 } 58 59 return NS_OK; 60 } 61 62 namespace { 63 64 nsresult GetPermissionState(nsIPrincipal* aPrincipal, PermissionState& aState) { 65 nsCOMPtr<nsIPermissionManager> permManager = 66 mozilla::components::PermissionManager::Service(); 67 68 if (!permManager) { 69 return NS_ERROR_FAILURE; 70 } 71 uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION; 72 nsresult rv = permManager->TestExactPermissionFromPrincipal( 73 aPrincipal, "desktop-notification"_ns, &permission); 74 if (NS_WARN_IF(NS_FAILED(rv))) { 75 return rv; 76 } 77 78 if (permission == nsIPermissionManager::ALLOW_ACTION || 79 Preferences::GetBool("dom.push.testing.ignorePermission", false)) { 80 aState = PermissionState::Granted; 81 } else if (permission == nsIPermissionManager::DENY_ACTION) { 82 aState = PermissionState::Denied; 83 } else { 84 aState = PermissionState::Prompt; 85 } 86 87 return NS_OK; 88 } 89 90 class GetSubscriptionResultRunnable final : public WorkerThreadRunnable { 91 public: 92 GetSubscriptionResultRunnable(WorkerPrivate* aWorkerPrivate, 93 RefPtr<PromiseWorkerProxy>&& aProxy, 94 nsresult aStatus, const nsAString& aEndpoint, 95 const nsAString& aScope, 96 Nullable<EpochTimeStamp>&& aExpirationTime, 97 nsTArray<uint8_t>&& aRawP256dhKey, 98 nsTArray<uint8_t>&& aAuthSecret, 99 nsTArray<uint8_t>&& aAppServerKey) 100 : WorkerThreadRunnable("GetSubscriptionResultRunnable"), 101 mProxy(std::move(aProxy)), 102 mStatus(aStatus), 103 mEndpoint(aEndpoint), 104 mScope(aScope), 105 mExpirationTime(std::move(aExpirationTime)), 106 mRawP256dhKey(std::move(aRawP256dhKey)), 107 mAuthSecret(std::move(aAuthSecret)), 108 mAppServerKey(std::move(aAppServerKey)) {} 109 110 bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { 111 RefPtr<Promise> promise = mProxy->GetWorkerPromise(); 112 // Once Worker had already started shutdown, workerPromise would be nullptr 113 if (!promise) { 114 return true; 115 } 116 if (NS_SUCCEEDED(mStatus)) { 117 if (mEndpoint.IsEmpty()) { 118 promise->MaybeResolve(JS::NullHandleValue); 119 } else { 120 RefPtr<PushSubscription> sub = new PushSubscription( 121 nullptr, mEndpoint, mScope, std::move(mExpirationTime), 122 std::move(mRawP256dhKey), std::move(mAuthSecret), 123 std::move(mAppServerKey)); 124 promise->MaybeResolve(sub); 125 } 126 } else if (NS_ERROR_GET_MODULE(mStatus) == NS_ERROR_MODULE_DOM_PUSH) { 127 promise->MaybeReject(mStatus); 128 } else { 129 promise->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR); 130 } 131 132 mProxy->CleanUp(); 133 134 return true; 135 } 136 137 private: 138 ~GetSubscriptionResultRunnable() = default; 139 140 RefPtr<PromiseWorkerProxy> mProxy; 141 nsresult mStatus; 142 nsString mEndpoint; 143 nsString mScope; 144 Nullable<EpochTimeStamp> mExpirationTime; 145 nsTArray<uint8_t> mRawP256dhKey; 146 nsTArray<uint8_t> mAuthSecret; 147 nsTArray<uint8_t> mAppServerKey; 148 }; 149 150 class GetSubscriptionCallback final : public nsIPushSubscriptionCallback { 151 public: 152 NS_DECL_ISUPPORTS 153 154 explicit GetSubscriptionCallback(PromiseWorkerProxy* aProxy, 155 const nsAString& aScope) 156 : mProxy(aProxy), mScope(aScope) {} 157 158 NS_IMETHOD 159 OnPushSubscription(nsresult aStatus, 160 nsIPushSubscription* aSubscription) override { 161 AssertIsOnMainThread(); 162 MOZ_ASSERT(mProxy, "OnPushSubscription() called twice?"); 163 164 MutexAutoLock lock(mProxy->Lock()); 165 if (mProxy->CleanedUp()) { 166 return NS_OK; 167 } 168 169 nsAutoString endpoint; 170 nsTArray<uint8_t> rawP256dhKey, authSecret, appServerKey; 171 if (NS_SUCCEEDED(aStatus)) { 172 aStatus = GetSubscriptionParams(aSubscription, endpoint, rawP256dhKey, 173 authSecret, appServerKey); 174 } 175 176 WorkerPrivate* worker = mProxy->GetWorkerPrivate(); 177 RefPtr<GetSubscriptionResultRunnable> r = new GetSubscriptionResultRunnable( 178 worker, std::move(mProxy), aStatus, endpoint, mScope, 179 std::move(mExpirationTime), std::move(rawP256dhKey), 180 std::move(authSecret), std::move(appServerKey)); 181 if (!r->Dispatch(worker)) { 182 return NS_ERROR_UNEXPECTED; 183 } 184 185 return NS_OK; 186 } 187 188 // Convenience method for use in this file. 189 void OnPushSubscriptionError(nsresult aStatus) { 190 (void)NS_WARN_IF(NS_FAILED(OnPushSubscription(aStatus, nullptr))); 191 } 192 193 protected: 194 ~GetSubscriptionCallback() = default; 195 196 private: 197 RefPtr<PromiseWorkerProxy> mProxy; 198 nsString mScope; 199 Nullable<EpochTimeStamp> mExpirationTime; 200 }; 201 202 NS_IMPL_ISUPPORTS(GetSubscriptionCallback, nsIPushSubscriptionCallback) 203 204 class GetSubscriptionRunnable final : public Runnable { 205 public: 206 GetSubscriptionRunnable(PromiseWorkerProxy* aProxy, const nsAString& aScope, 207 PushManager::SubscriptionAction aAction, 208 nsTArray<uint8_t>&& aAppServerKey) 209 : Runnable("dom::GetSubscriptionRunnable"), 210 mProxy(aProxy), 211 mScope(aScope), 212 mAction(aAction), 213 mAppServerKey(std::move(aAppServerKey)) {} 214 215 NS_IMETHOD 216 Run() override { 217 AssertIsOnMainThread(); 218 219 nsCOMPtr<nsIPrincipal> principal; 220 221 { 222 // Bug 1228723: If permission is revoked or an error occurs, the 223 // subscription callback will be called synchronously. This causes 224 // `GetSubscriptionCallback::OnPushSubscription` to deadlock when 225 // it tries to acquire the lock. 226 MutexAutoLock lock(mProxy->Lock()); 227 if (mProxy->CleanedUp()) { 228 return NS_OK; 229 } 230 principal = mProxy->GetWorkerPrivate()->GetPrincipal(); 231 } 232 233 MOZ_ASSERT(principal); 234 235 RefPtr<GetSubscriptionCallback> callback = 236 new GetSubscriptionCallback(mProxy, mScope); 237 238 PermissionState state; 239 nsresult rv = GetPermissionState(principal, state); 240 if (NS_FAILED(rv)) { 241 callback->OnPushSubscriptionError(NS_ERROR_FAILURE); 242 return NS_OK; 243 } 244 245 if (state != PermissionState::Granted) { 246 if (mAction == PushManager::GetSubscriptionAction) { 247 callback->OnPushSubscriptionError(NS_OK); 248 return NS_OK; 249 } 250 callback->OnPushSubscriptionError(NS_ERROR_DOM_PUSH_DENIED_ERR); 251 return NS_OK; 252 } 253 254 nsCOMPtr<nsIPushService> service = 255 do_GetService("@mozilla.org/push/Service;1"); 256 if (NS_WARN_IF(!service)) { 257 callback->OnPushSubscriptionError(NS_ERROR_FAILURE); 258 return NS_OK; 259 } 260 261 if (mAction == PushManager::SubscribeAction) { 262 if (mAppServerKey.IsEmpty()) { 263 rv = service->Subscribe(mScope, principal, callback); 264 } else { 265 rv = service->SubscribeWithKey(mScope, principal, mAppServerKey, 266 callback); 267 } 268 } else { 269 MOZ_ASSERT(mAction == PushManager::GetSubscriptionAction); 270 rv = service->GetSubscription(mScope, principal, callback); 271 } 272 273 if (NS_WARN_IF(NS_FAILED(rv))) { 274 callback->OnPushSubscriptionError(NS_ERROR_FAILURE); 275 return NS_OK; 276 } 277 278 return NS_OK; 279 } 280 281 private: 282 ~GetSubscriptionRunnable() = default; 283 284 RefPtr<PromiseWorkerProxy> mProxy; 285 nsString mScope; 286 PushManager::SubscriptionAction mAction; 287 nsTArray<uint8_t> mAppServerKey; 288 }; 289 290 class PermissionResultRunnable final : public WorkerThreadRunnable { 291 public: 292 PermissionResultRunnable(PromiseWorkerProxy* aProxy, nsresult aStatus, 293 PermissionState aState) 294 : WorkerThreadRunnable("PermissionResultRunnable"), 295 mProxy(aProxy), 296 mStatus(aStatus), 297 mState(aState) { 298 AssertIsOnMainThread(); 299 } 300 301 bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { 302 MOZ_ASSERT(aWorkerPrivate); 303 aWorkerPrivate->AssertIsOnWorkerThread(); 304 RefPtr<Promise> promise = mProxy->GetWorkerPromise(); 305 if (!promise) { 306 return true; 307 } 308 if (NS_SUCCEEDED(mStatus)) { 309 promise->MaybeResolve(mState); 310 } else { 311 promise->MaybeRejectWithUndefined(); 312 } 313 314 mProxy->CleanUp(); 315 316 return true; 317 } 318 319 private: 320 ~PermissionResultRunnable() = default; 321 322 RefPtr<PromiseWorkerProxy> mProxy; 323 nsresult mStatus; 324 PermissionState mState; 325 }; 326 327 class PermissionStateRunnable final : public Runnable { 328 public: 329 explicit PermissionStateRunnable(PromiseWorkerProxy* aProxy) 330 : Runnable("dom::PermissionStateRunnable"), mProxy(aProxy) {} 331 332 NS_IMETHOD 333 Run() override { 334 AssertIsOnMainThread(); 335 MutexAutoLock lock(mProxy->Lock()); 336 if (mProxy->CleanedUp()) { 337 return NS_OK; 338 } 339 340 PermissionState state; 341 nsresult rv = 342 GetPermissionState(mProxy->GetWorkerPrivate()->GetPrincipal(), state); 343 344 RefPtr<PermissionResultRunnable> r = 345 new PermissionResultRunnable(mProxy, rv, state); 346 347 // This can fail if the worker thread is already shutting down, but there's 348 // nothing we can do in that case. 349 (void)NS_WARN_IF(!r->Dispatch(mProxy->GetWorkerPrivate())); 350 351 return NS_OK; 352 } 353 354 private: 355 ~PermissionStateRunnable() = default; 356 357 RefPtr<PromiseWorkerProxy> mProxy; 358 }; 359 360 } // anonymous namespace 361 362 PushManager::PushManager(nsIGlobalObject* aGlobal, PushManagerImpl* aImpl) 363 : mGlobal(aGlobal), mImpl(aImpl) { 364 AssertIsOnMainThread(); 365 MOZ_ASSERT(aImpl); 366 } 367 368 PushManager::PushManager(const nsAString& aScope) : mScope(aScope) { 369 #ifdef DEBUG 370 // There's only one global on a worker, so we don't need to pass a global 371 // object to the constructor. 372 WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); 373 MOZ_ASSERT(worker); 374 worker->AssertIsOnWorkerThread(); 375 #endif 376 } 377 378 PushManager::~PushManager() = default; 379 380 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushManager, mGlobal, mImpl) 381 NS_IMPL_CYCLE_COLLECTING_ADDREF(PushManager) 382 NS_IMPL_CYCLE_COLLECTING_RELEASE(PushManager) 383 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushManager) 384 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 385 NS_INTERFACE_MAP_ENTRY(nsISupports) 386 NS_INTERFACE_MAP_END 387 388 JSObject* PushManager::WrapObject(JSContext* aCx, 389 JS::Handle<JSObject*> aGivenProto) { 390 return PushManager_Binding::Wrap(aCx, this, aGivenProto); 391 } 392 393 // static 394 already_AddRefed<PushManager> PushManager::Constructor(GlobalObject& aGlobal, 395 const nsAString& aScope, 396 ErrorResult& aRv) { 397 if (!NS_IsMainThread()) { 398 RefPtr<PushManager> ret = new PushManager(aScope); 399 return ret.forget(); 400 } 401 402 RefPtr<PushManagerImpl> impl = 403 PushManagerImpl::Constructor(aGlobal, aGlobal.Context(), aScope, aRv); 404 if (aRv.Failed()) { 405 return nullptr; 406 } 407 408 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); 409 RefPtr<PushManager> ret = new PushManager(global, impl); 410 411 return ret.forget(); 412 } 413 414 bool PushManager::IsEnabled(JSContext* aCx, JSObject* aGlobal) { 415 return StaticPrefs::dom_push_enabled() && ServiceWorkersEnabled(aCx, aGlobal); 416 } 417 418 // https://w3c.github.io/push-api/#dom-pushmanager-supportedcontentencodings 419 void PushManager::GetSupportedContentEncodings( 420 GlobalObject& aGlobal, JS::MutableHandle<JSObject*> aEncodings, 421 ErrorResult& aRv) { 422 JSContext* cx = aGlobal.Context(); 423 424 nsTArray<nsString> encodings{u"aes128gcm"_ns}; 425 if (StaticPrefs::dom_push_indicate_aesgcm_support_enabled()) { 426 // The spec does not define orders, but Chrome is returning ["aes128gcm", 427 // "aesgcm"] and there are a bunch of code like below, which is copypasted 428 // from Minishlink/web-push-php-example endorsed by 429 // web-push-libs/web-push-php. Which means practically the preferred 430 // algorithm should go to the first place. 431 // 432 // (PushManager.supportedContentEncodings || ['aesgcm'])[0]; 433 encodings.AppendElement(u"aesgcm"_ns); 434 } 435 436 JS::Rooted<JS::Value> encodingsValue(cx); 437 if (!ToJSValue(cx, encodings, &encodingsValue)) { 438 if (JS_IsThrowingOutOfMemory(cx)) { 439 MOZ_CRASH("Out of memory"); 440 } else { 441 aRv.NoteJSContextException(cx); 442 return; 443 } 444 }; 445 JS::Rooted<JSObject*> object(cx, &encodingsValue.toObject()); 446 if (!JS_FreezeObject(cx, object)) { 447 aRv.NoteJSContextException(cx); 448 return; 449 } 450 aEncodings.set(object); 451 } 452 453 already_AddRefed<Promise> PushManager::Subscribe( 454 const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv) { 455 if (mImpl) { 456 MOZ_ASSERT(NS_IsMainThread()); 457 return mImpl->Subscribe(aOptions, aRv); 458 } 459 460 return PerformSubscriptionActionFromWorker(SubscribeAction, aOptions, aRv); 461 } 462 463 already_AddRefed<Promise> PushManager::GetSubscription(ErrorResult& aRv) { 464 if (mImpl) { 465 MOZ_ASSERT(NS_IsMainThread()); 466 return mImpl->GetSubscription(aRv); 467 } 468 469 return PerformSubscriptionActionFromWorker(GetSubscriptionAction, aRv); 470 } 471 472 already_AddRefed<Promise> PushManager::PermissionState( 473 const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv) { 474 if (mImpl) { 475 MOZ_ASSERT(NS_IsMainThread()); 476 return mImpl->PermissionState(aOptions, aRv); 477 } 478 479 WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); 480 MOZ_ASSERT(worker); 481 worker->AssertIsOnWorkerThread(); 482 483 nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope(); 484 RefPtr<Promise> p = Promise::Create(global, aRv); 485 if (NS_WARN_IF(aRv.Failed())) { 486 return nullptr; 487 } 488 489 RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p); 490 if (!proxy) { 491 p->MaybeRejectWithUndefined(); 492 return p.forget(); 493 } 494 495 RefPtr<PermissionStateRunnable> r = new PermissionStateRunnable(proxy); 496 NS_DispatchToMainThread(r); 497 498 return p.forget(); 499 } 500 501 already_AddRefed<Promise> PushManager::PerformSubscriptionActionFromWorker( 502 SubscriptionAction aAction, ErrorResult& aRv) { 503 RootedDictionary<PushSubscriptionOptionsInit> options(RootingCx()); 504 return PerformSubscriptionActionFromWorker(aAction, options, aRv); 505 } 506 507 already_AddRefed<Promise> PushManager::PerformSubscriptionActionFromWorker( 508 SubscriptionAction aAction, const PushSubscriptionOptionsInit& aOptions, 509 ErrorResult& aRv) { 510 WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); 511 MOZ_ASSERT(worker); 512 worker->AssertIsOnWorkerThread(); 513 514 nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope(); 515 RefPtr<Promise> p = Promise::Create(global, aRv); 516 if (NS_WARN_IF(aRv.Failed())) { 517 return nullptr; 518 } 519 520 RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p); 521 if (!proxy) { 522 p->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR); 523 return p.forget(); 524 } 525 526 nsTArray<uint8_t> appServerKey; 527 if (!aOptions.mApplicationServerKey.IsNull()) { 528 nsresult rv = NormalizeAppServerKey(aOptions.mApplicationServerKey.Value(), 529 appServerKey); 530 if (NS_FAILED(rv)) { 531 p->MaybeReject(rv); 532 return p.forget(); 533 } 534 } 535 536 RefPtr<GetSubscriptionRunnable> r = new GetSubscriptionRunnable( 537 proxy, mScope, aAction, std::move(appServerKey)); 538 MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r)); 539 540 return p.forget(); 541 } 542 543 nsresult PushManager::NormalizeAppServerKey( 544 const OwningArrayBufferViewOrArrayBufferOrString& aSource, 545 nsTArray<uint8_t>& aAppServerKey) { 546 if (aSource.IsString()) { 547 NS_ConvertUTF16toUTF8 base64Key(aSource.GetAsString()); 548 FallibleTArray<uint8_t> decodedKey; 549 nsresult rv = Base64URLDecode( 550 base64Key, Base64URLDecodePaddingPolicy::Reject, decodedKey); 551 if (NS_FAILED(rv)) { 552 return NS_ERROR_DOM_INVALID_CHARACTER_ERR; 553 } 554 aAppServerKey = decodedKey; 555 } else { 556 if (!AppendTypedArrayDataTo(aSource, aAppServerKey)) { 557 return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR; 558 } 559 } 560 if (aAppServerKey.IsEmpty()) { 561 return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR; 562 } 563 return NS_OK; 564 } 565 566 } // namespace mozilla::dom