diff options
Diffstat (limited to 'dom/serviceworkers/ServiceWorkerContainer.cpp')
-rw-r--r-- | dom/serviceworkers/ServiceWorkerContainer.cpp | 893 |
1 files changed, 893 insertions, 0 deletions
diff --git a/dom/serviceworkers/ServiceWorkerContainer.cpp b/dom/serviceworkers/ServiceWorkerContainer.cpp new file mode 100644 index 0000000000..4e33b9fc75 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerContainer.cpp @@ -0,0 +1,893 @@ +/* -*- 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 "ServiceWorkerContainer.h" + +#include "nsContentPolicyUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "mozilla/dom/Document.h" +#include "nsIServiceWorkerManager.h" +#include "nsIScriptError.h" +#include "nsThreadUtils.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "mozilla/Components.h" +#include "mozilla/StaticPrefs_dom.h" + +#include "nsCycleCollectionParticipant.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/dom/ClientIPCTypes.h" +#include "mozilla/dom/DOMMozPromiseRequestHolder.h" +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ServiceWorker.h" +#include "mozilla/dom/ServiceWorkerContainerBinding.h" +#include "mozilla/dom/ServiceWorkerContainerChild.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/dom/ipc/StructuredCloneData.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" + +#include "ServiceWorker.h" +#include "ServiceWorkerRegistration.h" +#include "ServiceWorkerUtils.h" + +// This is defined to something else on Windows +#ifdef DispatchMessage +# undef DispatchMessage +#endif + +namespace mozilla::dom { + +using mozilla::ipc::BackgroundChild; +using mozilla::ipc::PBackgroundChild; +using mozilla::ipc::ResponseRejectReason; + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerContainer) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper, + mControllerWorker, mReadyPromise) + +// static +already_AddRefed<ServiceWorkerContainer> ServiceWorkerContainer::Create( + nsIGlobalObject* aGlobal) { + RefPtr<ServiceWorkerContainer> ref = new ServiceWorkerContainer(aGlobal); + return ref.forget(); +} + +ServiceWorkerContainer::ServiceWorkerContainer(nsIGlobalObject* aGlobal) + : DOMEventTargetHelper(aGlobal), mShutdown(false) { + PBackgroundChild* parentActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!parentActor)) { + Shutdown(); + return; + } + + RefPtr<ServiceWorkerContainerChild> actor = + ServiceWorkerContainerChild::Create(); + if (NS_WARN_IF(!actor)) { + Shutdown(); + return; + } + + PServiceWorkerContainerChild* sentActor = + parentActor->SendPServiceWorkerContainerConstructor(actor); + if (NS_WARN_IF(!sentActor)) { + Shutdown(); + return; + } + MOZ_DIAGNOSTIC_ASSERT(sentActor == actor); + + mActor = std::move(actor); + mActor->SetOwner(this); + + Maybe<ServiceWorkerDescriptor> controller = aGlobal->GetController(); + if (controller.isSome()) { + mControllerWorker = aGlobal->GetOrCreateServiceWorker(controller.ref()); + } +} + +ServiceWorkerContainer::~ServiceWorkerContainer() { Shutdown(); } + +void ServiceWorkerContainer::DisconnectFromOwner() { + mControllerWorker = nullptr; + mReadyPromise = nullptr; + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void ServiceWorkerContainer::ControllerChanged(ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> go = GetParentObject(); + if (!go) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + mControllerWorker = go->GetOrCreateServiceWorker(go->GetController().ref()); + aRv = DispatchTrustedEvent(u"controllerchange"_ns); +} + +using mozilla::dom::ipc::StructuredCloneData; + +// A ReceivedMessage represents a message sent via +// Client.postMessage(). It is used as used both for queuing of +// incoming messages and as an interface to DispatchMessage(). +struct MOZ_HEAP_CLASS ServiceWorkerContainer::ReceivedMessage { + explicit ReceivedMessage(const ClientPostMessageArgs& aArgs) + : mServiceWorker(aArgs.serviceWorker()) { + mClonedData.CopyFromClonedMessageData(aArgs.clonedData()); + } + + ServiceWorkerDescriptor mServiceWorker; + StructuredCloneData mClonedData; + + NS_INLINE_DECL_REFCOUNTING(ReceivedMessage) + + private: + ~ReceivedMessage() = default; +}; + +void ServiceWorkerContainer::ReceiveMessage( + const ClientPostMessageArgs& aArgs) { + RefPtr<ReceivedMessage> message = new ReceivedMessage(aArgs); + if (mMessagesStarted) { + EnqueueReceivedMessageDispatch(std::move(message)); + } else { + mPendingMessages.AppendElement(message.forget()); + } +} + +void ServiceWorkerContainer::RevokeActor(ServiceWorkerContainerChild* aActor) { + MOZ_DIAGNOSTIC_ASSERT(mActor); + MOZ_DIAGNOSTIC_ASSERT(mActor == aActor); + mActor->RevokeOwner(this); + mActor = nullptr; + + mShutdown = true; +} + +JSObject* ServiceWorkerContainer::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return ServiceWorkerContainer_Binding::Wrap(aCx, this, aGivenProto); +} + +namespace { + +already_AddRefed<nsIURI> GetBaseURIFromGlobal(nsIGlobalObject* aGlobal, + ErrorResult& aRv) { + // It would be nice not to require a window here, but right + // now we don't have a great way to get the base URL just + // from the nsIGlobalObject. + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); + if (!window) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + Document* doc = window->GetExtantDoc(); + if (!doc) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + nsCOMPtr<nsIURI> baseURI = doc->GetDocBaseURI(); + if (!baseURI) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + return baseURI.forget(); +} + +} // anonymous namespace + +already_AddRefed<Promise> ServiceWorkerContainer::Register( + const nsAString& aScriptURL, const RegistrationOptions& aOptions, + const CallerType aCallerType, ErrorResult& aRv) { + // Note, we can't use GetGlobalIfValid() from the start here. If we + // hit a storage failure we want to log a message with the final + // scope string we put together below. + nsIGlobalObject* global = GetParentObject(); + if (!global) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + Maybe<ClientInfo> clientInfo = global->GetClientInfo(); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + nsCOMPtr<nsIURI> baseURI = GetBaseURIFromGlobal(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. + nsAutoCString scriptURL; + if (!AppendUTF16toUTF8(aScriptURL, scriptURL, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + nsCOMPtr<nsIURI> scriptURI; + nsresult rv = + NS_NewURI(getter_AddRefs(scriptURI), scriptURL, nullptr, baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowTypeError<MSG_INVALID_URL>(scriptURL); + return nullptr; + } + + // Never allow script URL with moz-extension scheme if support is fully + // disabled by the 'extensions.background_service_worker.enabled' pref. + if (scriptURI->SchemeIs("moz-extension") && + !StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + // In ServiceWorkerContainer.register() the scope argument is parsed against + // different base URLs depending on whether it was passed or not. + nsCOMPtr<nsIURI> scopeURI; + + // Step 4. If none passed, parse against script's URL + if (!aOptions.mScope.WasPassed()) { + constexpr auto defaultScope = "./"_ns; + rv = NS_NewURI(getter_AddRefs(scopeURI), defaultScope, nullptr, scriptURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsAutoCString spec; + scriptURI->GetSpec(spec); + aRv.ThrowTypeError<MSG_INVALID_SCOPE>(defaultScope, spec); + return nullptr; + } + } else { + // Step 5. Parse against entry settings object's base URL. + rv = NS_NewURI(getter_AddRefs(scopeURI), aOptions.mScope.Value(), nullptr, + baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsIURI* uri = baseURI ? baseURI : scriptURI; + nsAutoCString spec; + uri->GetSpec(spec); + aRv.ThrowTypeError<MSG_INVALID_SCOPE>( + NS_ConvertUTF16toUTF8(aOptions.mScope.Value()), spec); + return nullptr; + } + } + + // Strip the any ref from both the script and scope URLs. + nsCOMPtr<nsIURI> cloneWithoutRef; + aRv = NS_GetURIWithoutRef(scriptURI, getter_AddRefs(cloneWithoutRef)); + if (aRv.Failed()) { + return nullptr; + } + scriptURI = std::move(cloneWithoutRef); + + aRv = NS_GetURIWithoutRef(scopeURI, getter_AddRefs(cloneWithoutRef)); + if (aRv.Failed()) { + return nullptr; + } + scopeURI = std::move(cloneWithoutRef); + + ServiceWorkerScopeAndScriptAreValid(clientInfo.ref(), scopeURI, scriptURI, + aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global); + if (!window) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + Document* doc = window->GetExtantDoc(); + if (!doc) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + // The next section of code executes an NS_CheckContentLoadPolicy() + // check. This is necessary to enforce the CSP of the calling client. + // Currently this requires an Document. Once bug 965637 lands we + // should try to move this into ServiceWorkerScopeAndScriptAreValid() + // using the ClientInfo instead of doing a window-specific check here. + // See bug 1455077 for further investigation. + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new mozilla::net::LoadInfo( + doc->NodePrincipal(), // loading principal + doc->NodePrincipal(), // triggering principal + doc, // loading node + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER); + + // Check content policy. + int16_t decision = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(scriptURI, secCheckLoadInfo, + "application/javascript"_ns, &decision); + if (NS_FAILED(rv)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + if (NS_WARN_IF(decision != nsIContentPolicy::ACCEPT)) { + aRv.Throw(NS_ERROR_CONTENT_BLOCKED); + return nullptr; + } + + // Get the string representation for both the script and scope since + // we sanitized them above. + nsCString cleanedScopeURL; + aRv = scopeURI->GetSpec(cleanedScopeURL); + if (aRv.Failed()) { + return nullptr; + } + + nsCString cleanedScriptURL; + aRv = scriptURI->GetSpec(cleanedScriptURL); + if (aRv.Failed()) { + return nullptr; + } + + // Verify that the global is valid and has permission to store + // data. We perform this late so that we can report the final + // scope URL in any error message. + Unused << GetGlobalIfValid(aRv, [&](Document* aDoc) { + AutoTArray<nsString, 1> param; + CopyUTF8toUTF16(cleanedScopeURL, *param.AppendElement()); + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + "Service Workers"_ns, aDoc, + nsContentUtils::eDOM_PROPERTIES, + "ServiceWorkerRegisterStorageError", param); + }); + + window->NoteCalledRegisterForServiceWorkerScope(cleanedScopeURL); + + RefPtr<Promise> outer = + Promise::Create(global, aRv, Promise::ePropagateUserInteraction); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<ServiceWorkerContainer> self = this; + + if (!mActor) { + aRv.ThrowInvalidStateError("Can't register service worker"); + return nullptr; + } + + mActor->SendRegister( + clientInfo.ref().ToIPC(), nsCString(cleanedScopeURL), + nsCString(cleanedScriptURL), aOptions.mUpdateViaCache, + [self, + outer](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult& + aResult) { + if (aResult.type() == + IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult:: + TCopyableErrorResult) { + // application layer error + CopyableErrorResult rv = aResult.get_CopyableErrorResult(); + MOZ_DIAGNOSTIC_ASSERT(rv.Failed()); + outer->MaybeReject(std::move(rv)); + return; + } + // success + const auto& ipcDesc = + aResult.get_IPCServiceWorkerRegistrationDescriptor(); + ErrorResult rv; + nsIGlobalObject* global = self->GetGlobalIfValid(rv); + if (rv.Failed()) { + outer->MaybeReject(std::move(rv)); + return; + } + RefPtr<ServiceWorkerRegistration> reg = + global->GetOrCreateServiceWorkerRegistration( + ServiceWorkerRegistrationDescriptor(ipcDesc)); + outer->MaybeResolve(reg); + }, + [outer](ResponseRejectReason&& aReason) { + // IPC layer error + CopyableErrorResult rv; + rv.ThrowInvalidStateError("Failed to register service worker"); + outer->MaybeReject(std::move(rv)); + }); + + return outer.forget(); +} + +already_AddRefed<ServiceWorker> ServiceWorkerContainer::GetController() { + RefPtr<ServiceWorker> ref = mControllerWorker; + return ref.forget(); +} + +already_AddRefed<Promise> ServiceWorkerContainer::GetRegistrations( + ErrorResult& aRv) { + nsIGlobalObject* global = GetGlobalIfValid(aRv, [](Document* aDoc) { + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + "Service Workers"_ns, aDoc, + nsContentUtils::eDOM_PROPERTIES, + "ServiceWorkerGetRegistrationStorageError"); + }); + if (aRv.Failed()) { + return nullptr; + } + + Maybe<ClientInfo> clientInfo = global->GetClientInfo(); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + RefPtr<Promise> outer = + Promise::Create(global, aRv, Promise::ePropagateUserInteraction); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<ServiceWorkerContainer> self = this; + + if (!mActor) { + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return outer.forget(); + } + + mActor->SendGetRegistrations( + clientInfo.ref().ToIPC(), + [self, outer]( + const IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult& + aResult) { + if (aResult.type() == + IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult:: + TCopyableErrorResult) { + // application layer error + const auto& rv = aResult.get_CopyableErrorResult(); + MOZ_DIAGNOSTIC_ASSERT(rv.Failed()); + outer->MaybeReject(CopyableErrorResult(rv)); + return; + } + // success + const auto& ipcList = + aResult.get_IPCServiceWorkerRegistrationDescriptorList(); + nsTArray<ServiceWorkerRegistrationDescriptor> list( + ipcList.values().Length()); + for (const auto& ipcDesc : ipcList.values()) { + list.AppendElement(ServiceWorkerRegistrationDescriptor(ipcDesc)); + } + + ErrorResult rv; + nsIGlobalObject* global = self->GetGlobalIfValid(rv); + if (rv.Failed()) { + outer->MaybeReject(std::move(rv)); + return; + } + nsTArray<RefPtr<ServiceWorkerRegistration>> regList; + for (auto& desc : list) { + RefPtr<ServiceWorkerRegistration> reg = + global->GetOrCreateServiceWorkerRegistration(desc); + if (reg) { + regList.AppendElement(std::move(reg)); + } + } + outer->MaybeResolve(regList); + }, + [outer](ResponseRejectReason&& aReason) { + // IPC layer error + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + }); + + return outer.forget(); +} + +void ServiceWorkerContainer::StartMessages() { + while (!mPendingMessages.IsEmpty()) { + EnqueueReceivedMessageDispatch(mPendingMessages.ElementAt(0)); + mPendingMessages.RemoveElementAt(0); + } + mMessagesStarted = true; +} + +already_AddRefed<Promise> ServiceWorkerContainer::GetRegistration( + const nsAString& aURL, ErrorResult& aRv) { + nsIGlobalObject* global = GetGlobalIfValid(aRv, [](Document* aDoc) { + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + "Service Workers"_ns, aDoc, + nsContentUtils::eDOM_PROPERTIES, + "ServiceWorkerGetRegistrationStorageError"); + }); + if (aRv.Failed()) { + return nullptr; + } + + Maybe<ClientInfo> clientInfo = global->GetClientInfo(); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + nsCOMPtr<nsIURI> baseURI = GetBaseURIFromGlobal(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsCOMPtr<nsIURI> uri; + aRv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, baseURI); + if (aRv.Failed()) { + return nullptr; + } + + nsCString spec; + aRv = uri->GetSpec(spec); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<Promise> outer = + Promise::Create(global, aRv, Promise::ePropagateUserInteraction); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<ServiceWorkerContainer> self = this; + + if (!mActor) { + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return outer.forget(); + } + + mActor->SendGetRegistration( + clientInfo.ref().ToIPC(), spec, + [self, + outer](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult& + aResult) { + if (aResult.type() == + IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult:: + TCopyableErrorResult) { + CopyableErrorResult ipcRv(aResult.get_CopyableErrorResult()); + ErrorResult rv(std::move(ipcRv)); + if (!rv.Failed()) { + // ErrorResult rv; + // If rv is a failure then this is an application layer error. + // Note, though, we also reject with NS_OK to indicate that we just + // didn't find a registration. + Unused << self->GetGlobalIfValid(rv); + if (!rv.Failed()) { + outer->MaybeResolveWithUndefined(); + return; + } + } + outer->MaybeReject(std::move(rv)); + return; + } + // success + const auto& ipcDesc = + aResult.get_IPCServiceWorkerRegistrationDescriptor(); + ErrorResult rv; + nsIGlobalObject* global = self->GetGlobalIfValid(rv); + if (rv.Failed()) { + outer->MaybeReject(std::move(rv)); + return; + } + RefPtr<ServiceWorkerRegistration> reg = + global->GetOrCreateServiceWorkerRegistration( + ServiceWorkerRegistrationDescriptor(ipcDesc)); + outer->MaybeResolve(reg); + }, + [self, outer](ResponseRejectReason&& aReason) { + // IPC layer error + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + }); + return outer.forget(); +} + +Promise* ServiceWorkerContainer::GetReady(ErrorResult& aRv) { + if (mReadyPromise) { + return mReadyPromise; + } + + nsIGlobalObject* global = GetGlobalIfValid(aRv); + if (aRv.Failed()) { + return nullptr; + } + MOZ_DIAGNOSTIC_ASSERT(global); + + Maybe<ClientInfo> clientInfo(global->GetClientInfo()); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + mReadyPromise = + Promise::Create(global, aRv, Promise::ePropagateUserInteraction); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<ServiceWorkerContainer> self = this; + RefPtr<Promise> outer = mReadyPromise; + + if (!mActor) { + mReadyPromise->MaybeReject( + CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return mReadyPromise; + } + + mActor->SendGetReady( + clientInfo.ref().ToIPC(), + [self, + outer](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult& + aResult) { + if (aResult.type() == + IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult:: + TCopyableErrorResult) { + // application layer error + CopyableErrorResult rv(aResult.get_CopyableErrorResult()); + MOZ_DIAGNOSTIC_ASSERT(rv.Failed()); + outer->MaybeReject(std::move(rv)); + return; + } + // success + const auto& ipcDesc = + aResult.get_IPCServiceWorkerRegistrationDescriptor(); + ErrorResult rv; + nsIGlobalObject* global = self->GetGlobalIfValid(rv); + if (rv.Failed()) { + outer->MaybeReject(std::move(rv)); + return; + } + RefPtr<ServiceWorkerRegistration> reg = + global->GetOrCreateServiceWorkerRegistration( + ServiceWorkerRegistrationDescriptor(ipcDesc)); + NS_ENSURE_TRUE_VOID(reg); + + // Don't resolve the ready promise until the registration has + // reached the right version. This ensures that the active + // worker property is set correctly on the registration. + reg->WhenVersionReached(ipcDesc.version(), [outer, reg](bool aResult) { + outer->MaybeResolve(reg); + }); + }, + [outer](ResponseRejectReason&& aReason) { + // IPC layer error + outer->MaybeReject(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + }); + + return mReadyPromise; +} + +// Testing only. +void ServiceWorkerContainer::GetScopeForUrl(const nsAString& aUrl, + nsString& aScope, + ErrorResult& aRv) { + nsCOMPtr<nsIServiceWorkerManager> swm = + mozilla::components::ServiceWorkerManager::Service(); + if (!swm) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsCOMPtr<nsPIDOMWindowInner> window = GetOwner(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = StoragePrincipalHelper::GetPrincipal( + window, + StaticPrefs::privacy_partition_serviceWorkers() + ? StoragePrincipalHelper::eForeignPartitionedPrincipal + : StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(principal)); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + aRv = swm->GetScopeForUrl(principal, aUrl, aScope); +} + +nsIGlobalObject* ServiceWorkerContainer::GetGlobalIfValid( + ErrorResult& aRv, + const std::function<void(Document*)>&& aStorageFailureCB) const { + // For now we require a window since ServiceWorkerContainer is + // not exposed on worker globals yet. The main thing we need + // to fix here to support that is the storage access check via + // the nsIGlobalObject. + nsPIDOMWindowInner* window = GetOwner(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + nsCOMPtr<Document> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + // Don't allow a service worker to access service worker registrations + // from a window with storage disabled. If these windows can access + // the registration it increases the chance they can bypass the storage + // block via postMessage(), etc. + auto storageAllowed = StorageAllowedForWindow(window); + if (NS_WARN_IF(storageAllowed != StorageAccess::eAllow && + (!StaticPrefs::privacy_partition_serviceWorkers() || + !StoragePartitioningEnabled(storageAllowed, + doc->CookieJarSettings())))) { + if (aStorageFailureCB) { + aStorageFailureCB(doc); + } + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + // Don't allow service workers when the document is chrome. + if (NS_WARN_IF(doc->NodePrincipal()->IsSystemPrincipal())) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + return window->AsGlobal(); +} + +void ServiceWorkerContainer::EnqueueReceivedMessageDispatch( + RefPtr<ReceivedMessage> aMessage) { + if (nsPIDOMWindowInner* const window = GetOwner()) { + if (auto* const target = window->EventTargetFor(TaskCategory::Other)) { + target->Dispatch(NewRunnableMethod<RefPtr<ReceivedMessage>>( + "ServiceWorkerContainer::DispatchMessage", this, + &ServiceWorkerContainer::DispatchMessage, std::move(aMessage))); + } + } +} + +template <typename F> +void ServiceWorkerContainer::RunWithJSContext(F&& aCallable) { + nsCOMPtr<nsIGlobalObject> globalObject; + if (nsPIDOMWindowInner* const window = GetOwner()) { + globalObject = do_QueryInterface(window); + } + + // If AutoJSAPI::Init() fails then either global is nullptr or not + // in a usable state. + AutoJSAPI jsapi; + if (!jsapi.Init(globalObject)) { + return; + } + + aCallable(jsapi.cx(), globalObject); +} + +void ServiceWorkerContainer::DispatchMessage(RefPtr<ReceivedMessage> aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + + // When dispatching a message, either DOMContentLoaded has already + // been fired, or someone called startMessages() or set onmessage. + // Either way, a global object is supposed to be present. If it's + // not, we'd fail to initialize the JS API and exit. + RunWithJSContext([this, message = std::move(aMessage)]( + JSContext* const aCx, nsIGlobalObject* const aGlobal) { + ErrorResult result; + bool deserializationFailed = false; + RootedDictionary<MessageEventInit> init(aCx); + auto res = FillInMessageEventInit(aCx, aGlobal, *message, init, result); + if (res.isErr()) { + deserializationFailed = res.unwrapErr(); + MOZ_ASSERT_IF(deserializationFailed, init.mData.isNull()); + MOZ_ASSERT_IF(deserializationFailed, init.mPorts.IsEmpty()); + MOZ_ASSERT_IF(deserializationFailed, !init.mOrigin.IsEmpty()); + MOZ_ASSERT_IF(deserializationFailed, !init.mSource.IsNull()); + result.SuppressException(); + + if (!deserializationFailed && result.MaybeSetPendingException(aCx)) { + return; + } + } + + RefPtr<MessageEvent> event = MessageEvent::Constructor( + this, deserializationFailed ? u"messageerror"_ns : u"message"_ns, init); + event->SetTrusted(true); + + result = NS_OK; + DispatchEvent(*event, result); + if (result.Failed()) { + result.SuppressException(); + } + }); +} + +namespace { + +nsresult FillInOriginNoSuffix(const ServiceWorkerDescriptor& aServiceWorker, + nsString& aOrigin) { + using mozilla::ipc::PrincipalInfoToPrincipal; + + nsresult rv; + + auto principalOrErr = + PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo()); + if (NS_WARN_IF(principalOrErr.isErr())) { + return principalOrErr.unwrapErr(); + } + + nsAutoCString originUTF8; + rv = principalOrErr.unwrap()->GetOriginNoSuffix(originUTF8); + if (NS_FAILED(rv)) { + return rv; + } + + CopyUTF8toUTF16(originUTF8, aOrigin); + return NS_OK; +} + +} // namespace + +Result<Ok, bool> ServiceWorkerContainer::FillInMessageEventInit( + JSContext* const aCx, nsIGlobalObject* const aGlobal, + ReceivedMessage& aMessage, MessageEventInit& aInit, ErrorResult& aRv) { + // Determining the source and origin should preceed attempting deserialization + // because on a "messageerror" event (i.e. when deserialization fails), the + // dispatched message needs to contain such an origin and source, per spec: + // + // "If this throws an exception, catch it, fire an event named messageerror + // at destination, using MessageEvent, with the origin attribute initialized + // to origin and the source attribute initialized to source, and then abort + // these steps." - 6.4 of postMessage + // See: https://w3c.github.io/ServiceWorker/#service-worker-postmessage + const RefPtr<ServiceWorker> serviceWorkerInstance = + aGlobal->GetOrCreateServiceWorker(aMessage.mServiceWorker); + if (serviceWorkerInstance) { + aInit.mSource.SetValue().SetAsServiceWorker() = serviceWorkerInstance; + } + + const nsresult rv = + FillInOriginNoSuffix(aMessage.mServiceWorker, aInit.mOrigin); + if (NS_FAILED(rv)) { + return Err(false); + } + + JS::Rooted<JS::Value> messageData(aCx); + aMessage.mClonedData.Read(aCx, &messageData, aRv); + if (aRv.Failed()) { + return Err(true); + } + + aInit.mData = messageData; + + if (!aMessage.mClonedData.TakeTransferredPortsAsSequence(aInit.mPorts)) { + xpc::Throw(aCx, NS_ERROR_OUT_OF_MEMORY); + return Err(false); + } + + return Ok(); +} + +void ServiceWorkerContainer::Shutdown() { + if (mShutdown) { + return; + } + mShutdown = true; + + if (mActor) { + mActor->RevokeOwner(this); + mActor->MaybeStartTeardown(); + mActor = nullptr; + } +} + +} // namespace mozilla::dom |