/* -*- 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 "ServiceWorkerManager.h" #include "nsAutoPtr.h" #include "nsIConsoleService.h" #include "nsIEffectiveTLDService.h" #include "nsIScriptSecurityManager.h" #include "nsIStreamLoader.h" #include "nsIHttpChannel.h" #include "nsIHttpChannelInternal.h" #include "nsIHttpHeaderVisitor.h" #include "nsINamed.h" #include "nsINetworkInterceptController.h" #include "nsIMutableArray.h" #include "nsIScriptError.h" #include "nsISimpleEnumerator.h" #include "nsITimer.h" #include "nsIUploadChannel2.h" #include "nsServiceManagerUtils.h" #include "nsDebug.h" #include "nsISupportsPrimitives.h" #include "nsIPermissionManager.h" #include "nsXULAppAPI.h" #include "jsapi.h" #include "mozilla/BasePrincipal.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/ErrorNames.h" #include "mozilla/LoadContext.h" #include "mozilla/SystemGroup.h" #include "mozilla/Telemetry.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/ClientHandle.h" #include "mozilla/dom/ClientManager.h" #include "mozilla/dom/ClientSource.h" #include "mozilla/dom/ConsoleUtils.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/ErrorEvent.h" #include "mozilla/dom/Headers.h" #include "mozilla/dom/InternalHeaders.h" #include "mozilla/dom/Navigator.h" #include "mozilla/dom/NotificationEvent.h" #include "mozilla/dom/PromiseNativeHandler.h" #include "mozilla/dom/Request.h" #include "mozilla/dom/RootedDictionary.h" #include "mozilla/dom/TypedArray.h" #include "mozilla/dom/SharedWorker.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WorkerRunnable.h" #include "mozilla/dom/WorkerScope.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "mozilla/dom/ScriptLoader.h" #include "mozilla/Unused.h" #include "mozilla/EnumSet.h" #include "nsContentUtils.h" #include "nsNetUtil.h" #include "nsProxyRelease.h" #include "nsQueryObject.h" #include "nsTArray.h" #include "ServiceWorker.h" #include "ServiceWorkerContainer.h" #include "ServiceWorkerInfo.h" #include "ServiceWorkerJobQueue.h" #include "ServiceWorkerManagerChild.h" #include "ServiceWorkerPrivate.h" #include "ServiceWorkerRegisterJob.h" #include "ServiceWorkerRegistrar.h" #include "ServiceWorkerRegistration.h" #include "ServiceWorkerScriptCache.h" #include "ServiceWorkerShutdownBlocker.h" #include "ServiceWorkerEvents.h" #include "ServiceWorkerUnregisterJob.h" #include "ServiceWorkerUpdateJob.h" #include "ServiceWorkerUpdaterChild.h" #include "ServiceWorkerUtils.h" #ifdef PostMessage # undef PostMessage #endif using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::ipc; namespace mozilla { namespace dom { #define CLEAR_ORIGIN_DATA "clear-origin-attributes-data" static_assert( nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN == static_cast(RequestMode::Same_origin), "RequestMode enumeration value should match Necko CORS mode value."); static_assert( nsIHttpChannelInternal::CORS_MODE_NO_CORS == static_cast(RequestMode::No_cors), "RequestMode enumeration value should match Necko CORS mode value."); static_assert( nsIHttpChannelInternal::CORS_MODE_CORS == static_cast(RequestMode::Cors), "RequestMode enumeration value should match Necko CORS mode value."); static_assert( nsIHttpChannelInternal::CORS_MODE_NAVIGATE == static_cast(RequestMode::Navigate), "RequestMode enumeration value should match Necko CORS mode value."); static_assert( nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW == static_cast(RequestRedirect::Follow), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert( nsIHttpChannelInternal::REDIRECT_MODE_ERROR == static_cast(RequestRedirect::Error), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert( nsIHttpChannelInternal::REDIRECT_MODE_MANUAL == static_cast(RequestRedirect::Manual), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert( 3 == static_cast(RequestRedirect::EndGuard_), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT == static_cast(RequestCache::Default), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE == static_cast(RequestCache::No_store), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD == static_cast(RequestCache::Reload), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE == static_cast(RequestCache::No_cache), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE == static_cast(RequestCache::Force_cache), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED == static_cast(RequestCache::Only_if_cached), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( 6 == static_cast(RequestCache::EndGuard_), "RequestCache enumeration value should match Necko Cache mode value."); static_assert(static_cast(ServiceWorkerUpdateViaCache::Imports) == nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*" " should match ServiceWorkerUpdateViaCache enumeration."); static_assert(static_cast(ServiceWorkerUpdateViaCache::All) == nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*" " should match ServiceWorkerUpdateViaCache enumeration."); static_assert(static_cast(ServiceWorkerUpdateViaCache::None) == nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE, "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*" " should match ServiceWorkerUpdateViaCache enumeration."); static StaticRefPtr gInstance; struct ServiceWorkerManager::RegistrationDataPerPrincipal final { // Ordered list of scopes for glob matching. // Each entry is an absolute URL representing the scope. // Each value of the hash table is an array of an absolute URLs representing // the scopes. // // An array is used for now since the number of controlled scopes per // domain is expected to be relatively low. If that assumption was proved // wrong this should be replaced with a better structure to avoid the // memmoves associated with inserting stuff in the middle of the array. nsTArray mOrderedScopes; // Scope to registration. // The scope should be a fully qualified valid URL. nsRefPtrHashtable mInfos; // Maps scopes to job queues. nsRefPtrHashtable mJobQueues; // Map scopes to scheduled update timers. nsInterfaceHashtable mUpdateTimers; }; namespace { nsresult PopulateRegistrationData( nsIPrincipal* aPrincipal, const ServiceWorkerRegistrationInfo* aRegistration, ServiceWorkerRegistrationData& aData) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aRegistration); if (NS_WARN_IF(!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal())) { return NS_ERROR_FAILURE; } nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aData.principal()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } aData.scope() = aRegistration->Scope(); // TODO: When bug 1426401 is implemented we will need to handle more // than just the active worker here. RefPtr active = aRegistration->GetActive(); MOZ_ASSERT(active); if (NS_WARN_IF(!active)) { return NS_ERROR_FAILURE; } aData.currentWorkerURL() = active->ScriptSpec(); aData.cacheName() = active->CacheName(); aData.currentWorkerHandlesFetch() = active->HandlesFetch(); aData.currentWorkerInstalledTime() = active->GetInstalledTime(); aData.currentWorkerActivatedTime() = active->GetActivatedTime(); aData.updateViaCache() = static_cast(aRegistration->GetUpdateViaCache()); aData.lastUpdateTime() = aRegistration->GetLastUpdateTime(); MOZ_ASSERT(ServiceWorkerRegistrationDataIsValid(aData)); return NS_OK; } class TeardownRunnable final : public Runnable { public: explicit TeardownRunnable(ServiceWorkerManagerChild* aActor) : Runnable("dom::ServiceWorkerManager::TeardownRunnable"), mActor(aActor) { MOZ_ASSERT(mActor); } NS_IMETHOD Run() override { MOZ_ASSERT(mActor); mActor->SendShutdown(); return NS_OK; } private: ~TeardownRunnable() {} RefPtr mActor; }; bool ServiceWorkersAreCrossProcess() { return ServiceWorkerParentInterceptEnabled() && XRE_IsE10sParentProcess(); } const char* GetXPCOMShutdownTopic() { if (ServiceWorkersAreCrossProcess()) { return "profile-change-teardown"; } return NS_XPCOM_SHUTDOWN_OBSERVER_ID; } already_AddRefed GetAsyncShutdownBarrier() { AssertIsOnMainThread(); if (!ServiceWorkersAreCrossProcess()) { return nullptr; } nsCOMPtr svc = services::GetAsyncShutdown(); MOZ_ASSERT(svc); nsCOMPtr barrier; DebugOnly rv = svc->GetProfileChangeTeardown(getter_AddRefs(barrier)); MOZ_ASSERT(NS_SUCCEEDED(rv)); return barrier.forget(); } } // namespace ////////////////////////// // ServiceWorkerManager // ////////////////////////// NS_IMPL_ADDREF(ServiceWorkerManager) NS_IMPL_RELEASE(ServiceWorkerManager) NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager) NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIServiceWorkerManager) NS_INTERFACE_MAP_END ServiceWorkerManager::ServiceWorkerManager() : mActor(nullptr), mShuttingDown(false) {} ServiceWorkerManager::~ServiceWorkerManager() { // The map will assert if it is not empty when destroyed. mRegistrationInfos.Clear(); if (!ServiceWorkersAreCrossProcess()) { MOZ_ASSERT(!mActor); } } void ServiceWorkerManager::BlockShutdownOn( GenericNonExclusivePromise* aPromise) { AssertIsOnMainThread(); // This may be called when in non-e10s mode with parent-intercept enabled. if (!ServiceWorkersAreCrossProcess()) { return; } MOZ_ASSERT(mShutdownBlocker); MOZ_ASSERT(aPromise); mShutdownBlocker->WaitOnPromise(aPromise); } void ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) { nsCOMPtr shutdownBarrier = GetAsyncShutdownBarrier(); if (shutdownBarrier) { mShutdownBlocker = ServiceWorkerShutdownBlocker::CreateAndRegisterOn(shutdownBarrier); MOZ_ASSERT(mShutdownBlocker); } nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { DebugOnly rv; rv = obs->AddObserver(this, GetXPCOMShutdownTopic(), false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); } if (XRE_IsParentProcess()) { MOZ_DIAGNOSTIC_ASSERT(aRegistrar); nsTArray data; aRegistrar->GetRegistrations(data); LoadRegistrations(data); if (obs) { DebugOnly rv; rv = obs->AddObserver(this, CLEAR_ORIGIN_DATA, false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread(); if (NS_WARN_IF(!actorChild)) { MaybeStartShutdown(); return; } PServiceWorkerManagerChild* actor = actorChild->SendPServiceWorkerManagerConstructor(); if (!actor) { MaybeStartShutdown(); return; } mActor = static_cast(actor); } RefPtr ServiceWorkerManager::StartControllingClient( const ClientInfo& aClientInfo, ServiceWorkerRegistrationInfo* aRegistrationInfo, bool aControlClientHandle) { MOZ_DIAGNOSTIC_ASSERT(aRegistrationInfo->GetActive()); RefPtr promise; RefPtr self(this); const ServiceWorkerDescriptor& active = aRegistrationInfo->GetActive()->Descriptor(); auto entry = mControlledClients.LookupForAdd(aClientInfo.Id()); if (entry) { RefPtr old = entry.Data()->mRegistrationInfo.forget(); if (aControlClientHandle) { promise = entry.Data()->mClientHandle->Control(active); } else { promise = GenericPromise::CreateAndResolve(false, __func__); } entry.Data()->mRegistrationInfo = aRegistrationInfo; if (old != aRegistrationInfo) { StopControllingRegistration(old); aRegistrationInfo->StartControllingClient(); } Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS, 1); // Always check to see if we failed to actually control the client. In // that case removed the client from our list of controlled clients. return promise->Then( SystemGroup::EventTargetFor(TaskCategory::Other), __func__, [](bool) { // do nothing on success return GenericPromise::CreateAndResolve(true, __func__); }, [self, aClientInfo](nsresult aRv) { // failed to control, forget about this client self->StopControllingClient(aClientInfo); return GenericPromise::CreateAndReject(aRv, __func__); }); } RefPtr clientHandle = ClientManager::CreateHandle( aClientInfo, SystemGroup::EventTargetFor(TaskCategory::Other)); if (aControlClientHandle) { promise = clientHandle->Control(active); } else { promise = GenericPromise::CreateAndResolve(false, __func__); } aRegistrationInfo->StartControllingClient(); entry.OrInsert([&] { return new ControlledClientData(clientHandle, aRegistrationInfo); }); clientHandle->OnDetach()->Then( SystemGroup::EventTargetFor(TaskCategory::Other), __func__, [self, aClientInfo] { self->StopControllingClient(aClientInfo); }); Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS, 1); // Always check to see if we failed to actually control the client. In // that case removed the client from our list of controlled clients. return promise->Then( SystemGroup::EventTargetFor(TaskCategory::Other), __func__, [](bool) { // do nothing on success return GenericPromise::CreateAndResolve(true, __func__); }, [self, aClientInfo](nsresult aRv) { // failed to control, forget about this client self->StopControllingClient(aClientInfo); return GenericPromise::CreateAndReject(aRv, __func__); }); } void ServiceWorkerManager::StopControllingClient( const ClientInfo& aClientInfo) { auto entry = mControlledClients.Lookup(aClientInfo.Id()); if (!entry) { return; } RefPtr reg = entry.Data()->mRegistrationInfo.forget(); entry.Remove(); StopControllingRegistration(reg); } void ServiceWorkerManager::MaybeStartShutdown() { MOZ_ASSERT(NS_IsMainThread()); if (mShuttingDown) { return; } mShuttingDown = true; for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { for (auto it2 = it1.UserData()->mUpdateTimers.Iter(); !it2.Done(); it2.Next()) { nsCOMPtr timer = it2.UserData(); timer->Cancel(); } it1.UserData()->mUpdateTimers.Clear(); for (auto it2 = it1.UserData()->mJobQueues.Iter(); !it2.Done(); it2.Next()) { RefPtr queue = it2.UserData(); queue->CancelAll(); } it1.UserData()->mJobQueues.Clear(); for (auto it2 = it1.UserData()->mInfos.Iter(); !it2.Done(); it2.Next()) { RefPtr regInfo = it2.UserData(); regInfo->Clear(); } it1.UserData()->mInfos.Clear(); } if (mShutdownBlocker) { mShutdownBlocker->StopAcceptingPromises(); } nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, GetXPCOMShutdownTopic()); if (XRE_IsParentProcess()) { obs->RemoveObserver(this, CLEAR_ORIGIN_DATA); } } if (!mActor) { return; } mActor->ManagerShuttingDown(); RefPtr runnable = new TeardownRunnable(mActor); nsresult rv = NS_DispatchToMainThread(runnable); Unused << NS_WARN_IF(NS_FAILED(rv)); mActor = nullptr; } class ServiceWorkerResolveWindowPromiseOnRegisterCallback final : public ServiceWorkerJob::Callback { RefPtr mPromise; ~ServiceWorkerResolveWindowPromiseOnRegisterCallback() {} virtual void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aJob); if (aStatus.Failed()) { mPromise->Reject(std::move(aStatus), __func__); return; } MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Register); RefPtr registerJob = static_cast(aJob); RefPtr reg = registerJob->GetRegistration(); mPromise->Resolve(reg->Descriptor(), __func__); } public: ServiceWorkerResolveWindowPromiseOnRegisterCallback() : mPromise(new ServiceWorkerRegistrationPromise::Private(__func__)) {} RefPtr Promise() const { return mPromise; } NS_INLINE_DECL_REFCOUNTING( ServiceWorkerResolveWindowPromiseOnRegisterCallback, override) }; namespace { class PropagateSoftUpdateRunnable final : public Runnable { public: PropagateSoftUpdateRunnable(const OriginAttributes& aOriginAttributes, const nsAString& aScope) : Runnable("dom::ServiceWorkerManager::PropagateSoftUpdateRunnable"), mOriginAttributes(aOriginAttributes), mScope(aScope) {} NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); RefPtr swm = ServiceWorkerManager::GetInstance(); if (swm) { swm->PropagateSoftUpdate(mOriginAttributes, mScope); } return NS_OK; } private: ~PropagateSoftUpdateRunnable() {} const OriginAttributes mOriginAttributes; const nsString mScope; }; class PromiseResolverCallback final : public ServiceWorkerUpdateFinishCallback { public: PromiseResolverCallback(ServiceWorkerUpdateFinishCallback* aCallback, GenericPromise::Private* aPromise) : mCallback(aCallback), mPromise(aPromise) { MOZ_DIAGNOSTIC_ASSERT(mPromise); } void UpdateSucceeded(ServiceWorkerRegistrationInfo* aInfo) override { MOZ_DIAGNOSTIC_ASSERT(mPromise); if (mCallback) { mCallback->UpdateSucceeded(aInfo); } MaybeResolve(); } void UpdateFailed(ErrorResult& aStatus) override { MOZ_DIAGNOSTIC_ASSERT(mPromise); if (mCallback) { mCallback->UpdateFailed(aStatus); } MaybeResolve(); } private: ~PromiseResolverCallback() { MaybeResolve(); } void MaybeResolve() { if (mPromise) { mPromise->Resolve(true, __func__); mPromise = nullptr; } } RefPtr mCallback; RefPtr mPromise; }; // This runnable is used for 2 different tasks: // - to postpone the SoftUpdate() until the IPC SWM actor is created // (aInternalMethod == false) // - to call the 'real' SoftUpdate when the ServiceWorkerUpdaterChild is // notified by the parent (aInternalMethod == true) class SoftUpdateRunnable final : public CancelableRunnable { public: SoftUpdateRunnable(const OriginAttributes& aOriginAttributes, const nsACString& aScope, bool aInternalMethod, GenericPromise::Private* aPromise) : CancelableRunnable("dom::ServiceWorkerManager::SoftUpdateRunnable"), mAttrs(aOriginAttributes), mScope(aScope), mInternalMethod(aInternalMethod), mPromise(aPromise) {} NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { return NS_ERROR_FAILURE; } if (mInternalMethod) { RefPtr callback = new PromiseResolverCallback(nullptr, mPromise); mPromise = nullptr; swm->SoftUpdateInternal(mAttrs, mScope, callback); } else { swm->SoftUpdate(mAttrs, mScope); } return NS_OK; } nsresult Cancel() override { mPromise = nullptr; return NS_OK; } private: ~SoftUpdateRunnable() { if (mPromise) { mPromise->Resolve(true, __func__); } } const OriginAttributes mAttrs; const nsCString mScope; bool mInternalMethod; RefPtr mPromise; }; // This runnable is used for 3 different tasks: // - to postpone the Update() until the IPC SWM actor is created // (aType == ePostpone) // - to call the 'real' Update when the ServiceWorkerUpdaterChild is // notified by the parent (aType == eSuccess) // - an error must be propagated (aType == eFailure) class UpdateRunnable final : public CancelableRunnable { public: enum Type { ePostpone, eSuccess, eFailure, }; UpdateRunnable(nsIPrincipal* aPrincipal, const nsACString& aScope, ServiceWorkerUpdateFinishCallback* aCallback, Type aType, GenericPromise::Private* aPromise) : CancelableRunnable("dom::ServiceWorkerManager::UpdateRunnable"), mPrincipal(aPrincipal), mScope(aScope), mCallback(aCallback), mType(aType), mPromise(aPromise) {} NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { return NS_ERROR_FAILURE; } if (mType == ePostpone) { swm->Update(mPrincipal, mScope, mCallback); return NS_OK; } MOZ_ASSERT(mPromise); RefPtr callback = new PromiseResolverCallback(mCallback, mPromise); mPromise = nullptr; if (mType == eSuccess) { swm->UpdateInternal(mPrincipal, mScope, callback); return NS_OK; } ErrorResult error(NS_ERROR_DOM_ABORT_ERR); callback->UpdateFailed(error); return NS_OK; } nsresult Cancel() override { mPromise = nullptr; return NS_OK; } private: ~UpdateRunnable() { if (mPromise) { mPromise->Resolve(true, __func__); } } nsCOMPtr mPrincipal; const nsCString mScope; RefPtr mCallback; Type mType; RefPtr mPromise; }; class ResolvePromiseRunnable final : public CancelableRunnable { public: explicit ResolvePromiseRunnable(GenericPromise::Private* aPromise) : CancelableRunnable("dom::ServiceWorkerManager::ResolvePromiseRunnable"), mPromise(aPromise) {} NS_IMETHOD Run() override { MaybeResolve(); return NS_OK; } nsresult Cancel() override { mPromise = nullptr; return NS_OK; } private: ~ResolvePromiseRunnable() { MaybeResolve(); } void MaybeResolve() { if (mPromise) { mPromise->Resolve(true, __func__); mPromise = nullptr; } } RefPtr mPromise; }; } // namespace RefPtr ServiceWorkerManager::Register( const ClientInfo& aClientInfo, const nsACString& aScopeURL, const nsACString& aScriptURL, ServiceWorkerUpdateViaCache aUpdateViaCache) { nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScopeURL); if (NS_FAILED(rv)) { return ServiceWorkerRegistrationPromise::CreateAndReject( CopyableErrorResult(rv), __func__); } nsCOMPtr scriptURI; rv = NS_NewURI(getter_AddRefs(scriptURI), aScriptURL); if (NS_FAILED(rv)) { return ServiceWorkerRegistrationPromise::CreateAndReject( CopyableErrorResult(rv), __func__); } rv = ServiceWorkerScopeAndScriptAreValid(aClientInfo, scopeURI, scriptURI); if (NS_FAILED(rv)) { return ServiceWorkerRegistrationPromise::CreateAndReject( CopyableErrorResult(rv), __func__); } // If the previous validation step passed then we must have a principal. nsCOMPtr principal = aClientInfo.GetPrincipal(); nsAutoCString scopeKey; rv = PrincipalToScopeKey(principal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return ServiceWorkerRegistrationPromise::CreateAndReject( CopyableErrorResult(rv), __func__); } RefPtr queue = GetOrCreateJobQueue(scopeKey, aScopeURL); RefPtr cb = new ServiceWorkerResolveWindowPromiseOnRegisterCallback(); RefPtr job = new ServiceWorkerRegisterJob( principal, aScopeURL, aScriptURL, static_cast(aUpdateViaCache)); job->AppendResultCallback(cb); queue->ScheduleJob(job); MOZ_ASSERT(NS_IsMainThread()); Telemetry::Accumulate(Telemetry::SERVICE_WORKER_REGISTRATIONS, 1); return cb->Promise(); } /* * Implements the async aspects of the getRegistrations algorithm. */ class GetRegistrationsRunnable final : public Runnable { const ClientInfo mClientInfo; RefPtr mPromise; public: explicit GetRegistrationsRunnable(const ClientInfo& aClientInfo) : Runnable("dom::ServiceWorkerManager::GetRegistrationsRunnable"), mClientInfo(aClientInfo), mPromise(new ServiceWorkerRegistrationListPromise::Private(__func__)) {} RefPtr Promise() const { return mPromise; } NS_IMETHOD Run() override { auto scopeExit = MakeScopeExit( [&] { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { return NS_OK; } nsCOMPtr principal = mClientInfo.GetPrincipal(); if (!principal) { return NS_OK; } nsTArray array; if (NS_WARN_IF(!BasePrincipal::Cast(principal)->IsContentPrincipal())) { return NS_OK; } nsAutoCString scopeKey; nsresult rv = swm->PrincipalToScopeKey(principal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } ServiceWorkerManager::RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { scopeExit.release(); mPromise->Resolve(array, __func__); return NS_OK; } for (uint32_t i = 0; i < data->mOrderedScopes.Length(); ++i) { RefPtr info = data->mInfos.GetWeak(data->mOrderedScopes[i]); NS_ConvertUTF8toUTF16 scope(data->mOrderedScopes[i]); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope); if (NS_WARN_IF(NS_FAILED(rv))) { break; } rv = principal->CheckMayLoad(scopeURI, true /* report */, false /* allowIfInheritsPrincipal */); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } array.AppendElement(info->Descriptor()); } scopeExit.release(); mPromise->Resolve(array, __func__); return NS_OK; } }; RefPtr ServiceWorkerManager::GetRegistrations(const ClientInfo& aClientInfo) const { RefPtr runnable = new GetRegistrationsRunnable(aClientInfo); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable)); return runnable->Promise(); ; } /* * Implements the async aspects of the getRegistration algorithm. */ class GetRegistrationRunnable final : public Runnable { const ClientInfo mClientInfo; RefPtr mPromise; nsCString mURL; public: GetRegistrationRunnable(const ClientInfo& aClientInfo, const nsACString& aURL) : Runnable("dom::ServiceWorkerManager::GetRegistrationRunnable"), mClientInfo(aClientInfo), mPromise(new ServiceWorkerRegistrationPromise::Private(__func__)), mURL(aURL) {} RefPtr Promise() const { return mPromise; } NS_IMETHOD Run() override { RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); return NS_OK; } nsCOMPtr principal = mClientInfo.GetPrincipal(); if (!principal) { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); return NS_OK; } nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL); if (NS_WARN_IF(NS_FAILED(rv))) { mPromise->Reject(rv, __func__); return NS_OK; } rv = principal->CheckMayLoad(uri, true /* report */, false /* allowIfInheritsPrinciple */); if (NS_FAILED(rv)) { mPromise->Reject(NS_ERROR_DOM_SECURITY_ERR, __func__); return NS_OK; } RefPtr registration = swm->GetServiceWorkerRegistrationInfo(principal, uri); if (!registration) { // Reject with NS_OK means "not found". mPromise->Reject(NS_OK, __func__); return NS_OK; } mPromise->Resolve(registration->Descriptor(), __func__); return NS_OK; } }; RefPtr ServiceWorkerManager::GetRegistration( const ClientInfo& aClientInfo, const nsACString& aURL) const { MOZ_ASSERT(NS_IsMainThread()); RefPtr runnable = new GetRegistrationRunnable(aClientInfo, aURL); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable)); return runnable->Promise(); } NS_IMETHODIMP ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes, const nsACString& aScope, const nsTArray& aDataBytes, uint8_t optional_argc) { if (optional_argc == 1) { // This does one copy here (while constructing the Maybe) and another when // we end up copying into the SendPushEventRunnable. We could fix that to // only do one copy by making things between here and there take // Maybe>&&, but then we'd need to copy before we know // whether we really need to in PushMessageDispatcher::NotifyWorkers. Since // in practice this only affects JS callers that pass data, and we don't // have any right now, let's not worry about it. return SendPushEvent(aOriginAttributes, aScope, EmptyString(), Some(aDataBytes)); } MOZ_ASSERT(optional_argc == 0); return SendPushEvent(aOriginAttributes, aScope, EmptyString(), Nothing()); } nsresult ServiceWorkerManager::SendPushEvent( const nsACString& aOriginAttributes, const nsACString& aScope, const nsAString& aMessageId, const Maybe>& aData) { OriginAttributes attrs; if (!attrs.PopulateFromSuffix(aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } ServiceWorkerInfo* serviceWorker = GetActiveWorkerInfoForScope(attrs, aScope); if (NS_WARN_IF(!serviceWorker)) { return NS_ERROR_FAILURE; } RefPtr registration = GetRegistration(serviceWorker->Principal(), aScope); MOZ_DIAGNOSTIC_ASSERT(registration); return serviceWorker->WorkerPrivate()->SendPushEvent(aMessageId, aData, registration); } NS_IMETHODIMP ServiceWorkerManager::SendPushSubscriptionChangeEvent( const nsACString& aOriginAttributes, const nsACString& aScope) { OriginAttributes attrs; if (!attrs.PopulateFromSuffix(aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); if (!info) { return NS_ERROR_FAILURE; } return info->WorkerPrivate()->SendPushSubscriptionChangeEvent(); } nsresult ServiceWorkerManager::SendNotificationEvent( const nsAString& aEventName, const nsACString& aOriginSuffix, const nsACString& aScope, const nsAString& aID, const nsAString& aTitle, const nsAString& aDir, const nsAString& aLang, const nsAString& aBody, const nsAString& aTag, const nsAString& aIcon, const nsAString& aData, const nsAString& aBehavior) { OriginAttributes attrs; if (!attrs.PopulateFromSuffix(aOriginSuffix)) { return NS_ERROR_INVALID_ARG; } ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); if (!info) { return NS_ERROR_FAILURE; } ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate(); return workerPrivate->SendNotificationEvent( aEventName, aID, aTitle, aDir, aLang, aBody, aTag, aIcon, aData, aBehavior, NS_ConvertUTF8toUTF16(aScope)); } NS_IMETHODIMP ServiceWorkerManager::SendNotificationClickEvent( const nsACString& aOriginSuffix, const nsACString& aScope, const nsAString& aID, const nsAString& aTitle, const nsAString& aDir, const nsAString& aLang, const nsAString& aBody, const nsAString& aTag, const nsAString& aIcon, const nsAString& aData, const nsAString& aBehavior) { return SendNotificationEvent(NS_LITERAL_STRING(NOTIFICATION_CLICK_EVENT_NAME), aOriginSuffix, aScope, aID, aTitle, aDir, aLang, aBody, aTag, aIcon, aData, aBehavior); } NS_IMETHODIMP ServiceWorkerManager::SendNotificationCloseEvent( const nsACString& aOriginSuffix, const nsACString& aScope, const nsAString& aID, const nsAString& aTitle, const nsAString& aDir, const nsAString& aLang, const nsAString& aBody, const nsAString& aTag, const nsAString& aIcon, const nsAString& aData, const nsAString& aBehavior) { return SendNotificationEvent(NS_LITERAL_STRING(NOTIFICATION_CLOSE_EVENT_NAME), aOriginSuffix, aScope, aID, aTitle, aDir, aLang, aBody, aTag, aIcon, aData, aBehavior); } RefPtr ServiceWorkerManager::WhenReady( const ClientInfo& aClientInfo) { AssertIsOnMainThread(); for (auto& prd : mPendingReadyList) { if (prd->mClientHandle->Info().Id() == aClientInfo.Id() && prd->mClientHandle->Info().PrincipalInfo() == aClientInfo.PrincipalInfo()) { return prd->mPromise; } } RefPtr reg = GetServiceWorkerRegistrationInfo(aClientInfo); if (reg && reg->GetActive()) { return ServiceWorkerRegistrationPromise::CreateAndResolve(reg->Descriptor(), __func__); } nsCOMPtr target = SystemGroup::EventTargetFor(TaskCategory::Other); RefPtr handle = ClientManager::CreateHandle(aClientInfo, target); mPendingReadyList.AppendElement(MakeUnique(handle)); RefPtr self(this); handle->OnDetach()->Then(target, __func__, [self = std::move(self), aClientInfo] { self->RemovePendingReadyPromise(aClientInfo); }); return mPendingReadyList.LastElement()->mPromise; } void ServiceWorkerManager::CheckPendingReadyPromises() { nsTArray> pendingReadyList; mPendingReadyList.SwapElements(pendingReadyList); for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) { UniquePtr prd(std::move(pendingReadyList[i])); RefPtr reg = GetServiceWorkerRegistrationInfo(prd->mClientHandle->Info()); if (reg && reg->GetActive()) { prd->mPromise->Resolve(reg->Descriptor(), __func__); } else { mPendingReadyList.AppendElement(std::move(prd)); } } } void ServiceWorkerManager::RemovePendingReadyPromise( const ClientInfo& aClientInfo) { nsTArray> pendingReadyList; mPendingReadyList.SwapElements(pendingReadyList); for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) { UniquePtr prd(std::move(pendingReadyList[i])); if (prd->mClientHandle->Info().Id() == aClientInfo.Id() && prd->mClientHandle->Info().PrincipalInfo() == aClientInfo.PrincipalInfo()) { prd->mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); } else { mPendingReadyList.AppendElement(std::move(prd)); } } } void ServiceWorkerManager::NoteInheritedController( const ClientInfo& aClientInfo, const ServiceWorkerDescriptor& aController) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr principal = PrincipalInfoToPrincipal(aController.PrincipalInfo()); NS_ENSURE_TRUE_VOID(principal); nsCOMPtr scope; nsresult rv = NS_NewURI(getter_AddRefs(scope), aController.Scope()); NS_ENSURE_SUCCESS_VOID(rv); RefPtr registration = GetServiceWorkerRegistrationInfo(principal, scope); NS_ENSURE_TRUE_VOID(registration); NS_ENSURE_TRUE_VOID(registration->GetActive()); StartControllingClient(aClientInfo, registration, false /* aControlClientHandle */); } ServiceWorkerInfo* ServiceWorkerManager::GetActiveWorkerInfoForScope( const OriginAttributes& aOriginAttributes, const nsACString& aScope) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); if (NS_FAILED(rv)) { return nullptr; } nsCOMPtr principal = BasePrincipal::CreateContentPrincipal(scopeURI, aOriginAttributes); RefPtr registration = GetServiceWorkerRegistrationInfo(principal, scopeURI); if (!registration) { return nullptr; } return registration->GetActive(); } namespace { class UnregisterJobCallback final : public ServiceWorkerJob::Callback { nsCOMPtr mCallback; ~UnregisterJobCallback() {} public: explicit UnregisterJobCallback(nsIServiceWorkerUnregisterCallback* aCallback) : mCallback(aCallback) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mCallback); } void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aJob); if (aStatus.Failed()) { mCallback->UnregisterFailed(); return; } MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Unregister); RefPtr unregisterJob = static_cast(aJob); mCallback->UnregisterSucceeded(unregisterJob->GetResult()); } NS_INLINE_DECL_REFCOUNTING(UnregisterJobCallback, override) }; } // anonymous namespace NS_IMETHODIMP ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback, const nsAString& aScope) { MOZ_ASSERT(NS_IsMainThread()); if (!aPrincipal) { return NS_ERROR_FAILURE; } nsresult rv; // This is not accessible by content, and callers should always ensure scope is // a correct URI, so this is wrapped in DEBUG #ifdef DEBUG nsCOMPtr scopeURI; rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } #endif nsAutoCString scopeKey; rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } NS_ConvertUTF16toUTF8 scope(aScope); RefPtr queue = GetOrCreateJobQueue(scopeKey, scope); RefPtr job = new ServiceWorkerUnregisterJob( aPrincipal, scope, true /* send to parent */); if (aCallback) { RefPtr cb = new UnregisterJobCallback(aCallback); job->AppendResultCallback(cb); } queue->ScheduleJob(job); return NS_OK; } nsresult ServiceWorkerManager::NotifyUnregister(nsIPrincipal* aPrincipal, const nsAString& aScope) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); nsresult rv; // This is not accessible by content, and callers should always ensure scope is // a correct URI, so this is wrapped in DEBUG #ifdef DEBUG nsCOMPtr scopeURI; rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #endif nsAutoCString scopeKey; rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } NS_ConvertUTF16toUTF8 scope(aScope); RefPtr queue = GetOrCreateJobQueue(scopeKey, scope); RefPtr job = new ServiceWorkerUnregisterJob( aPrincipal, scope, false /* send to parent */); queue->ScheduleJob(job); return NS_OK; } void ServiceWorkerManager::WorkerIsIdle(ServiceWorkerInfo* aWorker) { MOZ_ASSERT(NS_IsMainThread()); MOZ_DIAGNOSTIC_ASSERT(aWorker); RefPtr reg = GetRegistration(aWorker->Principal(), aWorker->Scope()); if (!reg) { return; } if (reg->GetActive() != aWorker) { return; } reg->TryToActivateAsync(); } already_AddRefed ServiceWorkerManager::GetOrCreateJobQueue(const nsACString& aKey, const nsACString& aScope) { MOZ_ASSERT(!aKey.IsEmpty()); ServiceWorkerManager::RegistrationDataPerPrincipal* data; // XXX we could use LookupForAdd here to avoid a hashtable lookup, except that // leads to a false positive assertion, see bug 1370674 comment 7. if (!mRegistrationInfos.Get(aKey, &data)) { data = new RegistrationDataPerPrincipal(); mRegistrationInfos.Put(aKey, data); } RefPtr queue = data->mJobQueues.LookupForAdd(aScope).OrInsert( []() { return new ServiceWorkerJobQueue(); }); return queue.forget(); } /* static */ already_AddRefed ServiceWorkerManager::GetInstance() { // Note: We don't simply check gInstance for null-ness here, since otherwise // this can resurrect the ServiceWorkerManager pretty late during shutdown. static bool firstTime = true; if (firstTime) { RefPtr swr; // Don't create the ServiceWorkerManager until the ServiceWorkerRegistrar is // initialized. if (XRE_IsParentProcess()) { swr = ServiceWorkerRegistrar::Get(); if (!swr) { return nullptr; } } firstTime = false; MOZ_ASSERT(NS_IsMainThread()); gInstance = new ServiceWorkerManager(); gInstance->Init(swr); ClearOnShutdown(&gInstance); } RefPtr copy = gInstance.get(); return copy.forget(); } void ServiceWorkerManager::FinishFetch( ServiceWorkerRegistrationInfo* aRegistration) {} void ServiceWorkerManager::ReportToAllClients( const nsCString& aScope, const nsString& aMessage, const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, uint32_t aColumnNumber, uint32_t aFlags) { ConsoleUtils::ReportForServiceWorkerScope( NS_ConvertUTF8toUTF16(aScope), aMessage, aFilename, aLineNumber, aColumnNumber, ConsoleUtils::eError); } /* static */ void ServiceWorkerManager::LocalizeAndReportToAllClients( const nsCString& aScope, const char* aStringKey, const nsTArray& aParamArray, uint32_t aFlags, const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, uint32_t aColumnNumber) { RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { return; } nsresult rv; nsAutoString message; rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, aStringKey, aParamArray, message); if (NS_SUCCEEDED(rv)) { swm->ReportToAllClients(aScope, message, aFilename, aLine, aLineNumber, aColumnNumber, aFlags); } else { NS_WARNING("Failed to format and therefore report localized error."); } } void ServiceWorkerManager::HandleError( JSContext* aCx, nsIPrincipal* aPrincipal, const nsCString& aScope, const nsString& aWorkerURL, const nsString& aMessage, const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, uint32_t aColumnNumber, uint32_t aFlags, JSExnType aExnType) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } ServiceWorkerManager::RegistrationDataPerPrincipal* data; if (NS_WARN_IF(!mRegistrationInfos.Get(scopeKey, &data))) { return; } // Always report any uncaught exceptions or errors to the console of // each client. ReportToAllClients(aScope, aMessage, aFilename, aLine, aLineNumber, aColumnNumber, aFlags); } void ServiceWorkerManager::LoadRegistration( const ServiceWorkerRegistrationData& aRegistration) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr principal = PrincipalInfoToPrincipal(aRegistration.principal()); if (!principal) { return; } RefPtr registration = GetRegistration(principal, aRegistration.scope()); if (!registration) { registration = CreateNewRegistration(aRegistration.scope(), principal, static_cast( aRegistration.updateViaCache())); } else { // If active worker script matches our expectations for a "current worker", // then we are done. Since scripts with the same URL might have different // contents such as updated scripts or scripts with different LoadFlags, we // use the CacheName to judje whether the two scripts are identical, where // the CacheName is an UUID generated when a new script is found. if (registration->GetActive() && registration->GetActive()->CacheName() == aRegistration.cacheName()) { // No needs for updates. return; } } registration->SetLastUpdateTime(aRegistration.lastUpdateTime()); nsLoadFlags importsLoadFlags = nsIChannel::LOAD_BYPASS_SERVICE_WORKER; importsLoadFlags |= aRegistration.updateViaCache() == static_cast(ServiceWorkerUpdateViaCache::None) ? nsIRequest::LOAD_NORMAL : nsIRequest::VALIDATE_ALWAYS; const nsCString& currentWorkerURL = aRegistration.currentWorkerURL(); if (!currentWorkerURL.IsEmpty()) { registration->SetActive(new ServiceWorkerInfo( registration->Principal(), registration->Scope(), registration->Id(), registration->Version(), currentWorkerURL, aRegistration.cacheName(), importsLoadFlags)); registration->GetActive()->SetHandlesFetch( aRegistration.currentWorkerHandlesFetch()); registration->GetActive()->SetInstalledTime( aRegistration.currentWorkerInstalledTime()); registration->GetActive()->SetActivatedTime( aRegistration.currentWorkerActivatedTime()); } } void ServiceWorkerManager::LoadRegistrations( const nsTArray& aRegistrations) { MOZ_ASSERT(NS_IsMainThread()); for (uint32_t i = 0, len = aRegistrations.Length(); i < len; ++i) { LoadRegistration(aRegistrations[i]); } } void ServiceWorkerManager::StoreRegistration( nsIPrincipal* aPrincipal, ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aRegistration); if (mShuttingDown) { return; } ServiceWorkerRegistrationData data; nsresult rv = PopulateRegistrationData(aPrincipal, aRegistration, data); if (NS_WARN_IF(NS_FAILED(rv))) { return; } PrincipalInfo principalInfo; if (NS_WARN_IF( NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) { return; } mActor->SendRegister(data); } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistrationInfo( const ClientInfo& aClientInfo) const { nsCOMPtr principal = aClientInfo.GetPrincipal(); NS_ENSURE_TRUE(principal, nullptr); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aClientInfo.URL()); NS_ENSURE_SUCCESS(rv, nullptr); return GetServiceWorkerRegistrationInfo(principal, uri); } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal, nsIURI* aURI) const { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aURI); nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_FAILED(rv)) { return nullptr; } return GetServiceWorkerRegistrationInfo(scopeKey, aURI); } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistrationInfo( const nsACString& aScopeKey, nsIURI* aURI) const { MOZ_ASSERT(aURI); nsAutoCString spec; nsresult rv = aURI->GetSpec(spec); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } nsAutoCString scope; RegistrationDataPerPrincipal* data; if (!FindScopeForPath(aScopeKey, spec, &data, scope)) { return nullptr; } MOZ_ASSERT(data); RefPtr registration; data->mInfos.Get(scope, getter_AddRefs(registration)); // ordered scopes and registrations better be in sync. MOZ_ASSERT(registration); #ifdef DEBUG nsAutoCString origin; rv = registration->Principal()->GetOrigin(origin); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(origin.Equals(aScopeKey)); #endif return registration.forget(); } /* static */ nsresult ServiceWorkerManager::PrincipalToScopeKey(nsIPrincipal* aPrincipal, nsACString& aKey) { MOZ_ASSERT(aPrincipal); if (!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal()) { return NS_ERROR_FAILURE; } nsresult rv = aPrincipal->GetOrigin(aKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } /* static */ nsresult ServiceWorkerManager::PrincipalInfoToScopeKey( const PrincipalInfo& aPrincipalInfo, nsACString& aKey) { if (aPrincipalInfo.type() != PrincipalInfo::TContentPrincipalInfo) { return NS_ERROR_FAILURE; } auto content = aPrincipalInfo.get_ContentPrincipalInfo(); nsAutoCString suffix; content.attrs().CreateSuffix(suffix); aKey = content.originNoSuffix(); aKey.Append(suffix); return NS_OK; } /* static */ void ServiceWorkerManager::AddScopeAndRegistration( const nsACString& aScope, ServiceWorkerRegistrationInfo* aInfo) { MOZ_ASSERT(aInfo); MOZ_ASSERT(aInfo->Principal()); MOZ_ASSERT(!aInfo->IsUnregistered()); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { // browser shutdown return; } nsAutoCString scopeKey; nsresult rv = swm->PrincipalToScopeKey(aInfo->Principal(), scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } MOZ_ASSERT(!scopeKey.IsEmpty()); RegistrationDataPerPrincipal* data = swm->mRegistrationInfos.LookupForAdd(scopeKey).OrInsert( []() { return new RegistrationDataPerPrincipal(); }); for (uint32_t i = 0; i < data->mOrderedScopes.Length(); ++i) { const nsCString& current = data->mOrderedScopes[i]; // Perfect match! if (aScope.Equals(current)) { data->mInfos.Put(aScope, aInfo); swm->NotifyListenersOnRegister(aInfo); return; } // Sort by length, with longest match first. // /foo/bar should be before /foo/ // Similarly /foo/b is between the two. if (StringBeginsWith(aScope, current)) { data->mOrderedScopes.InsertElementAt(i, aScope); data->mInfos.Put(aScope, aInfo); swm->NotifyListenersOnRegister(aInfo); return; } } data->mOrderedScopes.AppendElement(aScope); data->mInfos.Put(aScope, aInfo); swm->NotifyListenersOnRegister(aInfo); } /* static */ bool ServiceWorkerManager::FindScopeForPath( const nsACString& aScopeKey, const nsACString& aPath, RegistrationDataPerPrincipal** aData, nsACString& aMatch) { MOZ_ASSERT(aData); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm || !swm->mRegistrationInfos.Get(aScopeKey, aData)) { return false; } for (uint32_t i = 0; i < (*aData)->mOrderedScopes.Length(); ++i) { const nsCString& current = (*aData)->mOrderedScopes[i]; if (StringBeginsWith(aPath, current)) { aMatch = current; return true; } } return false; } /* static */ bool ServiceWorkerManager::HasScope(nsIPrincipal* aPrincipal, const nsACString& aScope) { RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { return false; } nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { return false; } return data->mOrderedScopes.Contains(aScope); } /* static */ void ServiceWorkerManager::RemoveScopeAndRegistration( ServiceWorkerRegistrationInfo* aRegistration) { RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { return; } nsAutoCString scopeKey; nsresult rv = swm->PrincipalToScopeKey(aRegistration->Principal(), scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { return; } if (auto entry = data->mUpdateTimers.Lookup(aRegistration->Scope())) { entry.Data()->Cancel(); entry.Remove(); } // Verify there are no controlled clients for the purged registration. for (auto iter = swm->mControlledClients.Iter(); !iter.Done(); iter.Next()) { auto& reg = iter.UserData()->mRegistrationInfo; if (reg->Scope().Equals(aRegistration->Scope()) && reg->Principal()->Equals(aRegistration->Principal()) && reg->IsCorrupt()) { iter.Remove(); } } RefPtr info; data->mInfos.Remove(aRegistration->Scope(), getter_AddRefs(info)); aRegistration->SetUnregistered(); data->mOrderedScopes.RemoveElement(aRegistration->Scope()); swm->NotifyListenersOnUnregister(info); swm->MaybeRemoveRegistrationInfo(scopeKey); } void ServiceWorkerManager::MaybeRemoveRegistrationInfo( const nsACString& aScopeKey) { if (auto entry = mRegistrationInfos.Lookup(aScopeKey)) { if (entry.Data()->mOrderedScopes.IsEmpty() && entry.Data()->mJobQueues.Count() == 0) { entry.Remove(); } } } bool ServiceWorkerManager::StartControlling( const ClientInfo& aClientInfo, const ServiceWorkerDescriptor& aServiceWorker) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr principal = PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo()); NS_ENSURE_TRUE(principal, false); nsCOMPtr scope; nsresult rv = NS_NewURI(getter_AddRefs(scope), aServiceWorker.Scope()); NS_ENSURE_SUCCESS(rv, false); RefPtr registration = GetServiceWorkerRegistrationInfo(principal, scope); NS_ENSURE_TRUE(registration, false); NS_ENSURE_TRUE(registration->GetActive(), false); StartControllingClient(aClientInfo, registration); return true; } void ServiceWorkerManager::MaybeCheckNavigationUpdate( const ClientInfo& aClientInfo) { MOZ_ASSERT(NS_IsMainThread()); // We perform these success path navigation update steps when the // document tells us its more or less done loading. This avoids // slowing down page load and also lets pages consistently get // updatefound events when they fire. // // 9.8.20 If respondWithEntered is false, then: // 9.8.22 Else: (respondWith was entered and succeeded) // If request is a non-subresource request, then: Invoke Soft Update // algorithm. ControlledClientData* data = mControlledClients.Get(aClientInfo.Id()); if (data && data->mRegistrationInfo) { data->mRegistrationInfo->MaybeScheduleUpdate(); } } void ServiceWorkerManager::StopControllingRegistration( ServiceWorkerRegistrationInfo* aRegistration) { aRegistration->StopControllingClient(); if (aRegistration->IsControllingClients()) { return; } if (aRegistration->IsUnregistered()) { if (aRegistration->IsIdle()) { aRegistration->Clear(); } else { aRegistration->ClearWhenIdle(); } return; } // We use to aggressively terminate the worker at this point, but it // caused problems. There are more uses for a service worker than actively // controlled documents. We need to let the worker naturally terminate // in case its handling push events, message events, etc. aRegistration->TryToActivateAsync(); } NS_IMETHODIMP ServiceWorkerManager::GetScopeForUrl(nsIPrincipal* aPrincipal, const nsAString& aUrl, nsAString& aScope) { MOZ_ASSERT(aPrincipal); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } RefPtr r = GetServiceWorkerRegistrationInfo(aPrincipal, uri); if (!r) { return NS_ERROR_FAILURE; } aScope = NS_ConvertUTF8toUTF16(r->Scope()); return NS_OK; } namespace { class ContinueDispatchFetchEventRunnable : public Runnable { RefPtr mServiceWorkerPrivate; nsCOMPtr mChannel; nsCOMPtr mLoadGroup; bool mIsReload; public: ContinueDispatchFetchEventRunnable( ServiceWorkerPrivate* aServiceWorkerPrivate, nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup, bool aIsReload) : Runnable( "dom::ServiceWorkerManager::ContinueDispatchFetchEventRunnable"), mServiceWorkerPrivate(aServiceWorkerPrivate), mChannel(aChannel), mLoadGroup(aLoadGroup), mIsReload(aIsReload) { MOZ_ASSERT(aServiceWorkerPrivate); MOZ_ASSERT(aChannel); } void HandleError() { MOZ_ASSERT(NS_IsMainThread()); NS_WARNING("Unexpected error while dispatching fetch event!"); nsresult rv = mChannel->ResetInterception(); if (NS_FAILED(rv)) { NS_WARNING("Failed to resume intercepted network request"); mChannel->CancelInterception(rv); } } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr channel; nsresult rv = mChannel->GetChannel(getter_AddRefs(channel)); if (NS_WARN_IF(NS_FAILED(rv))) { HandleError(); return NS_OK; } // The channel might have encountered an unexpected error while ensuring // the upload stream is cloneable. Check here and reset the interception // if that happens. nsresult status; rv = channel->GetStatus(&status); if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(status))) { HandleError(); return NS_OK; } nsString clientId; nsString resultingClientId; nsCOMPtr loadInfo = channel->LoadInfo(); char buf[NSID_LENGTH]; Maybe clientInfo = loadInfo->GetClientInfo(); if (clientInfo.isSome()) { clientInfo.ref().Id().ToProvidedString(buf); NS_ConvertASCIItoUTF16 uuid(buf); // Remove {} and the null terminator clientId.Assign(Substring(uuid, 1, NSID_LENGTH - 3)); } // Having an initial or reserved client are mutually exclusive events: // either an initial client is used upon navigating an about:blank // iframe, or a new, reserved environment/client is created (e.g. // upon a top-level navigation). See step 4 of // https://html.spec.whatwg.org/#process-a-navigate-fetch as well as // https://github.com/w3c/ServiceWorker/issues/1228#issuecomment-345132444 Maybe resulting = loadInfo->GetInitialClientInfo(); if (resulting.isNothing()) { resulting = loadInfo->GetReservedClientInfo(); } else { MOZ_ASSERT(loadInfo->GetReservedClientInfo().isNothing()); } if (resulting.isSome()) { resulting.ref().Id().ToProvidedString(buf); NS_ConvertASCIItoUTF16 uuid(buf); resultingClientId.Assign(Substring(uuid, 1, NSID_LENGTH - 3)); } rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, clientId, resultingClientId, mIsReload); if (NS_WARN_IF(NS_FAILED(rv))) { HandleError(); } return NS_OK; } }; } // anonymous namespace void ServiceWorkerManager::DispatchFetchEvent(nsIInterceptedChannel* aChannel, ErrorResult& aRv) { MOZ_ASSERT(aChannel); MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr internalChannel; aRv = aChannel->GetChannel(getter_AddRefs(internalChannel)); if (NS_WARN_IF(aRv.Failed())) { return; } nsCOMPtr loadGroup; aRv = internalChannel->GetLoadGroup(getter_AddRefs(loadGroup)); if (NS_WARN_IF(aRv.Failed())) { return; } nsCOMPtr loadInfo = internalChannel->LoadInfo(); RefPtr serviceWorker; if (!nsContentUtils::IsNonSubresourceRequest(internalChannel)) { const Maybe& controller = loadInfo->GetController(); if (NS_WARN_IF(controller.isNothing())) { aRv.Throw(NS_ERROR_FAILURE); return; } RefPtr registration; nsresult rv = GetClientRegistration(loadInfo->GetClientInfo().ref(), getter_AddRefs(registration)); if (NS_WARN_IF(NS_FAILED(rv))) { aRv.Throw(rv); return; } serviceWorker = registration->GetActive(); if (NS_WARN_IF(!serviceWorker) || NS_WARN_IF(serviceWorker->Descriptor().Id() != controller.ref().Id())) { aRv.Throw(NS_ERROR_FAILURE); return; } } else { nsCOMPtr uri; aRv = aChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri)); if (NS_WARN_IF(aRv.Failed())) { return; } // non-subresource request means the URI contains the principal nsCOMPtr principal = BasePrincipal::CreateContentPrincipal( uri, loadInfo->GetOriginAttributes()); RefPtr registration = GetServiceWorkerRegistrationInfo(principal, uri); if (NS_WARN_IF(!registration)) { aRv.Throw(NS_ERROR_FAILURE); return; } // While we only enter this method if IsAvailable() previously saw // an active worker, it is possible for that worker to be removed // before we get to this point. Therefore we must handle a nullptr // active worker here. serviceWorker = registration->GetActive(); if (NS_WARN_IF(!serviceWorker)) { aRv.Throw(NS_ERROR_FAILURE); return; } // If there is a reserved client it should be marked as controlled before // the FetchEvent is dispatched. Maybe clientInfo = loadInfo->GetReservedClientInfo(); // Also override the initial about:blank controller since the real // network load may be intercepted by a different service worker. If // the intial about:blank has a controller here its simply been // inherited from its parent. if (clientInfo.isNothing()) { clientInfo = loadInfo->GetInitialClientInfo(); // TODO: We need to handle the case where the initial about:blank is // controlled, but the final document load is not. Right now // the spec does not really say what to do. There currently // is no way for the controller to be cleared from a client in // the spec or our implementation. We may want to force a // new inner window to be created instead of reusing the // initial about:blank global. See bug 1419620 and the spec // issue here: https://github.com/w3c/ServiceWorker/issues/1232 } if (clientInfo.isSome()) { // ClientChannelHelper is not called for STS upgrades that get // intercepted by a service worker when interception occurs in // the content process. Therefore the reserved client is not // properly cleared in that case leading to a situation where // a ClientSource with an http:// principal is controlled by // a ServiceWorker with an https:// principal. // // This does not occur when interception is handled by the // simpler InterceptedHttpChannel approach in the parent. // // As a temporary work around check for this principal mismatch // here and perform the ClientChannelHelper's replacement of // reserved client automatically. if (!XRE_IsParentProcess()) { nsCOMPtr clientPrincipal = clientInfo.ref().GetPrincipal(); if (!clientPrincipal || !clientPrincipal->Equals(principal)) { UniquePtr reservedClient = loadInfo->TakeReservedClientSource(); nsCOMPtr target = reservedClient ? reservedClient->EventTarget() : SystemGroup::EventTargetFor(TaskCategory::Other); reservedClient.reset(); reservedClient = ClientManager::CreateSource(ClientType::Window, target, principal); loadInfo->GiveReservedClientSource(std::move(reservedClient)); clientInfo = loadInfo->GetReservedClientInfo(); } } // First, attempt to mark the reserved client controlled directly. This // will update the controlled status in the ClientManagerService in the // parent. It will also eventually propagate back to the ClientSource. StartControllingClient(clientInfo.ref(), registration); } uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL; nsCOMPtr http = do_QueryInterface(internalChannel); MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode)); // Synthetic redirects for non-subresource requests with a "follow" // redirect mode may switch controllers. This is basically worker // scripts right now. In this case we need to explicitly clear the // controller to avoid assertions on the SetController() below. if (redirectMode == nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) { loadInfo->ClearController(); } // But we also note the reserved state on the LoadInfo. This allows the // ClientSource to be updated immediately after the nsIChannel starts. // This is necessary to have the correct controller in place for immediate // follow-on requests. loadInfo->SetController(serviceWorker->Descriptor()); } MOZ_DIAGNOSTIC_ASSERT(serviceWorker); nsCOMPtr continueRunnable = new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(), aChannel, loadGroup, loadInfo->GetIsDocshellReload()); // When this service worker was registered, we also sent down the permissions // for the runnable. They should have arrived by now, but we still need to // wait for them if they have not. nsCOMPtr permissionsRunnable = NS_NewRunnableFunction( "dom::ServiceWorkerManager::DispatchFetchEvent", [=]() { nsCOMPtr permMgr = services::GetPermissionManager(); MOZ_ALWAYS_SUCCEEDS(permMgr->WhenPermissionsAvailable( serviceWorker->Principal(), continueRunnable)); }); nsCOMPtr uploadChannel = do_QueryInterface(internalChannel); // If there is no upload stream, then continue immediately if (!uploadChannel) { MOZ_ALWAYS_SUCCEEDS(permissionsRunnable->Run()); return; } // Otherwise, ensure the upload stream can be cloned directly. This may // require some async copying, so provide a callback. aRv = uploadChannel->EnsureUploadStreamIsCloneable(permissionsRunnable); } bool ServiceWorkerManager::IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aURI); RefPtr registration = GetServiceWorkerRegistrationInfo(aPrincipal, aURI); return registration && registration->GetActive(); } nsresult ServiceWorkerManager::GetClientRegistration( const ClientInfo& aClientInfo, ServiceWorkerRegistrationInfo** aRegistrationInfo) { ControlledClientData* data = mControlledClients.Get(aClientInfo.Id()); if (!data || !data->mRegistrationInfo) { return NS_ERROR_NOT_AVAILABLE; } // If the document is controlled, the current worker MUST be non-null. if (!data->mRegistrationInfo->GetActive()) { return NS_ERROR_NOT_AVAILABLE; } RefPtr ref = data->mRegistrationInfo; ref.forget(aRegistrationInfo); return NS_OK; } void ServiceWorkerManager::SoftUpdate(const OriginAttributes& aOriginAttributes, const nsACString& aScope) { MOZ_ASSERT(NS_IsMainThread()); if (mShuttingDown) { return; } if (ServiceWorkerParentInterceptEnabled()) { SoftUpdateInternal(aOriginAttributes, aScope, nullptr); return; } RefPtr promise = new GenericPromise::Private(__func__); RefPtr successRunnable = new SoftUpdateRunnable(aOriginAttributes, aScope, true, promise); RefPtr failureRunnable = new ResolvePromiseRunnable(promise); ServiceWorkerUpdaterChild* actor = new ServiceWorkerUpdaterChild(promise, successRunnable, failureRunnable); mActor->SendPServiceWorkerUpdaterConstructor(actor, aOriginAttributes, nsCString(aScope)); } namespace { class UpdateJobCallback final : public ServiceWorkerJob::Callback { RefPtr mCallback; ~UpdateJobCallback() = default; public: explicit UpdateJobCallback(ServiceWorkerUpdateFinishCallback* aCallback) : mCallback(aCallback) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mCallback); } void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aJob); if (aStatus.Failed()) { mCallback->UpdateFailed(aStatus); return; } MOZ_DIAGNOSTIC_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Update); RefPtr updateJob = static_cast(aJob); RefPtr reg = updateJob->GetRegistration(); mCallback->UpdateSucceeded(reg); } NS_INLINE_DECL_REFCOUNTING(UpdateJobCallback, override) }; } // anonymous namespace void ServiceWorkerManager::SoftUpdateInternal( const OriginAttributes& aOriginAttributes, const nsACString& aScope, ServiceWorkerUpdateFinishCallback* aCallback) { MOZ_ASSERT(NS_IsMainThread()); if (mShuttingDown) { return; } nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr principal = BasePrincipal::CreateContentPrincipal(scopeURI, aOriginAttributes); if (NS_WARN_IF(!principal)) { return; } nsAutoCString scopeKey; rv = PrincipalToScopeKey(principal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RefPtr registration = GetRegistration(scopeKey, aScope); if (NS_WARN_IF(!registration)) { return; } // "If registration's installing worker is not null, abort these steps." if (registration->GetInstalling()) { return; } // "Let newestWorker be the result of running Get Newest Worker algorithm // passing registration as its argument. // If newestWorker is null, abort these steps." RefPtr newest = registration->Newest(); if (!newest) { return; } // "If the registration queue for registration is empty, invoke Update // algorithm, or its equivalent, with client, registration as its argument." // TODO(catalinb): We don't implement the force bypass cache flag. // See: https://github.com/slightlyoff/ServiceWorker/issues/759 RefPtr queue = GetOrCreateJobQueue(scopeKey, aScope); RefPtr job = new ServiceWorkerUpdateJob( principal, registration->Scope(), newest->ScriptSpec(), registration->GetUpdateViaCache()); if (aCallback) { RefPtr cb = new UpdateJobCallback(aCallback); job->AppendResultCallback(cb); } queue->ScheduleJob(job); } void ServiceWorkerManager::Update( nsIPrincipal* aPrincipal, const nsACString& aScope, ServiceWorkerUpdateFinishCallback* aCallback) { MOZ_ASSERT(NS_IsMainThread()); if (ServiceWorkerParentInterceptEnabled()) { UpdateInternal(aPrincipal, aScope, aCallback); return; } RefPtr promise = new GenericPromise::Private(__func__); RefPtr successRunnable = new UpdateRunnable( aPrincipal, aScope, aCallback, UpdateRunnable::eSuccess, promise); RefPtr failureRunnable = new UpdateRunnable( aPrincipal, aScope, aCallback, UpdateRunnable::eFailure, promise); ServiceWorkerUpdaterChild* actor = new ServiceWorkerUpdaterChild(promise, successRunnable, failureRunnable); mActor->SendPServiceWorkerUpdaterConstructor( actor, aPrincipal->OriginAttributesRef(), nsCString(aScope)); } void ServiceWorkerManager::UpdateInternal( nsIPrincipal* aPrincipal, const nsACString& aScope, ServiceWorkerUpdateFinishCallback* aCallback) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aCallback); nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RefPtr registration = GetRegistration(scopeKey, aScope); if (NS_WARN_IF(!registration)) { ErrorResult error; error.ThrowTypeError( NS_ConvertUTF8toUTF16(aScope), NS_LITERAL_STRING("uninstalled")); aCallback->UpdateFailed(error); // In case the callback does not consume the exception error.SuppressException(); return; } RefPtr newest = registration->Newest(); MOZ_DIAGNOSTIC_ASSERT(newest, "The Update algorithm should have been aborted already " "if there wasn't a newest worker"); RefPtr queue = GetOrCreateJobQueue(scopeKey, aScope); // "Invoke Update algorithm, or its equivalent, with client, registration as // its argument." RefPtr job = new ServiceWorkerUpdateJob( aPrincipal, registration->Scope(), newest->ScriptSpec(), registration->GetUpdateViaCache()); RefPtr cb = new UpdateJobCallback(aCallback); job->AppendResultCallback(cb); queue->ScheduleJob(job); } RefPtr ServiceWorkerManager::MaybeClaimClient( const ClientInfo& aClientInfo, ServiceWorkerRegistrationInfo* aWorkerRegistration) { MOZ_DIAGNOSTIC_ASSERT(aWorkerRegistration); if (!aWorkerRegistration->GetActive()) { return GenericPromise::CreateAndReject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); } // Same origin check nsCOMPtr principal(aClientInfo.GetPrincipal()); if (!aWorkerRegistration->Principal()->Equals(principal)) { return GenericPromise::CreateAndReject(NS_ERROR_DOM_SECURITY_ERR, __func__); } // The registration that should be controlling the client RefPtr matchingRegistration = GetServiceWorkerRegistrationInfo(aClientInfo); // The registration currently controlling the client RefPtr controllingRegistration; GetClientRegistration(aClientInfo, getter_AddRefs(controllingRegistration)); if (aWorkerRegistration != matchingRegistration || aWorkerRegistration == controllingRegistration) { return GenericPromise::CreateAndResolve(true, __func__); } return StartControllingClient(aClientInfo, aWorkerRegistration); } RefPtr ServiceWorkerManager::MaybeClaimClient( const ClientInfo& aClientInfo, const ServiceWorkerDescriptor& aServiceWorker) { nsCOMPtr principal = aServiceWorker.GetPrincipal(); if (!principal) { return GenericPromise::CreateAndResolve(false, __func__); } RefPtr registration = GetRegistration(principal, aServiceWorker.Scope()); // While ServiceWorkerManager is distributed across child processes its // possible for us to sometimes get a claim for a new worker that has // not propagated to this process yet. For now, simply note that we // are done. The fix for this is to move the SWM to the parent process // so there are no consistency errors. if (NS_WARN_IF(!registration) || NS_WARN_IF(!registration->GetActive())) { return GenericPromise::CreateAndResolve(false, __func__); } return MaybeClaimClient(aClientInfo, registration); } void ServiceWorkerManager::SetSkipWaitingFlag(nsIPrincipal* aPrincipal, const nsCString& aScope, uint64_t aServiceWorkerID) { RefPtr registration = GetRegistration(aPrincipal, aScope); if (NS_WARN_IF(!registration)) { return; } RefPtr worker = registration->GetServiceWorkerInfoById(aServiceWorkerID); if (NS_WARN_IF(!worker)) { return; } worker->SetSkipWaitingFlag(); if (worker->State() == ServiceWorkerState::Installed) { registration->TryToActivateAsync(); } } void ServiceWorkerManager::UpdateClientControllers( ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(NS_IsMainThread()); RefPtr activeWorker = aRegistration->GetActive(); MOZ_DIAGNOSTIC_ASSERT(activeWorker); AutoTArray, 16> handleList; for (auto iter = mControlledClients.Iter(); !iter.Done(); iter.Next()) { if (iter.UserData()->mRegistrationInfo != aRegistration) { continue; } handleList.AppendElement(iter.UserData()->mClientHandle); } // Fire event after iterating mControlledClients is done to prevent // modification by reentering from the event handlers during iteration. for (auto& handle : handleList) { RefPtr p = handle->Control(activeWorker->Descriptor()); RefPtr self = this; // If we fail to control the client, then automatically remove it // from our list of controlled clients. p->Then( SystemGroup::EventTargetFor(TaskCategory::Other), __func__, [](bool) { // do nothing on success }, [self, clientInfo = handle->Info()](nsresult aRv) { // failed to control, forget about this client self->StopControllingClient(clientInfo); }); } } already_AddRefed ServiceWorkerManager::GetRegistration(nsIPrincipal* aPrincipal, const nsACString& aScope) const { MOZ_ASSERT(aPrincipal); nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } return GetRegistration(scopeKey, aScope); } already_AddRefed ServiceWorkerManager::GetRegistration(const PrincipalInfo& aPrincipalInfo, const nsACString& aScope) const { nsAutoCString scopeKey; nsresult rv = PrincipalInfoToScopeKey(aPrincipalInfo, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } return GetRegistration(scopeKey, aScope); } NS_IMETHODIMP ServiceWorkerManager::GetRegistrationByPrincipal( nsIPrincipal* aPrincipal, const nsAString& aScope, nsIServiceWorkerRegistrationInfo** aInfo) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aInfo); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } RefPtr info = GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI); if (!info) { return NS_ERROR_FAILURE; } info.forget(aInfo); return NS_OK; } already_AddRefed ServiceWorkerManager::GetRegistration(const nsACString& aScopeKey, const nsACString& aScope) const { RefPtr reg; RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(aScopeKey, &data)) { return reg.forget(); } data->mInfos.Get(aScope, getter_AddRefs(reg)); return reg.forget(); } already_AddRefed ServiceWorkerManager::CreateNewRegistration( const nsCString& aScope, nsIPrincipal* aPrincipal, ServiceWorkerUpdateViaCache aUpdateViaCache) { #ifdef DEBUG MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); MOZ_ASSERT(NS_SUCCEEDED(rv)); RefPtr tmp = GetRegistration(aPrincipal, aScope); MOZ_ASSERT(!tmp); #endif RefPtr registration = new ServiceWorkerRegistrationInfo(aScope, aPrincipal, aUpdateViaCache); // From now on ownership of registration is with // mServiceWorkerRegistrationInfos. AddScopeAndRegistration(aScope, registration); return registration.forget(); } void ServiceWorkerManager::MaybeRemoveRegistration( ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(aRegistration); RefPtr newest = aRegistration->Newest(); if (!newest && HasScope(aRegistration->Principal(), aRegistration->Scope())) { RemoveRegistration(aRegistration); } } void ServiceWorkerManager::RemoveRegistration( ServiceWorkerRegistrationInfo* aRegistration) { // Note, we do not need to call mActor->SendUnregister() here. There are a // few ways we can get here: 1) Through a normal unregister which calls // SendUnregister() in the // unregister job Start() method. // 2) Through origin storage being purged. These result in ForceUnregister() // starting unregister jobs which in turn call SendUnregister(). // 3) Through the failure to install a new service worker. Since we don't // store the registration until install succeeds, we do not need to call // SendUnregister here. MOZ_ASSERT(HasScope(aRegistration->Principal(), aRegistration->Scope())); RemoveScopeAndRegistration(aRegistration); } NS_IMETHODIMP ServiceWorkerManager::GetAllRegistrations(nsIArray** aResult) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr array(do_CreateInstance(NS_ARRAY_CONTRACTID)); if (!array) { return NS_ERROR_OUT_OF_MEMORY; } for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { for (auto it2 = it1.UserData()->mInfos.Iter(); !it2.Done(); it2.Next()) { ServiceWorkerRegistrationInfo* reg = it2.UserData(); MOZ_ASSERT(reg); array->AppendElement(reg); } } array.forget(aResult); return NS_OK; } // MUST ONLY BE CALLED FROM Remove(), RemoveAll() and RemoveAllRegistrations()! void ServiceWorkerManager::ForceUnregister( RegistrationDataPerPrincipal* aRegistrationData, ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(aRegistrationData); MOZ_ASSERT(aRegistration); RefPtr queue; aRegistrationData->mJobQueues.Get(aRegistration->Scope(), getter_AddRefs(queue)); if (queue) { queue->CancelAll(); } if (auto entry = aRegistrationData->mUpdateTimers.Lookup(aRegistration->Scope())) { entry.Data()->Cancel(); entry.Remove(); } // Since Unregister is async, it is ok to call it in an enumeration. Unregister(aRegistration->Principal(), nullptr, NS_ConvertUTF8toUTF16(aRegistration->Scope())); } void ServiceWorkerManager::Remove(const nsACString& aHost) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); if (NS_WARN_IF(!tldService)) { return; } for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { ServiceWorkerRegistrationInfo* reg = it2.UserData(); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), it2.Key()); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } nsAutoCString host; rv = scopeURI->GetHost(host); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } // This way subdomains are also cleared. bool hasRootDomain = false; rv = tldService->HasRootDomain(host, aHost, &hasRootDomain); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } if (hasRootDomain) { ForceUnregister(data, reg); } } } } void ServiceWorkerManager::PropagateRemove(const nsACString& aHost) { MOZ_ASSERT(NS_IsMainThread()); mActor->SendPropagateRemove(nsCString(aHost)); } void ServiceWorkerManager::RemoveAll() { MOZ_ASSERT(NS_IsMainThread()); for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { ServiceWorkerRegistrationInfo* reg = it2.UserData(); ForceUnregister(data, reg); } } } void ServiceWorkerManager::PropagateRemoveAll() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(XRE_IsParentProcess()); mActor->SendPropagateRemoveAll(); } void ServiceWorkerManager::RemoveAllRegistrations( OriginAttributesPattern* aPattern) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPattern); for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); // We can use iteration because ForceUnregister (and Unregister) are // async. Otherwise doing some R/W operations on an hashtable during // iteration will crash. for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { ServiceWorkerRegistrationInfo* reg = it2.UserData(); MOZ_ASSERT(reg); MOZ_ASSERT(reg->Principal()); bool matches = aPattern->Matches(reg->Principal()->OriginAttributesRef()); if (!matches) { continue; } ForceUnregister(data, reg); } } } NS_IMETHODIMP ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener) { MOZ_ASSERT(NS_IsMainThread()); if (!aListener || mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.AppendElement(aListener); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::RemoveListener( nsIServiceWorkerManagerListener* aListener) { MOZ_ASSERT(NS_IsMainThread()); if (!aListener || !mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.RemoveElement(aListener); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (strcmp(aTopic, CLEAR_ORIGIN_DATA) == 0) { MOZ_ASSERT(XRE_IsParentProcess()); OriginAttributesPattern pattern; MOZ_ALWAYS_TRUE(pattern.Init(nsAutoString(aData))); RemoveAllRegistrations(&pattern); return NS_OK; } if (strcmp(aTopic, GetXPCOMShutdownTopic()) == 0) { MaybeStartShutdown(); return NS_OK; } MOZ_CRASH("Received message we aren't supposed to be registered for!"); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::PropagateSoftUpdate( JS::Handle aOriginAttributes, const nsAString& aScope, JSContext* aCx) { MOZ_ASSERT(NS_IsMainThread()); OriginAttributes attrs; if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } PropagateSoftUpdate(attrs, aScope); return NS_OK; } void ServiceWorkerManager::PropagateSoftUpdate( const OriginAttributes& aOriginAttributes, const nsAString& aScope) { MOZ_ASSERT(NS_IsMainThread()); mActor->SendPropagateSoftUpdate(aOriginAttributes, nsString(aScope)); } NS_IMETHODIMP ServiceWorkerManager::PropagateUnregister( nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback, const nsAString& aScope) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); PrincipalInfo principalInfo; if (NS_WARN_IF( NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) { return NS_ERROR_FAILURE; } mActor->SendPropagateUnregister(principalInfo, nsString(aScope)); nsresult rv = Unregister(aPrincipal, aCallback, aScope); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void ServiceWorkerManager::NotifyListenersOnRegister( nsIServiceWorkerRegistrationInfo* aInfo) { nsTArray> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnRegister(aInfo); } } void ServiceWorkerManager::NotifyListenersOnUnregister( nsIServiceWorkerRegistrationInfo* aInfo) { nsTArray> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnUnregister(aInfo); } } class UpdateTimerCallback final : public nsITimerCallback, public nsINamed { nsCOMPtr mPrincipal; const nsCString mScope; ~UpdateTimerCallback() {} public: UpdateTimerCallback(nsIPrincipal* aPrincipal, const nsACString& aScope) : mPrincipal(aPrincipal), mScope(aScope) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mPrincipal); MOZ_ASSERT(!mScope.IsEmpty()); } NS_IMETHOD Notify(nsITimer* aTimer) override { MOZ_ASSERT(NS_IsMainThread()); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { // shutting down, do nothing return NS_OK; } swm->UpdateTimerFired(mPrincipal, mScope); return NS_OK; } NS_IMETHOD GetName(nsACString& aName) override { aName.AssignLiteral("UpdateTimerCallback"); return NS_OK; } NS_DECL_ISUPPORTS }; NS_IMPL_ISUPPORTS(UpdateTimerCallback, nsITimerCallback, nsINamed) bool ServiceWorkerManager::MayHaveActiveServiceWorkerInstance( ContentParent* aContent, nsIPrincipal* aPrincipal) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); if (mShuttingDown) { return false; } nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(scopeKey, &data)) { return false; } return true; } void ServiceWorkerManager::ScheduleUpdateTimer(nsIPrincipal* aPrincipal, const nsACString& aScope) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(!aScope.IsEmpty()); if (mShuttingDown) { return; } nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(scopeKey, &data)) { return; } nsCOMPtr& timer = data->mUpdateTimers.GetOrInsert(aScope); if (timer) { // There is already a timer scheduled. In this case just use the original // schedule time. We don't want to push it out to a later time since that // could allow updates to be starved forever if events are continuously // fired. return; } nsCOMPtr callback = new UpdateTimerCallback(aPrincipal, aScope); const uint32_t UPDATE_DELAY_MS = 1000; // Label with SystemGroup because UpdateTimerCallback only sends an IPC // message (PServiceWorkerUpdaterConstructor) without touching any web // contents. rv = NS_NewTimerWithCallback( getter_AddRefs(timer), callback, UPDATE_DELAY_MS, nsITimer::TYPE_ONE_SHOT, SystemGroup::EventTargetFor(TaskCategory::Other)); if (NS_WARN_IF(NS_FAILED(rv))) { data->mUpdateTimers.Remove(aScope); // another lookup, but very rare return; } } void ServiceWorkerManager::UpdateTimerFired(nsIPrincipal* aPrincipal, const nsACString& aScope) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(!aScope.IsEmpty()); if (mShuttingDown) { return; } // First cleanup the timer. nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(scopeKey, &data)) { return; } if (auto entry = data->mUpdateTimers.Lookup(aScope)) { entry.Data()->Cancel(); entry.Remove(); } RefPtr registration; data->mInfos.Get(aScope, getter_AddRefs(registration)); if (!registration) { return; } if (!registration->CheckAndClearIfUpdateNeeded()) { return; } OriginAttributes attrs = aPrincipal->OriginAttributesRef(); SoftUpdate(attrs, aScope); } void ServiceWorkerManager::MaybeSendUnregister(nsIPrincipal* aPrincipal, const nsACString& aScope) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(!aScope.IsEmpty()); if (!mActor) { return; } PrincipalInfo principalInfo; nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return; } Unused << mActor->SendUnregister(principalInfo, NS_ConvertUTF8toUTF16(aScope)); } NS_IMETHODIMP ServiceWorkerManager::IsParentInterceptEnabled(bool* aIsEnabled) { MOZ_ASSERT(NS_IsMainThread()); *aIsEnabled = ServiceWorkerParentInterceptEnabled(); return NS_OK; } } // namespace dom } // namespace mozilla