/* -*- 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/PushSubscription.h" #include "nsGlobalWindowInner.h" #include "nsIPushService.h" #include "nsIScriptObjectPrincipal.h" #include "nsServiceManagerUtils.h" #include "mozilla/Base64.h" #include "mozilla/Unused.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/PromiseWorkerProxy.h" #include "mozilla/dom/PushSubscriptionOptions.h" #include "mozilla/dom/PushUtil.h" #include "mozilla/dom/WorkerCommon.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WorkerRunnable.h" #include "mozilla/dom/WorkerScope.h" namespace mozilla::dom { namespace { class UnsubscribeResultCallback final : public nsIUnsubscribeResultCallback { public: NS_DECL_ISUPPORTS explicit UnsubscribeResultCallback(Promise* aPromise) : mPromise(aPromise) { AssertIsOnMainThread(); } NS_IMETHOD OnUnsubscribe(nsresult aStatus, bool aSuccess) override { if (NS_SUCCEEDED(aStatus)) { mPromise->MaybeResolve(aSuccess); } else { mPromise->MaybeReject(NS_ERROR_DOM_PUSH_SERVICE_UNREACHABLE); } return NS_OK; } private: ~UnsubscribeResultCallback() = default; RefPtr<Promise> mPromise; }; NS_IMPL_ISUPPORTS(UnsubscribeResultCallback, nsIUnsubscribeResultCallback) class UnsubscribeResultRunnable final : public WorkerRunnable { public: UnsubscribeResultRunnable(WorkerPrivate* aWorkerPrivate, RefPtr<PromiseWorkerProxy>&& aProxy, nsresult aStatus, bool aSuccess) : WorkerRunnable(aWorkerPrivate, "UnsubscribeResultRunnable"), mProxy(std::move(aProxy)), mStatus(aStatus), mSuccess(aSuccess) { AssertIsOnMainThread(); } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); RefPtr<Promise> promise = mProxy->GetWorkerPromise(); // Once Worker had already started shutdown, workerPromise would be nullptr if (!promise) { return true; } if (NS_SUCCEEDED(mStatus)) { promise->MaybeResolve(mSuccess); } else { promise->MaybeReject(NS_ERROR_DOM_PUSH_SERVICE_UNREACHABLE); } mProxy->CleanUp(); return true; } private: ~UnsubscribeResultRunnable() = default; RefPtr<PromiseWorkerProxy> mProxy; nsresult mStatus; bool mSuccess; }; class WorkerUnsubscribeResultCallback final : public nsIUnsubscribeResultCallback { public: NS_DECL_ISUPPORTS explicit WorkerUnsubscribeResultCallback(PromiseWorkerProxy* aProxy) : mProxy(aProxy) { AssertIsOnMainThread(); } NS_IMETHOD OnUnsubscribe(nsresult aStatus, bool aSuccess) override { AssertIsOnMainThread(); MOZ_ASSERT(mProxy, "OnUnsubscribe() called twice?"); MutexAutoLock lock(mProxy->Lock()); if (mProxy->CleanedUp()) { return NS_OK; } WorkerPrivate* worker = mProxy->GetWorkerPrivate(); RefPtr<UnsubscribeResultRunnable> r = new UnsubscribeResultRunnable( worker, std::move(mProxy), aStatus, aSuccess); MOZ_ALWAYS_TRUE(r->Dispatch()); return NS_OK; } private: ~WorkerUnsubscribeResultCallback() = default; RefPtr<PromiseWorkerProxy> mProxy; }; NS_IMPL_ISUPPORTS(WorkerUnsubscribeResultCallback, nsIUnsubscribeResultCallback) class UnsubscribeRunnable final : public Runnable { public: UnsubscribeRunnable(PromiseWorkerProxy* aProxy, const nsAString& aScope) : Runnable("dom::UnsubscribeRunnable"), mProxy(aProxy), mScope(aScope) { MOZ_ASSERT(aProxy); MOZ_ASSERT(!aScope.IsEmpty()); } NS_IMETHOD Run() override { AssertIsOnMainThread(); nsCOMPtr<nsIPrincipal> principal; { MutexAutoLock lock(mProxy->Lock()); if (mProxy->CleanedUp()) { return NS_OK; } principal = mProxy->GetWorkerPrivate()->GetPrincipal(); } MOZ_ASSERT(principal); RefPtr<WorkerUnsubscribeResultCallback> callback = new WorkerUnsubscribeResultCallback(mProxy); nsCOMPtr<nsIPushService> service = do_GetService("@mozilla.org/push/Service;1"); if (NS_WARN_IF(!service)) { callback->OnUnsubscribe(NS_ERROR_FAILURE, false); return NS_OK; } if (NS_WARN_IF( NS_FAILED(service->Unsubscribe(mScope, principal, callback)))) { callback->OnUnsubscribe(NS_ERROR_FAILURE, false); return NS_OK; } return NS_OK; } private: ~UnsubscribeRunnable() = default; RefPtr<PromiseWorkerProxy> mProxy; nsString mScope; }; } // anonymous namespace PushSubscription::PushSubscription(nsIGlobalObject* aGlobal, const nsAString& aEndpoint, const nsAString& aScope, Nullable<EpochTimeStamp>&& aExpirationTime, nsTArray<uint8_t>&& aRawP256dhKey, nsTArray<uint8_t>&& aAuthSecret, nsTArray<uint8_t>&& aAppServerKey) : mEndpoint(aEndpoint), mScope(aScope), mExpirationTime(std::move(aExpirationTime)), mRawP256dhKey(std::move(aRawP256dhKey)), mAuthSecret(std::move(aAuthSecret)) { if (NS_IsMainThread()) { mGlobal = aGlobal; } else { #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 } mOptions = new PushSubscriptionOptions(mGlobal, std::move(aAppServerKey)); } PushSubscription::~PushSubscription() = default; NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushSubscription, mGlobal, mOptions) NS_IMPL_CYCLE_COLLECTING_ADDREF(PushSubscription) NS_IMPL_CYCLE_COLLECTING_RELEASE(PushSubscription) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushSubscription) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END JSObject* PushSubscription::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { return PushSubscription_Binding::Wrap(aCx, this, aGivenProto); } // static already_AddRefed<PushSubscription> PushSubscription::Constructor( GlobalObject& aGlobal, const PushSubscriptionInit& aInitDict, ErrorResult& aRv) { nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); nsTArray<uint8_t> rawKey; if (aInitDict.mP256dhKey.WasPassed() && !aInitDict.mP256dhKey.Value().IsNull() && !aInitDict.mP256dhKey.Value().Value().AppendDataTo(rawKey)) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } nsTArray<uint8_t> authSecret; if (aInitDict.mAuthSecret.WasPassed() && !aInitDict.mAuthSecret.Value().IsNull() && !aInitDict.mAuthSecret.Value().Value().AppendDataTo(authSecret)) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } nsTArray<uint8_t> appServerKey; if (aInitDict.mAppServerKey.WasPassed() && !aInitDict.mAppServerKey.Value().IsNull()) { const OwningArrayBufferViewOrArrayBuffer& bufferSource = aInitDict.mAppServerKey.Value().Value(); if (!PushUtil::CopyBufferSourceToArray(bufferSource, appServerKey)) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } } Nullable<EpochTimeStamp> expirationTime; if (aInitDict.mExpirationTime.IsNull()) { expirationTime.SetNull(); } else { expirationTime.SetValue(aInitDict.mExpirationTime.Value()); } RefPtr<PushSubscription> sub = new PushSubscription( global, aInitDict.mEndpoint, aInitDict.mScope, std::move(expirationTime), std::move(rawKey), std::move(authSecret), std::move(appServerKey)); return sub.forget(); } already_AddRefed<Promise> PushSubscription::Unsubscribe(ErrorResult& aRv) { if (!NS_IsMainThread()) { RefPtr<Promise> p = UnsubscribeFromWorker(aRv); return p.forget(); } MOZ_ASSERT(mGlobal); nsCOMPtr<nsIPushService> service = do_GetService("@mozilla.org/push/Service;1"); if (NS_WARN_IF(!service)) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal); if (!window) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } RefPtr<Promise> p = Promise::Create(mGlobal, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr<UnsubscribeResultCallback> callback = new UnsubscribeResultCallback(p); Unused << NS_WARN_IF(NS_FAILED(service->Unsubscribe( mScope, nsGlobalWindowInner::Cast(window)->GetClientPrincipal(), callback))); return p.forget(); } void PushSubscription::GetKey(JSContext* aCx, PushEncryptionKeyName aType, JS::MutableHandle<JSObject*> aKey, ErrorResult& aRv) { if (aType == PushEncryptionKeyName::P256dh) { PushUtil::CopyArrayToArrayBuffer(aCx, mRawP256dhKey, aKey, aRv); } else if (aType == PushEncryptionKeyName::Auth) { PushUtil::CopyArrayToArrayBuffer(aCx, mAuthSecret, aKey, aRv); } else { aKey.set(nullptr); } } void PushSubscription::ToJSON(PushSubscriptionJSON& aJSON, ErrorResult& aRv) { aJSON.mEndpoint.Construct(); aJSON.mEndpoint.Value() = mEndpoint; aJSON.mKeys.mP256dh.Construct(); nsresult rv = Base64URLEncode( mRawP256dhKey.Length(), mRawP256dhKey.Elements(), Base64URLEncodePaddingPolicy::Omit, aJSON.mKeys.mP256dh.Value()); if (NS_WARN_IF(NS_FAILED(rv))) { aRv.Throw(rv); return; } aJSON.mKeys.mAuth.Construct(); rv = Base64URLEncode(mAuthSecret.Length(), mAuthSecret.Elements(), Base64URLEncodePaddingPolicy::Omit, aJSON.mKeys.mAuth.Value()); if (NS_WARN_IF(NS_FAILED(rv))) { aRv.Throw(rv); return; } aJSON.mExpirationTime.Construct(mExpirationTime); } already_AddRefed<PushSubscriptionOptions> PushSubscription::Options() { RefPtr<PushSubscriptionOptions> options = mOptions; return options.forget(); } already_AddRefed<Promise> PushSubscription::UnsubscribeFromWorker( 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_SERVICE_UNREACHABLE); return p.forget(); } RefPtr<UnsubscribeRunnable> r = new UnsubscribeRunnable(proxy, mScope); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r)); return p.forget(); } } // namespace mozilla::dom