/* -*- 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 "nsIDOMEventTarget.h" #include "nsIDocument.h" #include "nsIScriptSecurityManager.h" #include "nsIStreamLoader.h" #include "nsIHttpChannel.h" #include "nsIHttpChannelInternal.h" #include "nsIHttpHeaderVisitor.h" #include "nsINetworkInterceptController.h" #include "nsIMutableArray.h" #include "nsIScriptError.h" #include "nsISimpleEnumerator.h" #include "nsITimer.h" #include "nsIUploadChannel2.h" #include "nsPIDOMWindow.h" #include "nsScriptLoader.h" #include "nsServiceManagerUtils.h" #include "nsDebug.h" #include "nsISupportsPrimitives.h" #include "jsapi.h" #include "mozilla/BasePrincipal.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/ErrorNames.h" #include "mozilla/LoadContext.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/DOMError.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/ipc/BackgroundChild.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "mozilla/Unused.h" #include "mozilla/EnumSet.h" #include "nsContentPolicyUtils.h" #include "nsContentSecurityManager.h" #include "nsContentUtils.h" #include "nsGlobalWindow.h" #include "nsNetUtil.h" #include "nsProxyRelease.h" #include "nsQueryObject.h" #include "nsTArray.h" #include "RuntimeService.h" #include "ServiceWorker.h" #include "ServiceWorkerClient.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 "ServiceWorkerEvents.h" #include "ServiceWorkerUnregisterJob.h" #include "ServiceWorkerUpdateJob.h" #include "SharedWorker.h" #include "WorkerInlines.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "WorkerScope.h" #ifdef PostMessage #undef PostMessage #endif using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::ipc; BEGIN_WORKERS_NAMESPACE #define PURGE_DOMAIN_DATA "browser:purge-domain-data" #define PURGE_SESSION_HISTORY "browser:purge-session-history" #define CLEAR_ORIGIN_DATA "clear-origin-attributes-data" static_assert(nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN == static_cast<uint32_t>(RequestMode::Same_origin), "RequestMode enumeration value should match Necko CORS mode value."); static_assert(nsIHttpChannelInternal::CORS_MODE_NO_CORS == static_cast<uint32_t>(RequestMode::No_cors), "RequestMode enumeration value should match Necko CORS mode value."); static_assert(nsIHttpChannelInternal::CORS_MODE_CORS == static_cast<uint32_t>(RequestMode::Cors), "RequestMode enumeration value should match Necko CORS mode value."); static_assert(nsIHttpChannelInternal::CORS_MODE_NAVIGATE == static_cast<uint32_t>(RequestMode::Navigate), "RequestMode enumeration value should match Necko CORS mode value."); static_assert(nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW == static_cast<uint32_t>(RequestRedirect::Follow), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert(nsIHttpChannelInternal::REDIRECT_MODE_ERROR == static_cast<uint32_t>(RequestRedirect::Error), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert(nsIHttpChannelInternal::REDIRECT_MODE_MANUAL == static_cast<uint32_t>(RequestRedirect::Manual), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert(3 == static_cast<uint32_t>(RequestRedirect::EndGuard_), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT == static_cast<uint32_t>(RequestCache::Default), "RequestCache enumeration value should match Necko Cache mode value."); static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE == static_cast<uint32_t>(RequestCache::No_store), "RequestCache enumeration value should match Necko Cache mode value."); static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD == static_cast<uint32_t>(RequestCache::Reload), "RequestCache enumeration value should match Necko Cache mode value."); static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE == static_cast<uint32_t>(RequestCache::No_cache), "RequestCache enumeration value should match Necko Cache mode value."); static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE == static_cast<uint32_t>(RequestCache::Force_cache), "RequestCache enumeration value should match Necko Cache mode value."); static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED == static_cast<uint32_t>(RequestCache::Only_if_cached), "RequestCache enumeration value should match Necko Cache mode value."); static_assert(6 == static_cast<uint32_t>(RequestCache::EndGuard_), "RequestCache enumeration value should match Necko Cache mode value."); static StaticRefPtr<ServiceWorkerManager> 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<nsCString> mOrderedScopes; // Scope to registration. // The scope should be a fully qualified valid URL. nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerRegistrationInfo> mInfos; // Maps scopes to job queues. nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerJobQueue> mJobQueues; // Map scopes to scheduled update timers. nsInterfaceHashtable<nsCStringHashKey, nsITimer> mUpdateTimers; }; namespace { nsresult PopulateRegistrationData(nsIPrincipal* aPrincipal, const ServiceWorkerRegistrationInfo* aRegistration, ServiceWorkerRegistrationData& aData) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aRegistration); if (NS_WARN_IF(!BasePrincipal::Cast(aPrincipal)->IsCodebasePrincipal())) { return NS_ERROR_FAILURE; } nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aData.principal()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } aData.scope() = aRegistration->mScope; RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); if (NS_WARN_IF(!newest)) { return NS_ERROR_FAILURE; } if (aRegistration->GetActive()) { aData.currentWorkerURL() = aRegistration->GetActive()->ScriptSpec(); aData.cacheName() = aRegistration->GetActive()->CacheName(); } return NS_OK; } class TeardownRunnable final : public Runnable { public: explicit TeardownRunnable(ServiceWorkerManagerChild* aActor) : mActor(aActor) { MOZ_ASSERT(mActor); } NS_IMETHOD Run() override { MOZ_ASSERT(mActor); mActor->SendShutdown(); return NS_OK; } private: ~TeardownRunnable() {} RefPtr<ServiceWorkerManagerChild> mActor; }; } // namespace ////////////////////////// // ServiceWorkerManager // ////////////////////////// NS_IMPL_ADDREF(ServiceWorkerManager) NS_IMPL_RELEASE(ServiceWorkerManager) NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager) NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager) NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback) 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(); MOZ_ASSERT(!mActor); } void ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) { nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); if (obs) { DebugOnly<nsresult> rv; rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); } if (XRE_IsParentProcess()) { MOZ_DIAGNOSTIC_ASSERT(aRegistrar); nsTArray<ServiceWorkerRegistrationData> data; aRegistrar->GetRegistrations(data); LoadRegistrations(data); if (obs) { DebugOnly<nsresult> rv; rv = obs->AddObserver(this, PURGE_SESSION_HISTORY, false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = obs->AddObserver(this, PURGE_DOMAIN_DATA, false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = obs->AddObserver(this, CLEAR_ORIGIN_DATA, false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } if (!BackgroundChild::GetOrCreateForCurrentThread(this)) { // Make sure to do this last as our failure cleanup expects Init() to have // executed. ActorFailed(); } } 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<nsITimer> timer = it2.UserData(); timer->Cancel(); } it1.UserData()->mUpdateTimers.Clear(); for (auto it2 = it1.UserData()->mJobQueues.Iter(); !it2.Done(); it2.Next()) { RefPtr<ServiceWorkerJobQueue> queue = it2.UserData(); queue->CancelAll(); } it1.UserData()->mJobQueues.Clear(); } nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); if (XRE_IsParentProcess()) { obs->RemoveObserver(this, PURGE_SESSION_HISTORY); obs->RemoveObserver(this, PURGE_DOMAIN_DATA); obs->RemoveObserver(this, CLEAR_ORIGIN_DATA); } } mPendingOperations.Clear(); if (!mActor) { return; } mActor->ManagerShuttingDown(); RefPtr<TeardownRunnable> 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<nsPIDOMWindowInner> mWindow; // The promise "returned" by the call to Update up to // navigator.serviceWorker.register(). RefPtr<Promise> mPromise; ~ServiceWorkerResolveWindowPromiseOnRegisterCallback() {} virtual void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override { AssertIsOnMainThread(); MOZ_ASSERT(aJob); if (aStatus.Failed()) { mPromise->MaybeReject(aStatus); return; } MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Register); RefPtr<ServiceWorkerRegisterJob> registerJob = static_cast<ServiceWorkerRegisterJob*>(aJob); RefPtr<ServiceWorkerRegistrationInfo> reg = registerJob->GetRegistration(); RefPtr<ServiceWorkerRegistration> swr = mWindow->GetServiceWorkerRegistration(NS_ConvertUTF8toUTF16(reg->mScope)); mPromise->MaybeResolve(swr); } public: ServiceWorkerResolveWindowPromiseOnRegisterCallback(nsPIDOMWindowInner* aWindow, Promise* aPromise) : mWindow(aWindow) , mPromise(aPromise) {} NS_INLINE_DECL_REFCOUNTING(ServiceWorkerResolveWindowPromiseOnRegisterCallback, override) }; namespace { class PropagateSoftUpdateRunnable final : public Runnable { public: PropagateSoftUpdateRunnable(const PrincipalOriginAttributes& aOriginAttributes, const nsAString& aScope) : mOriginAttributes(aOriginAttributes) , mScope(aScope) {} NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (swm) { swm->PropagateSoftUpdate(mOriginAttributes, mScope); } return NS_OK; } private: ~PropagateSoftUpdateRunnable() {} const PrincipalOriginAttributes mOriginAttributes; const nsString mScope; }; class PropagateUnregisterRunnable final : public Runnable { public: PropagateUnregisterRunnable(nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback, const nsAString& aScope) : mPrincipal(aPrincipal) , mCallback(aCallback) , mScope(aScope) { MOZ_ASSERT(aPrincipal); } NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (swm) { swm->PropagateUnregister(mPrincipal, mCallback, mScope); } return NS_OK; } private: ~PropagateUnregisterRunnable() {} nsCOMPtr<nsIPrincipal> mPrincipal; nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback; const nsString mScope; }; class RemoveRunnable final : public Runnable { public: explicit RemoveRunnable(const nsACString& aHost) {} NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (swm) { swm->Remove(mHost); } return NS_OK; } private: ~RemoveRunnable() {} const nsCString mHost; }; class PropagateRemoveRunnable final : public Runnable { public: explicit PropagateRemoveRunnable(const nsACString& aHost) {} NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (swm) { swm->PropagateRemove(mHost); } return NS_OK; } private: ~PropagateRemoveRunnable() {} const nsCString mHost; }; class PropagateRemoveAllRunnable final : public Runnable { public: PropagateRemoveAllRunnable() {} NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (swm) { swm->PropagateRemoveAll(); } return NS_OK; } private: ~PropagateRemoveAllRunnable() {} }; } // namespace // This function implements parts of the step 3 of the following algorithm: // https://w3c.github.io/webappsec/specs/powerfulfeatures/#settings-secure static bool IsFromAuthenticatedOrigin(nsIDocument* aDoc) { MOZ_ASSERT(aDoc); nsCOMPtr<nsIDocument> doc(aDoc); nsCOMPtr<nsIContentSecurityManager> csm = do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID); if (NS_WARN_IF(!csm)) { return false; } while (doc && !nsContentUtils::IsChromeDoc(doc)) { bool trustworthyOrigin = false; // The origin of the document may be different from the document URI // itself. Check the principal, not the document URI itself. nsCOMPtr<nsIPrincipal> documentPrincipal = doc->NodePrincipal(); // The check for IsChromeDoc() above should mean we never see a system // principal inside the loop. MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(documentPrincipal)); csm->IsOriginPotentiallyTrustworthy(documentPrincipal, &trustworthyOrigin); if (!trustworthyOrigin) { return false; } doc = doc->GetParentDocument(); } return true; } // If we return an error code here, the ServiceWorkerContainer will // automatically reject the Promise. NS_IMETHODIMP ServiceWorkerManager::Register(mozIDOMWindow* aWindow, nsIURI* aScopeURI, nsIURI* aScriptURI, nsISupports** aPromise) { AssertIsOnMainThread(); if (NS_WARN_IF(!aWindow)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } auto* window = nsPIDOMWindowInner::From(aWindow); nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); if (!doc) { return NS_ERROR_FAILURE; } // Don't allow service workers to register when the *document* is chrome. if (NS_WARN_IF(nsContentUtils::IsSystemPrincipal(doc->NodePrincipal()))) { return NS_ERROR_DOM_SECURITY_ERR; } nsCOMPtr<nsPIDOMWindowOuter> outerWindow = window->GetOuterWindow(); bool serviceWorkersTestingEnabled = outerWindow->GetServiceWorkersTestingEnabled(); bool authenticatedOrigin; if (Preferences::GetBool("dom.serviceWorkers.testing.enabled") || serviceWorkersTestingEnabled) { authenticatedOrigin = true; } else { authenticatedOrigin = IsFromAuthenticatedOrigin(doc); } if (!authenticatedOrigin) { NS_WARNING("ServiceWorker registration from insecure websites is not allowed."); return NS_ERROR_DOM_SECURITY_ERR; } // Data URLs are not allowed. nsCOMPtr<nsIPrincipal> documentPrincipal = doc->NodePrincipal(); nsresult rv = documentPrincipal->CheckMayLoad(aScriptURI, true /* report */, false /* allowIfInheritsPrincipal */); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } // Check content policy. int16_t decision = nsIContentPolicy::ACCEPT; rv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER, aScriptURI, documentPrincipal, doc, EmptyCString(), nullptr, &decision); NS_ENSURE_SUCCESS(rv, rv); if (NS_WARN_IF(decision != nsIContentPolicy::ACCEPT)) { return NS_ERROR_CONTENT_BLOCKED; } rv = documentPrincipal->CheckMayLoad(aScopeURI, true /* report */, false /* allowIfInheritsPrinciple */); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } // The IsOriginPotentiallyTrustworthy() check allows file:// and possibly other // URI schemes. We need to explicitly only allows http and https schemes. // Note, we just use the aScriptURI here for the check since its already // been verified as same origin with the document principal. This also // is a good block against accidentally allowing blob: script URIs which // might inherit the origin. bool isHttp = false; bool isHttps = false; aScriptURI->SchemeIs("http", &isHttp); aScriptURI->SchemeIs("https", &isHttps); if (NS_WARN_IF(!isHttp && !isHttps)) { return NS_ERROR_DOM_SECURITY_ERR; } nsCString cleanedScope; rv = aScopeURI->GetSpecIgnoringRef(cleanedScope); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } nsAutoCString spec; rv = aScriptURI->GetSpecIgnoringRef(spec); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); ErrorResult result; RefPtr<Promise> promise = Promise::Create(sgo, result); if (result.Failed()) { return result.StealNSResult(); } nsAutoCString scopeKey; rv = PrincipalToScopeKey(documentPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } AddRegisteringDocument(cleanedScope, doc); RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, cleanedScope); RefPtr<ServiceWorkerResolveWindowPromiseOnRegisterCallback> cb = new ServiceWorkerResolveWindowPromiseOnRegisterCallback(window, promise); nsCOMPtr<nsILoadGroup> docLoadGroup = doc->GetDocumentLoadGroup(); RefPtr<WorkerLoadInfo::InterfaceRequestor> ir = new WorkerLoadInfo::InterfaceRequestor(documentPrincipal, docLoadGroup); ir->MaybeAddTabChild(docLoadGroup); // Create a load group that is separate from, yet related to, the document's load group. // This allows checks for interfaces like nsILoadContext to yield the values used by the // the document, yet will not cancel the update job if the document's load group is cancelled. nsCOMPtr<nsILoadGroup> loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); MOZ_ALWAYS_SUCCEEDS(loadGroup->SetNotificationCallbacks(ir)); RefPtr<ServiceWorkerRegisterJob> job = new ServiceWorkerRegisterJob(documentPrincipal, cleanedScope, spec, loadGroup); job->AppendResultCallback(cb); queue->ScheduleJob(job); AssertIsOnMainThread(); promise.forget(aPromise); return NS_OK; } void ServiceWorkerManager::AppendPendingOperation(nsIRunnable* aRunnable) { MOZ_ASSERT(!mActor); MOZ_ASSERT(aRunnable); if (!mShuttingDown) { mPendingOperations.AppendElement(aRunnable); } } /* * Implements the async aspects of the getRegistrations algorithm. */ class GetRegistrationsRunnable final : public Runnable { nsCOMPtr<nsPIDOMWindowInner> mWindow; RefPtr<Promise> mPromise; public: GetRegistrationsRunnable(nsPIDOMWindowInner* aWindow, Promise* aPromise) : mWindow(aWindow), mPromise(aPromise) {} NS_IMETHOD Run() override { RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (!swm) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsIDocument* doc = mWindow->GetExtantDoc(); if (!doc) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI(); if (!docURI) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); if (!principal) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsTArray<RefPtr<ServiceWorkerRegistration>> array; if (NS_WARN_IF(!BasePrincipal::Cast(principal)->IsCodebasePrincipal())) { 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)) { mPromise->MaybeResolve(array); return NS_OK; } for (uint32_t i = 0; i < data->mOrderedScopes.Length(); ++i) { RefPtr<ServiceWorkerRegistrationInfo> info = data->mInfos.GetWeak(data->mOrderedScopes[i]); if (info->mPendingUninstall) { continue; } NS_ConvertUTF8toUTF16 scope(data->mOrderedScopes[i]); nsCOMPtr<nsIURI> scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { mPromise->MaybeReject(rv); break; } rv = principal->CheckMayLoad(scopeURI, true /* report */, false /* allowIfInheritsPrincipal */); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } RefPtr<ServiceWorkerRegistration> swr = mWindow->GetServiceWorkerRegistration(scope); array.AppendElement(swr); } mPromise->MaybeResolve(array); return NS_OK; } }; // If we return an error code here, the ServiceWorkerContainer will // automatically reject the Promise. NS_IMETHODIMP ServiceWorkerManager::GetRegistrations(mozIDOMWindow* aWindow, nsISupports** aPromise) { AssertIsOnMainThread(); if (NS_WARN_IF(!aWindow)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } auto* window = nsPIDOMWindowInner::From(aWindow); nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } // Don't allow service workers to register when the *document* is chrome for // now. MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); ErrorResult result; RefPtr<Promise> promise = Promise::Create(sgo, result); if (result.Failed()) { return result.StealNSResult(); } nsCOMPtr<nsIRunnable> runnable = new GetRegistrationsRunnable(window, promise); promise.forget(aPromise); return NS_DispatchToCurrentThread(runnable); } /* * Implements the async aspects of the getRegistration algorithm. */ class GetRegistrationRunnable final : public Runnable { nsCOMPtr<nsPIDOMWindowInner> mWindow; RefPtr<Promise> mPromise; nsString mDocumentURL; public: GetRegistrationRunnable(nsPIDOMWindowInner* aWindow, Promise* aPromise, const nsAString& aDocumentURL) : mWindow(aWindow), mPromise(aPromise), mDocumentURL(aDocumentURL) {} NS_IMETHOD Run() override { RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (!swm) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsIDocument* doc = mWindow->GetExtantDoc(); if (!doc) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI(); if (!docURI) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr<nsIURI> uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), mDocumentURL, nullptr, docURI); if (NS_WARN_IF(NS_FAILED(rv))) { mPromise->MaybeReject(rv); return NS_OK; } nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); if (!principal) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } rv = principal->CheckMayLoad(uri, true /* report */, false /* allowIfInheritsPrinciple */); if (NS_FAILED(rv)) { mPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); return NS_OK; } RefPtr<ServiceWorkerRegistrationInfo> registration = swm->GetServiceWorkerRegistrationInfo(principal, uri); if (!registration) { mPromise->MaybeResolveWithUndefined(); return NS_OK; } NS_ConvertUTF8toUTF16 scope(registration->mScope); RefPtr<ServiceWorkerRegistration> swr = mWindow->GetServiceWorkerRegistration(scope); mPromise->MaybeResolve(swr); return NS_OK; } }; // If we return an error code here, the ServiceWorkerContainer will // automatically reject the Promise. NS_IMETHODIMP ServiceWorkerManager::GetRegistration(mozIDOMWindow* aWindow, const nsAString& aDocumentURL, nsISupports** aPromise) { AssertIsOnMainThread(); if (NS_WARN_IF(!aWindow)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } auto* window = nsPIDOMWindowInner::From(aWindow); nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } // Don't allow service workers to register when the *document* is chrome for // now. MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); ErrorResult result; RefPtr<Promise> promise = Promise::Create(sgo, result); if (result.Failed()) { return result.StealNSResult(); } nsCOMPtr<nsIRunnable> runnable = new GetRegistrationRunnable(window, promise, aDocumentURL); promise.forget(aPromise); return NS_DispatchToCurrentThread(runnable); } class GetReadyPromiseRunnable final : public Runnable { nsCOMPtr<nsPIDOMWindowInner> mWindow; RefPtr<Promise> mPromise; public: GetReadyPromiseRunnable(nsPIDOMWindowInner* aWindow, Promise* aPromise) : mWindow(aWindow), mPromise(aPromise) {} NS_IMETHOD Run() override { RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (!swm) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsIDocument* doc = mWindow->GetExtantDoc(); if (!doc) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI(); if (!docURI) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } if (!swm->CheckReadyPromise(mWindow, docURI, mPromise)) { swm->StorePendingReadyPromise(mWindow, docURI, mPromise); } return NS_OK; } }; NS_IMETHODIMP ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes, const nsACString& aScope, uint32_t aDataLength, uint8_t* aDataBytes, uint8_t optional_argc) { if (optional_argc == 2) { nsTArray<uint8_t> data; if (!data.InsertElementsAt(0, aDataBytes, aDataLength, fallible)) { return NS_ERROR_OUT_OF_MEMORY; } return SendPushEvent(aOriginAttributes, aScope, EmptyString(), Some(data)); } 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<nsTArray<uint8_t>>& aData) { PrincipalOriginAttributes 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<ServiceWorkerRegistrationInfo> registration = GetRegistration(serviceWorker->GetPrincipal(), aScope); MOZ_DIAGNOSTIC_ASSERT(registration); return serviceWorker->WorkerPrivate()->SendPushEvent(aMessageId, aData, registration); } NS_IMETHODIMP ServiceWorkerManager::SendPushSubscriptionChangeEvent(const nsACString& aOriginAttributes, const nsACString& aScope) { PrincipalOriginAttributes 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) { PrincipalOriginAttributes 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); } NS_IMETHODIMP ServiceWorkerManager::GetReadyPromise(mozIDOMWindow* aWindow, nsISupports** aPromise) { AssertIsOnMainThread(); if (NS_WARN_IF(!aWindow)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } auto* window = nsPIDOMWindowInner::From(aWindow); nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return NS_ERROR_FAILURE; } // Don't allow service workers to register when the *document* is chrome for // now. MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); MOZ_ASSERT(!mPendingReadyPromises.Contains(window)); nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); ErrorResult result; RefPtr<Promise> promise = Promise::Create(sgo, result); if (result.Failed()) { return result.StealNSResult(); } nsCOMPtr<nsIRunnable> runnable = new GetReadyPromiseRunnable(window, promise); promise.forget(aPromise); return NS_DispatchToCurrentThread(runnable); } NS_IMETHODIMP ServiceWorkerManager::RemoveReadyPromise(mozIDOMWindow* aWindow) { AssertIsOnMainThread(); MOZ_ASSERT(aWindow); if (!aWindow) { return NS_ERROR_FAILURE; } mPendingReadyPromises.Remove(aWindow); return NS_OK; } void ServiceWorkerManager::StorePendingReadyPromise(nsPIDOMWindowInner* aWindow, nsIURI* aURI, Promise* aPromise) { PendingReadyPromise* data; // We should not have 2 pending promises for the same window. MOZ_ASSERT(!mPendingReadyPromises.Get(aWindow, &data)); data = new PendingReadyPromise(aURI, aPromise); mPendingReadyPromises.Put(aWindow, data); } void ServiceWorkerManager::CheckPendingReadyPromises() { for (auto iter = mPendingReadyPromises.Iter(); !iter.Done(); iter.Next()) { nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(iter.Key()); MOZ_ASSERT(window); nsAutoPtr<PendingReadyPromise>& pendingReadyPromise = iter.Data(); if (CheckReadyPromise(window, pendingReadyPromise->mURI, pendingReadyPromise->mPromise)) { iter.Remove(); } } } bool ServiceWorkerManager::CheckReadyPromise(nsPIDOMWindowInner* aWindow, nsIURI* aURI, Promise* aPromise) { MOZ_ASSERT(aWindow); MOZ_ASSERT(aURI); nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); MOZ_ASSERT(doc); nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); MOZ_ASSERT(principal); RefPtr<ServiceWorkerRegistrationInfo> registration = GetServiceWorkerRegistrationInfo(principal, aURI); if (registration && registration->GetActive()) { NS_ConvertUTF8toUTF16 scope(registration->mScope); RefPtr<ServiceWorkerRegistration> swr = aWindow->GetServiceWorkerRegistration(scope); aPromise->MaybeResolve(swr); return true; } return false; } ServiceWorkerInfo* ServiceWorkerManager::GetActiveWorkerInfoForScope(const PrincipalOriginAttributes& aOriginAttributes, const nsACString& aScope) { AssertIsOnMainThread(); nsCOMPtr<nsIURI> scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); if (NS_FAILED(rv)) { return nullptr; } nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateCodebasePrincipal(scopeURI, aOriginAttributes); RefPtr<ServiceWorkerRegistrationInfo> registration = GetServiceWorkerRegistrationInfo(principal, scopeURI); if (!registration) { return nullptr; } return registration->GetActive(); } ServiceWorkerInfo* ServiceWorkerManager::GetActiveWorkerInfoForDocument(nsIDocument* aDocument) { AssertIsOnMainThread(); RefPtr<ServiceWorkerRegistrationInfo> registration; GetDocumentRegistration(aDocument, getter_AddRefs(registration)); if (!registration) { return nullptr; } return registration->GetActive(); } namespace { class UnregisterJobCallback final : public ServiceWorkerJob::Callback { nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback; ~UnregisterJobCallback() { } public: explicit UnregisterJobCallback(nsIServiceWorkerUnregisterCallback* aCallback) : mCallback(aCallback) { AssertIsOnMainThread(); MOZ_ASSERT(mCallback); } void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) { AssertIsOnMainThread(); MOZ_ASSERT(aJob); if (aStatus.Failed()) { mCallback->UnregisterFailed(); return; } MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Unregister); RefPtr<ServiceWorkerUnregisterJob> unregisterJob = static_cast<ServiceWorkerUnregisterJob*>(aJob); mCallback->UnregisterSucceeded(unregisterJob->GetResult()); } NS_INLINE_DECL_REFCOUNTING(UnregisterJobCallback) }; } // anonymous namespace NS_IMETHODIMP ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback, const nsAString& aScope) { AssertIsOnMainThread(); 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<nsIURI> scopeURI; rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); 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<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope); RefPtr<ServiceWorkerUnregisterJob> job = new ServiceWorkerUnregisterJob(aPrincipal, scope, true /* send to parent */); if (aCallback) { RefPtr<UnregisterJobCallback> cb = new UnregisterJobCallback(aCallback); job->AppendResultCallback(cb); } queue->ScheduleJob(job); return NS_OK; } nsresult ServiceWorkerManager::NotifyUnregister(nsIPrincipal* aPrincipal, const nsAString& aScope) { AssertIsOnMainThread(); 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<nsIURI> scopeURI; rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); 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<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope); RefPtr<ServiceWorkerUnregisterJob> job = new ServiceWorkerUnregisterJob(aPrincipal, scope, false /* send to parent */); queue->ScheduleJob(job); return NS_OK; } void ServiceWorkerManager::WorkerIsIdle(ServiceWorkerInfo* aWorker) { AssertIsOnMainThread(); MOZ_DIAGNOSTIC_ASSERT(aWorker); RefPtr<ServiceWorkerRegistrationInfo> reg = GetRegistration(aWorker->GetPrincipal(), aWorker->Scope()); if (!reg) { return; } if (reg->GetActive() != aWorker) { return; } if (!reg->IsControllingDocuments() && reg->mPendingUninstall) { RemoveRegistration(reg); return; } reg->TryToActivateAsync(); } already_AddRefed<ServiceWorkerJobQueue> ServiceWorkerManager::GetOrCreateJobQueue(const nsACString& aKey, const nsACString& aScope) { MOZ_ASSERT(!aKey.IsEmpty()); ServiceWorkerManager::RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(aKey, &data)) { data = new RegistrationDataPerPrincipal(); mRegistrationInfos.Put(aKey, data); } RefPtr<ServiceWorkerJobQueue> queue; if (!data->mJobQueues.Get(aScope, getter_AddRefs(queue))) { RefPtr<ServiceWorkerJobQueue> newQueue = new ServiceWorkerJobQueue(); queue = newQueue; data->mJobQueues.Put(aScope, newQueue.forget()); } return queue.forget(); } /* static */ already_AddRefed<ServiceWorkerManager> 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<ServiceWorkerRegistrar> swr; // Don't create the ServiceWorkerManager until the ServiceWorkerRegistrar is // initialized. if (XRE_IsParentProcess()) { swr = ServiceWorkerRegistrar::Get(); if (!swr) { return nullptr; } } firstTime = false; AssertIsOnMainThread(); gInstance = new ServiceWorkerManager(); gInstance->Init(swr); ClearOnShutdown(&gInstance); } RefPtr<ServiceWorkerManager> 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) { nsCOMPtr<nsIURI> uri; nsresult rv; if (!aFilename.IsEmpty()) { rv = NS_NewURI(getter_AddRefs(uri), aFilename); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } AutoTArray<uint64_t, 16> windows; // Report errors to every controlled document. for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { ServiceWorkerRegistrationInfo* reg = iter.UserData(); MOZ_ASSERT(reg); if (!reg->mScope.Equals(aScope)) { continue; } nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); if (!doc || !doc->IsCurrentActiveDocument() || !doc->GetWindow()) { continue; } windows.AppendElement(doc->InnerWindowID()); nsContentUtils::ReportToConsoleNonLocalized(aMessage, aFlags, NS_LITERAL_CSTRING("Service Workers"), doc, uri, aLine, aLineNumber, aColumnNumber, nsContentUtils::eOMIT_LOCATION); } // Report to any documents that have called .register() for this scope. They // may not be controlled, but will still want to see error reports. WeakDocumentList* regList = mRegisteringDocuments.Get(aScope); if (regList) { for (int32_t i = regList->Length() - 1; i >= 0; --i) { nsCOMPtr<nsIDocument> doc = do_QueryReferent(regList->ElementAt(i)); if (!doc) { regList->RemoveElementAt(i); continue; } if (!doc->IsCurrentActiveDocument()) { continue; } uint64_t innerWindowId = doc->InnerWindowID(); if (windows.Contains(innerWindowId)) { continue; } windows.AppendElement(innerWindowId); nsContentUtils::ReportToConsoleNonLocalized(aMessage, aFlags, NS_LITERAL_CSTRING("Service Workers"), doc, uri, aLine, aLineNumber, aColumnNumber, nsContentUtils::eOMIT_LOCATION); } if (regList->IsEmpty()) { regList = nullptr; nsAutoPtr<WeakDocumentList> doomed; mRegisteringDocuments.RemoveAndForget(aScope, doomed); } } InterceptionList* intList = mNavigationInterceptions.Get(aScope); if (intList) { nsIConsoleService* consoleService = nullptr; for (uint32_t i = 0; i < intList->Length(); ++i) { nsCOMPtr<nsIInterceptedChannel> channel = intList->ElementAt(i); nsCOMPtr<nsIChannel> inner; rv = channel->GetChannel(getter_AddRefs(inner)); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } uint64_t innerWindowId = nsContentUtils::GetInnerWindowID(inner); if (innerWindowId == 0 || windows.Contains(innerWindowId)) { continue; } windows.AppendElement(innerWindowId); // Unfortunately the nsContentUtils helpers don't provide a convenient // way to log to a window ID without a document. Use console service // directly. nsCOMPtr<nsIScriptError> errorObject = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return; } rv = errorObject->InitWithWindowID(aMessage, aFilename, aLine, aLineNumber, aColumnNumber, aFlags, NS_LITERAL_CSTRING("Service Workers"), innerWindowId); if (NS_WARN_IF(NS_FAILED(rv))) { return; } if (!consoleService) { rv = CallGetService(NS_CONSOLESERVICE_CONTRACTID, &consoleService); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } consoleService->LogMessage(errorObject); } } // If there are no documents to report to, at least report something to the // browser console. if (windows.IsEmpty()) { nsContentUtils::ReportToConsoleNonLocalized(aMessage, aFlags, NS_LITERAL_CSTRING("Service Workers"), nullptr, // document uri, aLine, aLineNumber, aColumnNumber, nsContentUtils::eOMIT_LOCATION); return; } } /* static */ void ServiceWorkerManager::LocalizeAndReportToAllClients( const nsCString& aScope, const char* aStringKey, const nsTArray<nsString>& aParamArray, uint32_t aFlags, const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, uint32_t aColumnNumber) { RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (!swm) { return; } nsresult rv; nsXPIDLString 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::FlushReportsToAllClients(const nsACString& aScope, nsIConsoleReportCollector* aReporter) { AutoTArray<uint64_t, 16> windows; // Report errors to every controlled document. for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { ServiceWorkerRegistrationInfo* reg = iter.UserData(); MOZ_ASSERT(reg); if (!reg->mScope.Equals(aScope)) { continue; } nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); if (!doc || !doc->IsCurrentActiveDocument() || !doc->GetWindow()) { continue; } windows.AppendElement(doc->InnerWindowID()); aReporter->FlushConsoleReports(doc, nsIConsoleReportCollector::ReportAction::Save); } // Report to any documents that have called .register() for this scope. They // may not be controlled, but will still want to see error reports. WeakDocumentList* regList = mRegisteringDocuments.Get(aScope); if (regList) { for (int32_t i = regList->Length() - 1; i >= 0; --i) { nsCOMPtr<nsIDocument> doc = do_QueryReferent(regList->ElementAt(i)); if (!doc) { regList->RemoveElementAt(i); continue; } if (!doc->IsCurrentActiveDocument()) { continue; } uint64_t innerWindowId = doc->InnerWindowID(); if (windows.Contains(innerWindowId)) { continue; } windows.AppendElement(innerWindowId); aReporter->FlushConsoleReports(doc, nsIConsoleReportCollector::ReportAction::Save); } if (regList->IsEmpty()) { regList = nullptr; nsAutoPtr<WeakDocumentList> doomed; mRegisteringDocuments.RemoveAndForget(aScope, doomed); } } nsresult rv; InterceptionList* intList = mNavigationInterceptions.Get(aScope); if (intList) { for (uint32_t i = 0; i < intList->Length(); ++i) { nsCOMPtr<nsIInterceptedChannel> channel = intList->ElementAt(i); nsCOMPtr<nsIChannel> inner; rv = channel->GetChannel(getter_AddRefs(inner)); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } uint64_t innerWindowId = nsContentUtils::GetInnerWindowID(inner); if (innerWindowId == 0 || windows.Contains(innerWindowId)) { continue; } windows.AppendElement(innerWindowId); aReporter->FlushReportsByWindowId(innerWindowId, nsIConsoleReportCollector::ReportAction::Save); } } // If there are no documents to report to, at least report something to the // browser console. if (windows.IsEmpty()) { aReporter->FlushConsoleReports((nsIDocument*)nullptr); return; } aReporter->ClearConsoleReports(); } 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) { AssertIsOnMainThread(); 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) { AssertIsOnMainThread(); nsCOMPtr<nsIPrincipal> principal = PrincipalInfoToPrincipal(aRegistration.principal()); if (!principal) { return; } RefPtr<ServiceWorkerRegistrationInfo> registration = GetRegistration(principal, aRegistration.scope()); if (!registration) { registration = CreateNewRegistration(aRegistration.scope(), principal); } else { // If active worker script matches our expectations for a "current worker", // then we are done. if (registration->GetActive() && registration->GetActive()->ScriptSpec() == aRegistration.currentWorkerURL()) { // No needs for updates. return; } } const nsCString& currentWorkerURL = aRegistration.currentWorkerURL(); if (!currentWorkerURL.IsEmpty()) { registration->SetActive( new ServiceWorkerInfo(registration->mPrincipal, registration->mScope, currentWorkerURL, aRegistration.cacheName())); registration->GetActive()->SetActivateStateUncheckedWithoutEvent(ServiceWorkerState::Activated); } } void ServiceWorkerManager::LoadRegistrations( const nsTArray<ServiceWorkerRegistrationData>& aRegistrations) { AssertIsOnMainThread(); for (uint32_t i = 0, len = aRegistrations.Length(); i < len; ++i) { LoadRegistration(aRegistrations[i]); } } void ServiceWorkerManager::ActorFailed() { MOZ_DIAGNOSTIC_ASSERT(!mActor); MaybeStartShutdown(); } void ServiceWorkerManager::ActorCreated(mozilla::ipc::PBackgroundChild* aActor) { MOZ_ASSERT(aActor); MOZ_ASSERT(!mActor); if (mShuttingDown) { MOZ_DIAGNOSTIC_ASSERT(mPendingOperations.IsEmpty()); return; } PServiceWorkerManagerChild* actor = aActor->SendPServiceWorkerManagerConstructor(); if (!actor) { ActorFailed(); return; } mActor = static_cast<ServiceWorkerManagerChild*>(actor); // Flush the pending requests. for (uint32_t i = 0, len = mPendingOperations.Length(); i < len; ++i) { MOZ_ASSERT(mPendingOperations[i]); nsresult rv = NS_DispatchToCurrentThread(mPendingOperations[i].forget()); if (NS_FAILED(rv)) { NS_WARNING("Failed to dispatch a runnable."); } } mPendingOperations.Clear(); } void ServiceWorkerManager::StoreRegistration( nsIPrincipal* aPrincipal, ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aRegistration); if (mShuttingDown) { return; } MOZ_DIAGNOSTIC_ASSERT(mActor); if (!mActor) { 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<ServiceWorkerRegistrationInfo> ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsPIDOMWindowInner* aWindow) { MOZ_ASSERT(aWindow); nsCOMPtr<nsIDocument> document = aWindow->GetExtantDoc(); return GetServiceWorkerRegistrationInfo(document); } already_AddRefed<ServiceWorkerRegistrationInfo> ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIDocument* aDoc) { MOZ_ASSERT(aDoc); nsCOMPtr<nsIURI> documentURI = aDoc->GetDocumentURI(); nsCOMPtr<nsIPrincipal> principal = aDoc->NodePrincipal(); return GetServiceWorkerRegistrationInfo(principal, documentURI); } already_AddRefed<ServiceWorkerRegistrationInfo> ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal, nsIURI* aURI) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aURI); //XXXnsm Temporary fix until Bug 1171432 is fixed. if (NS_WARN_IF(BasePrincipal::Cast(aPrincipal)->AppId() == nsIScriptSecurityManager::UNKNOWN_APP_ID)) { return nullptr; } nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_FAILED(rv)) { return nullptr; } return GetServiceWorkerRegistrationInfo(scopeKey, aURI); } already_AddRefed<ServiceWorkerRegistrationInfo> ServiceWorkerManager::GetServiceWorkerRegistrationInfo(const nsACString& aScopeKey, nsIURI* aURI) { 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<ServiceWorkerRegistrationInfo> 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->mPrincipal->GetOrigin(origin); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(origin.Equals(aScopeKey)); #endif if (registration->mPendingUninstall) { return nullptr; } return registration.forget(); } /* static */ nsresult ServiceWorkerManager::PrincipalToScopeKey(nsIPrincipal* aPrincipal, nsACString& aKey) { MOZ_ASSERT(aPrincipal); if (!BasePrincipal::Cast(aPrincipal)->IsCodebasePrincipal()) { return NS_ERROR_FAILURE; } nsresult rv = aPrincipal->GetOrigin(aKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } /* static */ void ServiceWorkerManager::AddScopeAndRegistration(const nsACString& aScope, ServiceWorkerRegistrationInfo* aInfo) { MOZ_ASSERT(aInfo); MOZ_ASSERT(aInfo->mPrincipal); RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (!swm) { // browser shutdown return; } nsAutoCString scopeKey; nsresult rv = swm->PrincipalToScopeKey(aInfo->mPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } MOZ_ASSERT(!scopeKey.IsEmpty()); RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { data = new RegistrationDataPerPrincipal(); swm->mRegistrationInfos.Put(scopeKey, data); } 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<ServiceWorkerManager> 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<ServiceWorkerManager> 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<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (!swm) { return; } nsAutoCString scopeKey; nsresult rv = swm->PrincipalToScopeKey(aRegistration->mPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { return; } nsCOMPtr<nsITimer> timer = data->mUpdateTimers.Get(aRegistration->mScope); if (timer) { timer->Cancel(); data->mUpdateTimers.Remove(aRegistration->mScope); } // The registration should generally only be removed if there are no controlled // documents, but mControlledDocuments can contain references to potentially // controlled docs. This happens when the service worker is not active yet. // We must purge these references since we are evicting the registration. for (auto iter = swm->mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { ServiceWorkerRegistrationInfo* reg = iter.UserData(); MOZ_ASSERT(reg); if (reg->mScope.Equals(aRegistration->mScope)) { iter.Remove(); } } RefPtr<ServiceWorkerRegistrationInfo> info; data->mInfos.Get(aRegistration->mScope, getter_AddRefs(info)); data->mInfos.Remove(aRegistration->mScope); data->mOrderedScopes.RemoveElement(aRegistration->mScope); swm->NotifyListenersOnUnregister(info); swm->MaybeRemoveRegistrationInfo(scopeKey); swm->NotifyServiceWorkerRegistrationRemoved(aRegistration); } void ServiceWorkerManager::MaybeRemoveRegistrationInfo(const nsACString& aScopeKey) { RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(aScopeKey, &data)) { return; } if (data->mOrderedScopes.IsEmpty() && data->mJobQueues.Count() == 0) { mRegistrationInfos.Remove(aScopeKey); } } void ServiceWorkerManager::MaybeStartControlling(nsIDocument* aDoc, const nsAString& aDocumentId) { AssertIsOnMainThread(); MOZ_ASSERT(aDoc); RefPtr<ServiceWorkerRegistrationInfo> registration = GetServiceWorkerRegistrationInfo(aDoc); if (registration) { MOZ_ASSERT(!mControlledDocuments.Contains(aDoc)); StartControllingADocument(registration, aDoc, aDocumentId); } } void ServiceWorkerManager::MaybeStopControlling(nsIDocument* aDoc) { AssertIsOnMainThread(); MOZ_ASSERT(aDoc); RefPtr<ServiceWorkerRegistrationInfo> registration; mControlledDocuments.Remove(aDoc, getter_AddRefs(registration)); // A document which was uncontrolled does not maintain that state itself, so // it will always call MaybeStopControlling() even if there isn't an // associated registration. So this check is required. if (registration) { StopControllingADocument(registration); } } void ServiceWorkerManager::MaybeCheckNavigationUpdate(nsIDocument* aDoc) { AssertIsOnMainThread(); MOZ_ASSERT(aDoc); // 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. RefPtr<ServiceWorkerRegistrationInfo> registration; mControlledDocuments.Get(aDoc, getter_AddRefs(registration)); if (registration) { registration->MaybeScheduleUpdate(); } } void ServiceWorkerManager::StartControllingADocument(ServiceWorkerRegistrationInfo* aRegistration, nsIDocument* aDoc, const nsAString& aDocumentId) { MOZ_ASSERT(aRegistration); MOZ_ASSERT(aDoc); aRegistration->StartControllingADocument(); mControlledDocuments.Put(aDoc, aRegistration); if (!aDocumentId.IsEmpty()) { aDoc->SetId(aDocumentId); } } void ServiceWorkerManager::StopControllingADocument(ServiceWorkerRegistrationInfo* aRegistration) { aRegistration->StopControllingADocument(); if (aRegistration->IsControllingDocuments() || !aRegistration->IsIdle()) { return; } if (aRegistration->mPendingUninstall) { RemoveRegistration(aRegistration); 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<nsIURI> uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } RefPtr<ServiceWorkerRegistrationInfo> r = GetServiceWorkerRegistrationInfo(aPrincipal, uri); if (!r) { return NS_ERROR_FAILURE; } aScope = NS_ConvertUTF8toUTF16(r->mScope); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::AddRegistrationEventListener(const nsAString& aScope, ServiceWorkerRegistrationListener* aListener) { AssertIsOnMainThread(); MOZ_ASSERT(aListener); #ifdef DEBUG // Ensure a registration is only listening for it's own scope. nsAutoString regScope; aListener->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); MOZ_ASSERT(aScope.Equals(regScope)); #endif MOZ_ASSERT(!mServiceWorkerRegistrationListeners.Contains(aListener)); mServiceWorkerRegistrationListeners.AppendElement(aListener); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::RemoveRegistrationEventListener(const nsAString& aScope, ServiceWorkerRegistrationListener* aListener) { AssertIsOnMainThread(); MOZ_ASSERT(aListener); #ifdef DEBUG // Ensure a registration is unregistering for it's own scope. nsAutoString regScope; aListener->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); MOZ_ASSERT(aScope.Equals(regScope)); #endif MOZ_ASSERT(mServiceWorkerRegistrationListeners.Contains(aListener)); mServiceWorkerRegistrationListeners.RemoveElement(aListener); return NS_OK; } void ServiceWorkerManager::FireUpdateFoundOnServiceWorkerRegistrations( ServiceWorkerRegistrationInfo* aRegistration) { AssertIsOnMainThread(); nsTObserverArray<ServiceWorkerRegistrationListener*>::ForwardIterator it(mServiceWorkerRegistrationListeners); while (it.HasMore()) { RefPtr<ServiceWorkerRegistrationListener> target = it.GetNext(); nsAutoString regScope; target->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); NS_ConvertUTF16toUTF8 utf8Scope(regScope); if (utf8Scope.Equals(aRegistration->mScope)) { target->UpdateFound(); } } } /* * This is used for installing, waiting and active. */ nsresult ServiceWorkerManager::GetServiceWorkerForScope(nsPIDOMWindowInner* aWindow, const nsAString& aScope, WhichServiceWorker aWhichWorker, nsISupports** aServiceWorker) { AssertIsOnMainThread(); if (NS_WARN_IF(!aWindow)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); MOZ_ASSERT(doc); /////////////////////////////////////////// // Security check nsAutoCString scope = NS_ConvertUTF16toUTF8(aScope); nsCOMPtr<nsIURI> scopeURI; // We pass nullptr as the base URI since scopes obtained from // ServiceWorkerRegistrations MUST be fully qualified URIs. nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } nsCOMPtr<nsIPrincipal> documentPrincipal = doc->NodePrincipal(); rv = documentPrincipal->CheckMayLoad(scopeURI, true /* report */, false /* allowIfInheritsPrinciple */); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } //////////////////////////////////////////// RefPtr<ServiceWorkerRegistrationInfo> registration = GetRegistration(documentPrincipal, scope); if (NS_WARN_IF(!registration)) { return NS_ERROR_FAILURE; } RefPtr<ServiceWorkerInfo> info; if (aWhichWorker == WhichServiceWorker::INSTALLING_WORKER) { info = registration->GetInstalling(); } else if (aWhichWorker == WhichServiceWorker::WAITING_WORKER) { info = registration->GetWaiting(); } else if (aWhichWorker == WhichServiceWorker::ACTIVE_WORKER) { info = registration->GetActive(); } else { MOZ_CRASH("Invalid worker type"); } if (NS_WARN_IF(!info)) { return NS_ERROR_DOM_NOT_FOUND_ERR; } RefPtr<ServiceWorker> serviceWorker = info->GetOrCreateInstance(aWindow); serviceWorker->SetState(info->State()); serviceWorker.forget(aServiceWorker); return NS_OK; } namespace { class ContinueDispatchFetchEventRunnable : public Runnable { RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; nsCOMPtr<nsIInterceptedChannel> mChannel; nsCOMPtr<nsILoadGroup> mLoadGroup; nsString mDocumentId; bool mIsReload; public: ContinueDispatchFetchEventRunnable(ServiceWorkerPrivate* aServiceWorkerPrivate, nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup, const nsAString& aDocumentId, bool aIsReload) : mServiceWorkerPrivate(aServiceWorkerPrivate) , mChannel(aChannel) , mLoadGroup(aLoadGroup) , mDocumentId(aDocumentId) , mIsReload(aIsReload) { MOZ_ASSERT(aServiceWorkerPrivate); MOZ_ASSERT(aChannel); } void HandleError() { AssertIsOnMainThread(); NS_WARNING("Unexpected error while dispatching fetch event!"); DebugOnly<nsresult> rv = mChannel->ResetInterception(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to resume intercepted network request"); } NS_IMETHOD Run() override { AssertIsOnMainThread(); nsCOMPtr<nsIChannel> 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; } rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, mDocumentId, mIsReload); if (NS_WARN_IF(NS_FAILED(rv))) { HandleError(); } return NS_OK; } }; } // anonymous namespace void ServiceWorkerManager::DispatchFetchEvent(const PrincipalOriginAttributes& aOriginAttributes, nsIDocument* aDoc, const nsAString& aDocumentIdForTopLevelNavigation, nsIInterceptedChannel* aChannel, bool aIsReload, bool aIsSubresourceLoad, ErrorResult& aRv) { MOZ_ASSERT(aChannel); AssertIsOnMainThread(); RefPtr<ServiceWorkerInfo> serviceWorker; nsCOMPtr<nsILoadGroup> loadGroup; nsAutoString documentId; if (aIsSubresourceLoad) { MOZ_ASSERT(aDoc); serviceWorker = GetActiveWorkerInfoForDocument(aDoc); if (!serviceWorker) { aRv.Throw(NS_ERROR_FAILURE); return; } loadGroup = aDoc->GetDocumentLoadGroup(); nsresult rv = aDoc->GetOrCreateId(documentId); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } else { nsCOMPtr<nsIChannel> internalChannel; aRv = aChannel->GetChannel(getter_AddRefs(internalChannel)); if (NS_WARN_IF(aRv.Failed())) { return; } internalChannel->GetLoadGroup(getter_AddRefs(loadGroup)); // TODO: Use aDocumentIdForTopLevelNavigation for potentialClientId, pending // the spec change. nsCOMPtr<nsIURI> uri; aRv = aChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri)); if (NS_WARN_IF(aRv.Failed())) { return; } // non-subresource request means the URI contains the principal nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateCodebasePrincipal(uri, aOriginAttributes); RefPtr<ServiceWorkerRegistrationInfo> registration = GetServiceWorkerRegistrationInfo(principal, uri); if (!registration) { NS_WARNING("No registration found when dispatching the fetch event"); 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 (!serviceWorker) { aRv.Throw(NS_ERROR_FAILURE); return; } AddNavigationInterception(serviceWorker->Scope(), aChannel); } if (NS_WARN_IF(aRv.Failed())) { return; } MOZ_DIAGNOSTIC_ASSERT(serviceWorker); nsCOMPtr<nsIRunnable> continueRunnable = new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(), aChannel, loadGroup, documentId, aIsReload); nsCOMPtr<nsIChannel> innerChannel; aRv = aChannel->GetChannel(getter_AddRefs(innerChannel)); if (NS_WARN_IF(aRv.Failed())) { return; } nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(innerChannel); // If there is no upload stream, then continue immediately if (!uploadChannel) { MOZ_ALWAYS_SUCCEEDS(continueRunnable->Run()); return; } // Otherwise, ensure the upload stream can be cloned directly. This may // require some async copying, so provide a callback. aRv = uploadChannel->EnsureUploadStreamIsCloneable(continueRunnable); } bool ServiceWorkerManager::IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aURI); RefPtr<ServiceWorkerRegistrationInfo> registration = GetServiceWorkerRegistrationInfo(aPrincipal, aURI); return registration && registration->GetActive(); } bool ServiceWorkerManager::IsControlled(nsIDocument* aDoc, ErrorResult& aRv) { MOZ_ASSERT(aDoc); if (nsContentUtils::IsInPrivateBrowsing(aDoc)) { // Handle the case where a service worker was previously registered in // a non-private window (bug 1255621). return false; } RefPtr<ServiceWorkerRegistrationInfo> registration; nsresult rv = GetDocumentRegistration(aDoc, getter_AddRefs(registration)); if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE)) { // It's OK to ignore the case where we don't have a registration. aRv.Throw(rv); return false; } return !!registration; } nsresult ServiceWorkerManager::GetDocumentRegistration(nsIDocument* aDoc, ServiceWorkerRegistrationInfo** aRegistrationInfo) { RefPtr<ServiceWorkerRegistrationInfo> registration; if (!mControlledDocuments.Get(aDoc, getter_AddRefs(registration))) { return NS_ERROR_NOT_AVAILABLE; } // If the document is controlled, the current worker MUST be non-null. if (!registration->GetActive()) { return NS_ERROR_NOT_AVAILABLE; } registration.forget(aRegistrationInfo); return NS_OK; } /* * The .controller is for the registration associated with the document when * the document was loaded. */ NS_IMETHODIMP ServiceWorkerManager::GetDocumentController(nsPIDOMWindowInner* aWindow, nsISupports** aServiceWorker) { if (NS_WARN_IF(!aWindow)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); if (!doc) { return NS_ERROR_DOM_INVALID_STATE_ERR; } RefPtr<ServiceWorkerRegistrationInfo> registration; nsresult rv = GetDocumentRegistration(doc, getter_AddRefs(registration)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(registration->GetActive()); RefPtr<ServiceWorker> serviceWorker = registration->GetActive()->GetOrCreateInstance(aWindow); serviceWorker.forget(aServiceWorker); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::GetInstalling(nsPIDOMWindowInner* aWindow, const nsAString& aScope, nsISupports** aServiceWorker) { return GetServiceWorkerForScope(aWindow, aScope, WhichServiceWorker::INSTALLING_WORKER, aServiceWorker); } NS_IMETHODIMP ServiceWorkerManager::GetWaiting(nsPIDOMWindowInner* aWindow, const nsAString& aScope, nsISupports** aServiceWorker) { return GetServiceWorkerForScope(aWindow, aScope, WhichServiceWorker::WAITING_WORKER, aServiceWorker); } NS_IMETHODIMP ServiceWorkerManager::GetActive(nsPIDOMWindowInner* aWindow, const nsAString& aScope, nsISupports** aServiceWorker) { return GetServiceWorkerForScope(aWindow, aScope, WhichServiceWorker::ACTIVE_WORKER, aServiceWorker); } void ServiceWorkerManager::InvalidateServiceWorkerRegistrationWorker(ServiceWorkerRegistrationInfo* aRegistration, WhichServiceWorker aWhichOnes) { AssertIsOnMainThread(); nsTObserverArray<ServiceWorkerRegistrationListener*>::ForwardIterator it(mServiceWorkerRegistrationListeners); while (it.HasMore()) { RefPtr<ServiceWorkerRegistrationListener> target = it.GetNext(); nsAutoString regScope; target->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); NS_ConvertUTF16toUTF8 utf8Scope(regScope); if (utf8Scope.Equals(aRegistration->mScope)) { target->InvalidateWorkers(aWhichOnes); } } } void ServiceWorkerManager::NotifyServiceWorkerRegistrationRemoved(ServiceWorkerRegistrationInfo* aRegistration) { AssertIsOnMainThread(); nsTObserverArray<ServiceWorkerRegistrationListener*>::ForwardIterator it(mServiceWorkerRegistrationListeners); while (it.HasMore()) { RefPtr<ServiceWorkerRegistrationListener> target = it.GetNext(); nsAutoString regScope; target->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); NS_ConvertUTF16toUTF8 utf8Scope(regScope); if (utf8Scope.Equals(aRegistration->mScope)) { target->RegistrationRemoved(); } } } void ServiceWorkerManager::SoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, const nsACString& aScope) { AssertIsOnMainThread(); if (mShuttingDown) { return; } nsCOMPtr<nsIURI> scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateCodebasePrincipal(scopeURI, aOriginAttributes); if (NS_WARN_IF(!principal)) { return; } nsAutoCString scopeKey; rv = PrincipalToScopeKey(principal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RefPtr<ServiceWorkerRegistrationInfo> registration = GetRegistration(scopeKey, aScope); if (NS_WARN_IF(!registration)) { return; } // "If registration's uninstalling flag is set, abort these steps." if (registration->mPendingUninstall) { 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<ServiceWorkerInfo> 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<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope); RefPtr<ServiceWorkerUpdateJob> job = new ServiceWorkerUpdateJob(principal, registration->mScope, newest->ScriptSpec(), nullptr); queue->ScheduleJob(job); } namespace { class UpdateJobCallback final : public ServiceWorkerJob::Callback { RefPtr<ServiceWorkerUpdateFinishCallback> mCallback; ~UpdateJobCallback() { } public: explicit UpdateJobCallback(ServiceWorkerUpdateFinishCallback* aCallback) : mCallback(aCallback) { AssertIsOnMainThread(); MOZ_ASSERT(mCallback); } void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) { AssertIsOnMainThread(); MOZ_ASSERT(aJob); if (aStatus.Failed()) { mCallback->UpdateFailed(aStatus); return; } MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Update); RefPtr<ServiceWorkerUpdateJob> updateJob = static_cast<ServiceWorkerUpdateJob*>(aJob); RefPtr<ServiceWorkerRegistrationInfo> reg = updateJob->GetRegistration(); mCallback->UpdateSucceeded(reg); } NS_INLINE_DECL_REFCOUNTING(UpdateJobCallback) }; } // anonymous namespace void ServiceWorkerManager::Update(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<ServiceWorkerRegistrationInfo> registration = GetRegistration(scopeKey, aScope); if (NS_WARN_IF(!registration)) { return; } // "Let newestWorker be the result of running Get Newest Worker algorithm // passing registration as its argument. // If newestWorker is null, return a promise rejected with "InvalidStateError" RefPtr<ServiceWorkerInfo> newest = registration->Newest(); if (!newest) { ErrorResult error(NS_ERROR_DOM_INVALID_STATE_ERR); aCallback->UpdateFailed(error); // In case the callback does not consume the exception error.SuppressException(); return; } RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope); // "Invoke Update algorithm, or its equivalent, with client, registration as // its argument." RefPtr<ServiceWorkerUpdateJob> job = new ServiceWorkerUpdateJob(aPrincipal, registration->mScope, newest->ScriptSpec(), nullptr); RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback); job->AppendResultCallback(cb); queue->ScheduleJob(job); } namespace { static void FireControllerChangeOnDocument(nsIDocument* aDocument) { AssertIsOnMainThread(); MOZ_ASSERT(aDocument); nsCOMPtr<nsPIDOMWindowInner> w = aDocument->GetInnerWindow(); if (!w) { NS_WARNING("Failed to dispatch controllerchange event"); return; } auto* window = nsGlobalWindow::Cast(w.get()); ErrorResult result; dom::Navigator* navigator = window->GetNavigator(result); if (NS_WARN_IF(result.Failed())) { result.SuppressException(); return; } RefPtr<ServiceWorkerContainer> container = navigator->ServiceWorker(); container->ControllerChanged(result); if (result.Failed()) { NS_WARNING("Failed to dispatch controllerchange event"); } } } // anonymous namespace UniquePtr<ServiceWorkerClientInfo> ServiceWorkerManager::GetClient(nsIPrincipal* aPrincipal, const nsAString& aClientId, ErrorResult& aRv) { UniquePtr<ServiceWorkerClientInfo> clientInfo; nsCOMPtr<nsISupportsInterfacePointer> ifptr = do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID); if (NS_WARN_IF(!ifptr)) { return clientInfo; } nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); if (NS_WARN_IF(!obs)) { return clientInfo; } nsresult rv = obs->NotifyObservers(ifptr, "service-worker-get-client", PromiseFlatString(aClientId).get()); if (NS_WARN_IF(NS_FAILED(rv))) { return clientInfo; } nsCOMPtr<nsISupports> ptr; ifptr->GetData(getter_AddRefs(ptr)); nsCOMPtr<nsIDocument> doc = do_QueryInterface(ptr); if (NS_WARN_IF(!doc)) { return clientInfo; } bool equals = false; aPrincipal->Equals(doc->NodePrincipal(), &equals); if (!equals) { return clientInfo; } if (!IsFromAuthenticatedOrigin(doc)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return clientInfo; } clientInfo.reset(new ServiceWorkerClientInfo(doc)); return clientInfo; } void ServiceWorkerManager::GetAllClients(nsIPrincipal* aPrincipal, const nsCString& aScope, bool aIncludeUncontrolled, nsTArray<ServiceWorkerClientInfo>& aDocuments) { MOZ_ASSERT(aPrincipal); RefPtr<ServiceWorkerRegistrationInfo> registration = GetRegistration(aPrincipal, aScope); if (!registration) { // The registration was removed, leave the array empty. return; } nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); if (NS_WARN_IF(!obs)) { return; } nsCOMPtr<nsISimpleEnumerator> enumerator; nsresult rv = obs->EnumerateObservers("service-worker-get-client", getter_AddRefs(enumerator)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } auto ProcessDocument = [&aDocuments](nsIPrincipal* aPrincipal, nsIDocument* aDoc) { if (!aDoc || !aDoc->GetWindow()) { return; } bool equals = false; aPrincipal->Equals(aDoc->NodePrincipal(), &equals); if (!equals) { return; } // Treat http windows with devtools opened as secure if the correct devtools // setting is enabled. if (!aDoc->GetWindow()->GetServiceWorkersTestingEnabled() && !Preferences::GetBool("dom.serviceWorkers.testing.enabled") && !IsFromAuthenticatedOrigin(aDoc)) { return; } ServiceWorkerClientInfo clientInfo(aDoc); aDocuments.AppendElement(aDoc); }; // Since it's not simple to check whether a document is in // mControlledDocuments, we take different code paths depending on whether we // need to look at all documents. The common parts of the two loops are // factored out into the ProcessDocument lambda. if (aIncludeUncontrolled) { bool loop = true; while (NS_SUCCEEDED(enumerator->HasMoreElements(&loop)) && loop) { nsCOMPtr<nsISupports> ptr; rv = enumerator->GetNext(getter_AddRefs(ptr)); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } nsCOMPtr<nsIDocument> doc = do_QueryInterface(ptr); ProcessDocument(aPrincipal, doc); } } else { for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { ServiceWorkerRegistrationInfo* thisRegistration = iter.UserData(); MOZ_ASSERT(thisRegistration); if (!registration->mScope.Equals(thisRegistration->mScope)) { continue; } nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); // All controlled documents must have an outer window. MOZ_ASSERT(doc->GetWindow()); ProcessDocument(aPrincipal, doc); } } } void ServiceWorkerManager::MaybeClaimClient(nsIDocument* aDocument, ServiceWorkerRegistrationInfo* aWorkerRegistration) { MOZ_ASSERT(aWorkerRegistration); MOZ_ASSERT(aWorkerRegistration->GetActive()); // Same origin check if (!aWorkerRegistration->mPrincipal->Equals(aDocument->NodePrincipal())) { return; } // The registration that should be controlling the client RefPtr<ServiceWorkerRegistrationInfo> matchingRegistration = GetServiceWorkerRegistrationInfo(aDocument); // The registration currently controlling the client RefPtr<ServiceWorkerRegistrationInfo> controllingRegistration; GetDocumentRegistration(aDocument, getter_AddRefs(controllingRegistration)); if (aWorkerRegistration != matchingRegistration || aWorkerRegistration == controllingRegistration) { return; } if (controllingRegistration) { StopControllingADocument(controllingRegistration); } StartControllingADocument(aWorkerRegistration, aDocument, NS_LITERAL_STRING("")); FireControllerChangeOnDocument(aDocument); } nsresult ServiceWorkerManager::ClaimClients(nsIPrincipal* aPrincipal, const nsCString& aScope, uint64_t aId) { RefPtr<ServiceWorkerRegistrationInfo> registration = GetRegistration(aPrincipal, aScope); if (!registration || !registration->GetActive() || !(registration->GetActive()->ID() == aId)) { // The worker is not active. return NS_ERROR_DOM_INVALID_STATE_ERR; } nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); if (NS_WARN_IF(!obs)) { return NS_ERROR_FAILURE; } nsCOMPtr<nsISimpleEnumerator> enumerator; nsresult rv = obs->EnumerateObservers("service-worker-get-client", getter_AddRefs(enumerator)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool loop = true; while (NS_SUCCEEDED(enumerator->HasMoreElements(&loop)) && loop) { nsCOMPtr<nsISupports> ptr; rv = enumerator->GetNext(getter_AddRefs(ptr)); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } nsCOMPtr<nsIDocument> doc = do_QueryInterface(ptr); MaybeClaimClient(doc, registration); } return NS_OK; } void ServiceWorkerManager::SetSkipWaitingFlag(nsIPrincipal* aPrincipal, const nsCString& aScope, uint64_t aServiceWorkerID) { RefPtr<ServiceWorkerRegistrationInfo> registration = GetRegistration(aPrincipal, aScope); if (NS_WARN_IF(!registration)) { return; } RefPtr<ServiceWorkerInfo> worker = registration->GetServiceWorkerInfoById(aServiceWorkerID); if (NS_WARN_IF(!worker)) { return; } worker->SetSkipWaitingFlag(); if (worker->State() == ServiceWorkerState::Installed) { registration->TryToActivateAsync(); } } void ServiceWorkerManager::FireControllerChange(ServiceWorkerRegistrationInfo* aRegistration) { AssertIsOnMainThread(); for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { if (iter.UserData() != aRegistration) { continue; } nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); if (NS_WARN_IF(!doc)) { continue; } FireControllerChangeOnDocument(doc); } } already_AddRefed<ServiceWorkerRegistrationInfo> 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); } NS_IMETHODIMP ServiceWorkerManager::GetRegistrationByPrincipal(nsIPrincipal* aPrincipal, const nsAString& aScope, nsIServiceWorkerRegistrationInfo** aInfo) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aInfo); nsCOMPtr<nsIURI> scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } RefPtr<ServiceWorkerRegistrationInfo> info = GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI); if (!info) { return NS_ERROR_FAILURE; } info.forget(aInfo); return NS_OK; } already_AddRefed<ServiceWorkerRegistrationInfo> ServiceWorkerManager::GetRegistration(const nsACString& aScopeKey, const nsACString& aScope) const { RefPtr<ServiceWorkerRegistrationInfo> reg; RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(aScopeKey, &data)) { return reg.forget(); } data->mInfos.Get(aScope, getter_AddRefs(reg)); return reg.forget(); } already_AddRefed<ServiceWorkerRegistrationInfo> ServiceWorkerManager::CreateNewRegistration(const nsCString& aScope, nsIPrincipal* aPrincipal) { #ifdef DEBUG AssertIsOnMainThread(); nsCOMPtr<nsIURI> scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); MOZ_ASSERT(NS_SUCCEEDED(rv)); RefPtr<ServiceWorkerRegistrationInfo> tmp = GetRegistration(aPrincipal, aScope); MOZ_ASSERT(!tmp); #endif RefPtr<ServiceWorkerRegistrationInfo> registration = new ServiceWorkerRegistrationInfo(aScope, aPrincipal); // From now on ownership of registration is with // mServiceWorkerRegistrationInfos. AddScopeAndRegistration(aScope, registration); return registration.forget(); } void ServiceWorkerManager::MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(aRegistration); RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); if (!newest && HasScope(aRegistration->mPrincipal, aRegistration->mScope)) { 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. // Assert these conditions by testing for pending uninstall (cases 1 and 2) or // null workers (case 3). #ifdef DEBUG RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); MOZ_ASSERT(aRegistration->mPendingUninstall || !newest); #endif MOZ_ASSERT(HasScope(aRegistration->mPrincipal, aRegistration->mScope)); // When a registration is removed, we must clear its contents since the DOM // object may be held by content script. aRegistration->Clear(); RemoveScopeAndRegistration(aRegistration); } namespace { /** * See toolkit/modules/sessionstore/Utils.jsm function hasRootDomain(). * * Returns true if the |url| passed in is part of the given root |domain|. * For example, if |url| is "www.mozilla.org", and we pass in |domain| as * "mozilla.org", this will return true. It would return false the other way * around. */ bool HasRootDomain(nsIURI* aURI, const nsACString& aDomain) { AssertIsOnMainThread(); MOZ_ASSERT(aURI); nsAutoCString host; nsresult rv = aURI->GetHost(host); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } nsACString::const_iterator start, end; host.BeginReading(start); host.EndReading(end); if (!FindInReadable(aDomain, start, end)) { return false; } if (host.Equals(aDomain)) { return true; } // Beginning of the string matches, can't look at the previous char. if (start.get() == host.BeginReading()) { // Equals failed so this is fine. return false; } char prevChar = *(--start); return prevChar == '.'; } } // namespace NS_IMETHODIMP ServiceWorkerManager::GetAllRegistrations(nsIArray** aResult) { AssertIsOnMainThread(); nsCOMPtr<nsIMutableArray> 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); if (reg->mPendingUninstall) { continue; } array->AppendElement(reg, false); } } 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<ServiceWorkerJobQueue> queue; aRegistrationData->mJobQueues.Get(aRegistration->mScope, getter_AddRefs(queue)); if (queue) { queue->CancelAll(); } nsCOMPtr<nsITimer> timer = aRegistrationData->mUpdateTimers.Get(aRegistration->mScope); if (timer) { timer->Cancel(); aRegistrationData->mUpdateTimers.Remove(aRegistration->mScope); } // Since Unregister is async, it is ok to call it in an enumeration. Unregister(aRegistration->mPrincipal, nullptr, NS_ConvertUTF8toUTF16(aRegistration->mScope)); } NS_IMETHODIMP ServiceWorkerManager::RemoveAndPropagate(const nsACString& aHost) { Remove(aHost); PropagateRemove(aHost); return NS_OK; } void ServiceWorkerManager::Remove(const nsACString& aHost) { AssertIsOnMainThread(); // We need to postpone this operation in case we don't have an actor because // this is needed by the ForceUnregister. if (!mActor) { RefPtr<nsIRunnable> runnable = new RemoveRunnable(aHost); AppendPendingOperation(runnable); 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<nsIURI> scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), it2.Key(), nullptr, nullptr); // This way subdomains are also cleared. if (NS_SUCCEEDED(rv) && HasRootDomain(scopeURI, aHost)) { ForceUnregister(data, reg); } } } } void ServiceWorkerManager::PropagateRemove(const nsACString& aHost) { AssertIsOnMainThread(); if (!mActor) { RefPtr<nsIRunnable> runnable = new PropagateRemoveRunnable(aHost); AppendPendingOperation(runnable); return; } mActor->SendPropagateRemove(nsCString(aHost)); } void ServiceWorkerManager::RemoveAll() { AssertIsOnMainThread(); 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() { AssertIsOnMainThread(); MOZ_ASSERT(XRE_IsParentProcess()); if (!mActor) { RefPtr<nsIRunnable> runnable = new PropagateRemoveAllRunnable(); AppendPendingOperation(runnable); return; } mActor->SendPropagateRemoveAll(); } void ServiceWorkerManager::RemoveAllRegistrations(OriginAttributesPattern* aPattern) { AssertIsOnMainThread(); 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->mPrincipal); bool matches = aPattern->Matches(BasePrincipal::Cast(reg->mPrincipal)->OriginAttributesRef()); if (!matches) { continue; } ForceUnregister(data, reg); } } } NS_IMETHODIMP ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener) { AssertIsOnMainThread(); if (!aListener || mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.AppendElement(aListener); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::RemoveListener(nsIServiceWorkerManagerListener* aListener) { AssertIsOnMainThread(); if (!aListener || !mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.RemoveElement(aListener); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::ShouldReportToWindow(mozIDOMWindowProxy* aWindow, const nsACString& aScope, bool* aResult) { AssertIsOnMainThread(); MOZ_ASSERT(aResult); *aResult = false; // Get the inner window ID to compare to our document windows below. nsCOMPtr<nsPIDOMWindowOuter> targetWin = nsPIDOMWindowOuter::From(aWindow); if (NS_WARN_IF(!targetWin)) { return NS_OK; } targetWin = targetWin->GetScriptableTop(); uint64_t winId = targetWin->WindowID(); // Check our weak registering document references first. This way we clear // out as many dead weak references as possible when this method is called. WeakDocumentList* list = mRegisteringDocuments.Get(aScope); if (list) { for (int32_t i = list->Length() - 1; i >= 0; --i) { nsCOMPtr<nsIDocument> doc = do_QueryReferent(list->ElementAt(i)); if (!doc) { list->RemoveElementAt(i); continue; } if (!doc->IsCurrentActiveDocument()) { continue; } nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow(); if (!win) { continue; } win = win->GetScriptableTop(); // Match. We should report to this window. if (win && winId == win->WindowID()) { *aResult = true; return NS_OK; } } if (list->IsEmpty()) { list = nullptr; nsAutoPtr<WeakDocumentList> doomed; mRegisteringDocuments.RemoveAndForget(aScope, doomed); } } // Examine any windows performing a navigation that we are currently // intercepting. InterceptionList* intList = mNavigationInterceptions.Get(aScope); if (intList) { for (uint32_t i = 0; i < intList->Length(); ++i) { nsCOMPtr<nsIInterceptedChannel> channel = intList->ElementAt(i); nsCOMPtr<nsIChannel> inner; nsresult rv = channel->GetChannel(getter_AddRefs(inner)); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } uint64_t id = nsContentUtils::GetInnerWindowID(inner); if (id == 0) { continue; } nsCOMPtr<nsPIDOMWindowInner> win = nsGlobalWindow::GetInnerWindowWithId(id)->AsInner(); if (!win) { continue; } nsCOMPtr<nsPIDOMWindowOuter> outer = win->GetScriptableTop(); // Match. We should report to this window. if (outer && winId == outer->WindowID()) { *aResult = true; return NS_OK; } } } // Next examine controlled documents to see if the windows match. for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { ServiceWorkerRegistrationInfo* reg = iter.UserData(); MOZ_ASSERT(reg); if (!reg->mScope.Equals(aScope)) { continue; } nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); if (!doc || !doc->IsCurrentActiveDocument()) { continue; } nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow(); if (!win) { continue; } win = win->GetScriptableTop(); // Match. We should report to this window. if (win && winId == win->WindowID()) { *aResult = true; return NS_OK; } } // No match. We should not report to this window. return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (strcmp(aTopic, PURGE_SESSION_HISTORY) == 0) { MOZ_ASSERT(XRE_IsParentProcess()); RemoveAll(); PropagateRemoveAll(); return NS_OK; } if (strcmp(aTopic, PURGE_DOMAIN_DATA) == 0) { MOZ_ASSERT(XRE_IsParentProcess()); nsAutoString domain(aData); RemoveAndPropagate(NS_ConvertUTF16toUTF8(domain)); return NS_OK; } 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, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 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<JS::Value> aOriginAttributes, const nsAString& aScope, JSContext* aCx) { AssertIsOnMainThread(); PrincipalOriginAttributes attrs; if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } PropagateSoftUpdate(attrs, aScope); return NS_OK; } void ServiceWorkerManager::PropagateSoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, const nsAString& aScope) { AssertIsOnMainThread(); if (!mActor) { RefPtr<nsIRunnable> runnable = new PropagateSoftUpdateRunnable(aOriginAttributes, aScope); AppendPendingOperation(runnable); return; } mActor->SendPropagateSoftUpdate(aOriginAttributes, nsString(aScope)); } NS_IMETHODIMP ServiceWorkerManager::PropagateUnregister(nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback, const nsAString& aScope) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); if (!mActor) { RefPtr<nsIRunnable> runnable = new PropagateUnregisterRunnable(aPrincipal, aCallback, aScope); AppendPendingOperation(runnable); return NS_OK; } 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<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnRegister(aInfo); } } void ServiceWorkerManager::NotifyListenersOnUnregister( nsIServiceWorkerRegistrationInfo* aInfo) { nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnUnregister(aInfo); } } void ServiceWorkerManager::AddRegisteringDocument(const nsACString& aScope, nsIDocument* aDoc) { AssertIsOnMainThread(); MOZ_ASSERT(!aScope.IsEmpty()); MOZ_ASSERT(aDoc); WeakDocumentList* list = mRegisteringDocuments.LookupOrAdd(aScope); MOZ_ASSERT(list); for (int32_t i = list->Length() - 1; i >= 0; --i) { nsCOMPtr<nsIDocument> existing = do_QueryReferent(list->ElementAt(i)); if (!existing) { list->RemoveElementAt(i); continue; } if (existing == aDoc) { return; } } list->AppendElement(do_GetWeakReference(aDoc)); } class ServiceWorkerManager::InterceptionReleaseHandle final : public nsISupports { const nsCString mScope; // Weak reference to channel is safe, because the channel holds a // reference to this object. Also, the pointer is only used for // comparison purposes. nsIInterceptedChannel* mChannel; ~InterceptionReleaseHandle() { RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (swm) { swm->RemoveNavigationInterception(mScope, mChannel); } } public: InterceptionReleaseHandle(const nsACString& aScope, nsIInterceptedChannel* aChannel) : mScope(aScope) , mChannel(aChannel) { AssertIsOnMainThread(); MOZ_ASSERT(!aScope.IsEmpty()); MOZ_ASSERT(mChannel); } NS_DECL_ISUPPORTS }; NS_IMPL_ISUPPORTS0(ServiceWorkerManager::InterceptionReleaseHandle); void ServiceWorkerManager::AddNavigationInterception(const nsACString& aScope, nsIInterceptedChannel* aChannel) { AssertIsOnMainThread(); MOZ_ASSERT(!aScope.IsEmpty()); MOZ_ASSERT(aChannel); InterceptionList* list = mNavigationInterceptions.LookupOrAdd(aScope); MOZ_ASSERT(list); MOZ_ASSERT(!list->Contains(aChannel)); nsCOMPtr<nsISupports> releaseHandle = new InterceptionReleaseHandle(aScope, aChannel); aChannel->SetReleaseHandle(releaseHandle); list->AppendElement(aChannel); } void ServiceWorkerManager::RemoveNavigationInterception(const nsACString& aScope, nsIInterceptedChannel* aChannel) { AssertIsOnMainThread(); MOZ_ASSERT(aChannel); InterceptionList* list = mNavigationInterceptions.Get(aScope); if (list) { MOZ_ALWAYS_TRUE(list->RemoveElement(aChannel)); MOZ_ASSERT(!list->Contains(aChannel)); if (list->IsEmpty()) { list = nullptr; nsAutoPtr<InterceptionList> doomed; mNavigationInterceptions.RemoveAndForget(aScope, doomed); } } } class UpdateTimerCallback final : public nsITimerCallback { nsCOMPtr<nsIPrincipal> mPrincipal; const nsCString mScope; ~UpdateTimerCallback() { } public: UpdateTimerCallback(nsIPrincipal* aPrincipal, const nsACString& aScope) : mPrincipal(aPrincipal) , mScope(aScope) { AssertIsOnMainThread(); MOZ_ASSERT(mPrincipal); MOZ_ASSERT(!mScope.IsEmpty()); } NS_IMETHOD Notify(nsITimer* aTimer) override { AssertIsOnMainThread(); RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); if (!swm) { // shutting down, do nothing return NS_OK; } swm->UpdateTimerFired(mPrincipal, mScope); return NS_OK; } NS_DECL_ISUPPORTS }; NS_IMPL_ISUPPORTS(UpdateTimerCallback, nsITimerCallback) bool ServiceWorkerManager::MayHaveActiveServiceWorkerInstance(ContentParent* aContent, nsIPrincipal* aPrincipal) { AssertIsOnMainThread(); 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) { AssertIsOnMainThread(); 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<nsITimer> timer = data->mUpdateTimers.Get(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; } timer = do_CreateInstance("@mozilla.org/timer;1", &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr<nsITimerCallback> callback = new UpdateTimerCallback(aPrincipal, aScope); const uint32_t UPDATE_DELAY_MS = 1000; rv = timer->InitWithCallback(callback, UPDATE_DELAY_MS, nsITimer::TYPE_ONE_SHOT); if (NS_WARN_IF(NS_FAILED(rv))) { return; } data->mUpdateTimers.Put(aScope, timer); } void ServiceWorkerManager::UpdateTimerFired(nsIPrincipal* aPrincipal, const nsACString& aScope) { AssertIsOnMainThread(); 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; } nsCOMPtr<nsITimer> timer = data->mUpdateTimers.Get(aScope); if (timer) { timer->Cancel(); data->mUpdateTimers.Remove(aScope); } RefPtr<ServiceWorkerRegistrationInfo> registration; data->mInfos.Get(aScope, getter_AddRefs(registration)); if (!registration) { return; } if (!registration->CheckAndClearIfUpdateNeeded()) { return; } PrincipalOriginAttributes attrs = BasePrincipal::Cast(aPrincipal)->OriginAttributesRef(); SoftUpdate(attrs, aScope); } void ServiceWorkerManager::MaybeSendUnregister(nsIPrincipal* aPrincipal, const nsACString& aScope) { AssertIsOnMainThread(); 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)); } END_WORKERS_NAMESPACE