tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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