/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/cache/CacheStorage.h" #include "mozilla/Preferences.h" #include "mozilla/Unused.h" #include "mozilla/dom/CacheBinding.h" #include "mozilla/dom/CacheStorageBinding.h" #include "mozilla/dom/InternalRequest.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/Response.h" #include "mozilla/dom/cache/AutoUtils.h" #include "mozilla/dom/cache/Cache.h" #include "mozilla/dom/cache/CacheChild.h" #include "mozilla/dom/cache/CacheCommon.h" #include "mozilla/dom/cache/CacheStorageChild.h" #include "mozilla/dom/cache/CacheWorkerRef.h" #include "mozilla/dom/cache/PCacheChild.h" #include "mozilla/dom/cache/ReadStream.h" #include "mozilla/dom/cache/TypeUtils.h" #include "mozilla/dom/quota/QuotaManager.h" #include "mozilla/dom/quota/ResultExtensions.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_extensions.h" #include "nsContentUtils.h" #include "mozilla/dom/Document.h" #include "nsIGlobalObject.h" #include "nsMixedContentBlocker.h" #include "nsURLParsers.h" #include "js/Object.h" // JS::GetClass #include "js/PropertyAndElement.h" // JS_DefineProperty namespace mozilla::dom::cache { using mozilla::ErrorResult; using mozilla::dom::quota::QuotaManager; using mozilla::ipc::BackgroundChild; using mozilla::ipc::PBackgroundChild; using mozilla::ipc::PrincipalInfo; using mozilla::ipc::PrincipalToPrincipalInfo; NS_IMPL_CYCLE_COLLECTING_ADDREF(mozilla::dom::cache::CacheStorage); NS_IMPL_CYCLE_COLLECTING_RELEASE(mozilla::dom::cache::CacheStorage); NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(mozilla::dom::cache::CacheStorage, mGlobal); NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CacheStorage) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END // We cannot reference IPC types in a webidl binding implementation header. So // define this in the .cpp. struct CacheStorage::Entry final { RefPtr mPromise; CacheOpArgs mArgs; // We cannot add the requests until after the actor is present. So store // the request data separately for now. SafeRefPtr mRequest; }; namespace { bool IsTrusted(const PrincipalInfo& aPrincipalInfo, bool aTestingPrefEnabled) { // Can happen on main thread or worker thread if (aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { return true; } // Require a ContentPrincipal to avoid null principal, etc. QM_TRY(OkIf(aPrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo), false); // If we're in testing mode, then don't do any more work to determine if // the origin is trusted. We have to run some tests as http. if (aTestingPrefEnabled) { return true; } // Now parse the scheme of the principal's origin. This is a short term // method for determining "trust". In the long term we need to implement // the full algorithm here: // // https://w3c.github.io/webappsec/specs/powerfulfeatures/#settings-secure // // TODO: Implement full secure setting algorithm. (bug 1177856) const nsCString& flatURL = aPrincipalInfo.get_ContentPrincipalInfo().spec(); const char* const url = flatURL.get(); // off the main thread URL parsing using nsStdURLParser. const nsCOMPtr urlParser = new nsStdURLParser(); uint32_t schemePos; int32_t schemeLen; uint32_t authPos; int32_t authLen; QM_TRY(MOZ_TO_RESULT(urlParser->ParseURL(url, flatURL.Length(), &schemePos, &schemeLen, &authPos, &authLen, nullptr, nullptr)), // ignore path false); const nsAutoCString scheme(Substring(flatURL, schemePos, schemeLen)); if (scheme.LowerCaseEqualsLiteral("https") || scheme.LowerCaseEqualsLiteral("file") || scheme.LowerCaseEqualsLiteral("moz-extension")) { return true; } uint32_t hostPos; int32_t hostLen; QM_TRY(MOZ_TO_RESULT( urlParser->ParseAuthority(url + authPos, authLen, nullptr, nullptr, // ignore username nullptr, nullptr, // ignore password &hostPos, &hostLen, nullptr)), // ignore port false); return nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost( nsDependentCSubstring(url + authPos + hostPos, hostLen)); } } // namespace // static already_AddRefed CacheStorage::CreateOnMainThread( Namespace aNamespace, nsIGlobalObject* aGlobal, nsIPrincipal* aPrincipal, bool aForceTrustedOrigin, ErrorResult& aRv) { MOZ_DIAGNOSTIC_ASSERT(aGlobal); MOZ_DIAGNOSTIC_ASSERT(aPrincipal); MOZ_ASSERT(NS_IsMainThread()); PrincipalInfo principalInfo; QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)), nullptr, [&aRv](const nsresult rv) { aRv.Throw(rv); }); QM_TRY(OkIf(QuotaManager::IsPrincipalInfoValid(principalInfo)), RefPtr{new CacheStorage(NS_ERROR_DOM_SECURITY_ERR)}.forget(), [](const auto) { NS_WARNING("CacheStorage not supported on invalid origins."); }); const bool testingEnabled = aForceTrustedOrigin || Preferences::GetBool("dom.caches.testing.enabled", false) || StaticPrefs::dom_serviceWorkers_testing_enabled(); if (!IsTrusted(principalInfo, testingEnabled)) { NS_WARNING("CacheStorage not supported on untrusted origins."); RefPtr ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR); return ref.forget(); } RefPtr ref = new CacheStorage(aNamespace, aGlobal, principalInfo, nullptr); return ref.forget(); } // static already_AddRefed CacheStorage::CreateOnWorker( Namespace aNamespace, nsIGlobalObject* aGlobal, WorkerPrivate* aWorkerPrivate, ErrorResult& aRv) { MOZ_DIAGNOSTIC_ASSERT(aGlobal); MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); if (aWorkerPrivate->GetOriginAttributes().mPrivateBrowsingId > 0) { NS_WARNING("CacheStorage not supported during private browsing."); RefPtr ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR); return ref.forget(); } SafeRefPtr workerRef = CacheWorkerRef::Create(aWorkerPrivate, CacheWorkerRef::eIPCWorkerRef); if (!workerRef) { NS_WARNING("Worker thread is shutting down."); aRv.Throw(NS_ERROR_FAILURE); return nullptr; } const PrincipalInfo& principalInfo = aWorkerPrivate->GetEffectiveStoragePrincipalInfo(); QM_TRY(OkIf(QuotaManager::IsPrincipalInfoValid(principalInfo)), nullptr, [&aRv](const auto) { aRv.Throw(NS_ERROR_FAILURE); }); // We have a number of cases where we want to skip the https scheme // validation: // // 1) Any worker when dom.caches.testing.enabled pref is true. // 2) Any worker when dom.serviceWorkers.testing.enabled pref is true. This // is mainly because most sites using SWs will expect Cache to work if // SWs are enabled. // 3) If the window that created this worker has the devtools SW testing // option enabled. Same reasoning as (2). // 4) If the worker itself is a ServiceWorker, then we always skip the // origin checks. The ServiceWorker has its own trusted origin checks // that are better than ours. In addition, we don't have information // about the window any more, so we can't do our own checks. bool testingEnabled = StaticPrefs::dom_caches_testing_enabled() || StaticPrefs::dom_serviceWorkers_testing_enabled() || aWorkerPrivate->ServiceWorkersTestingInWindow() || aWorkerPrivate->IsServiceWorker(); if (!IsTrusted(principalInfo, testingEnabled)) { NS_WARNING("CacheStorage not supported on untrusted origins."); RefPtr ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR); return ref.forget(); } RefPtr ref = new CacheStorage( aNamespace, aGlobal, principalInfo, std::move(workerRef)); return ref.forget(); } // static bool CacheStorage::DefineCaches(JSContext* aCx, JS::Handle aGlobal) { MOZ_ASSERT(NS_IsMainThread()); MOZ_DIAGNOSTIC_ASSERT(JS::GetClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL, "Passed object is not a global object!"); js::AssertSameCompartment(aCx, aGlobal); if (NS_WARN_IF(!CacheStorage_Binding::GetConstructorObject(aCx) || !Cache_Binding::GetConstructorObject(aCx))) { return false; } nsIPrincipal* principal = nsContentUtils::ObjectPrincipal(aGlobal); MOZ_DIAGNOSTIC_ASSERT(principal); ErrorResult rv; RefPtr storage = CreateOnMainThread(DEFAULT_NAMESPACE, xpc::NativeGlobal(aGlobal), principal, true, /* force trusted */ rv); if (NS_WARN_IF(rv.MaybeSetPendingException(aCx))) { return false; } JS::Rooted caches(aCx); if (NS_WARN_IF(!ToJSValue(aCx, storage, &caches))) { return false; } return JS_DefineProperty(aCx, aGlobal, "caches", caches, JSPROP_ENUMERATE); } CacheStorage::CacheStorage(Namespace aNamespace, nsIGlobalObject* aGlobal, const PrincipalInfo& aPrincipalInfo, SafeRefPtr aWorkerRef) : mNamespace(aNamespace), mGlobal(aGlobal), mPrincipalInfo(MakeUnique(aPrincipalInfo)), mActor(nullptr), mStatus(NS_OK) { MOZ_DIAGNOSTIC_ASSERT(mGlobal); // If the PBackground actor is already initialized then we can // immediately use it PBackgroundChild* actor = BackgroundChild::GetOrCreateForCurrentThread(); if (NS_WARN_IF(!actor)) { mStatus = NS_ERROR_UNEXPECTED; return; } // WorkerRef ownership is passed to the CacheStorageChild actor and any // actors it may create. The WorkerRef will keep the worker thread alive // until the actors can gracefully shutdown. CacheStorageChild* newActor = new CacheStorageChild(this, std::move(aWorkerRef)); PCacheStorageChild* constructedActor = actor->SendPCacheStorageConstructor( newActor, mNamespace, *mPrincipalInfo); if (NS_WARN_IF(!constructedActor)) { mStatus = NS_ERROR_UNEXPECTED; return; } MOZ_DIAGNOSTIC_ASSERT(constructedActor == newActor); mActor = newActor; } CacheStorage::CacheStorage(nsresult aFailureResult) : mNamespace(INVALID_NAMESPACE), mActor(nullptr), mStatus(aFailureResult) { MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mStatus)); } already_AddRefed CacheStorage::Match( JSContext* aCx, const RequestOrUSVString& aRequest, const MultiCacheQueryOptions& aOptions, ErrorResult& aRv) { NS_ASSERT_OWNINGTHREAD(CacheStorage); if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesMatch, UseCounterWorker::Custom_PrivateBrowsingCachesMatch)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return nullptr; } if (NS_WARN_IF(NS_FAILED(mStatus))) { aRv.Throw(mStatus); return nullptr; } SafeRefPtr request = ToInternalRequest(aCx, aRequest, IgnoreBody, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr promise = Promise::Create(mGlobal, aRv); if (NS_WARN_IF(!promise)) { return nullptr; } CacheQueryParams params; ToCacheQueryParams(params, aOptions); auto entry = MakeUnique(); entry->mPromise = promise; entry->mArgs = StorageMatchArgs(CacheRequest(), params, GetOpenMode()); entry->mRequest = std::move(request); RunRequest(std::move(entry)); return promise.forget(); } already_AddRefed CacheStorage::Has(const nsAString& aKey, ErrorResult& aRv) { NS_ASSERT_OWNINGTHREAD(CacheStorage); if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesHas, UseCounterWorker::Custom_PrivateBrowsingCachesHas)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return nullptr; } if (NS_WARN_IF(NS_FAILED(mStatus))) { aRv.Throw(mStatus); return nullptr; } RefPtr promise = Promise::Create(mGlobal, aRv); if (NS_WARN_IF(!promise)) { return nullptr; } auto entry = MakeUnique(); entry->mPromise = promise; entry->mArgs = StorageHasArgs(nsString(aKey)); RunRequest(std::move(entry)); return promise.forget(); } already_AddRefed CacheStorage::Open(const nsAString& aKey, ErrorResult& aRv) { NS_ASSERT_OWNINGTHREAD(CacheStorage); if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesOpen, UseCounterWorker::Custom_PrivateBrowsingCachesOpen)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return nullptr; } if (NS_WARN_IF(NS_FAILED(mStatus))) { aRv.Throw(mStatus); return nullptr; } RefPtr promise = Promise::Create(mGlobal, aRv); if (NS_WARN_IF(!promise)) { return nullptr; } auto entry = MakeUnique(); entry->mPromise = promise; entry->mArgs = StorageOpenArgs(nsString(aKey)); RunRequest(std::move(entry)); return promise.forget(); } already_AddRefed CacheStorage::Delete(const nsAString& aKey, ErrorResult& aRv) { NS_ASSERT_OWNINGTHREAD(CacheStorage); if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesDelete, UseCounterWorker::Custom_PrivateBrowsingCachesDelete)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return nullptr; } if (NS_WARN_IF(NS_FAILED(mStatus))) { aRv.Throw(mStatus); return nullptr; } RefPtr promise = Promise::Create(mGlobal, aRv); if (NS_WARN_IF(!promise)) { return nullptr; } auto entry = MakeUnique(); entry->mPromise = promise; entry->mArgs = StorageDeleteArgs(nsString(aKey)); RunRequest(std::move(entry)); return promise.forget(); } already_AddRefed CacheStorage::Keys(ErrorResult& aRv) { NS_ASSERT_OWNINGTHREAD(CacheStorage); if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesKeys, UseCounterWorker::Custom_PrivateBrowsingCachesKeys)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return nullptr; } if (NS_WARN_IF(NS_FAILED(mStatus))) { aRv.Throw(mStatus); return nullptr; } RefPtr promise = Promise::Create(mGlobal, aRv); if (NS_WARN_IF(!promise)) { return nullptr; } auto entry = MakeUnique(); entry->mPromise = promise; entry->mArgs = StorageKeysArgs(); RunRequest(std::move(entry)); return promise.forget(); } // static already_AddRefed CacheStorage::Constructor( const GlobalObject& aGlobal, CacheStorageNamespace aNamespace, nsIPrincipal* aPrincipal, ErrorResult& aRv) { if (NS_WARN_IF(!NS_IsMainThread())) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } // TODO: remove Namespace in favor of CacheStorageNamespace static_assert(DEFAULT_NAMESPACE == (uint32_t)CacheStorageNamespace::Content, "Default namespace should match webidl Content enum"); static_assert( CHROME_ONLY_NAMESPACE == (uint32_t)CacheStorageNamespace::Chrome, "Chrome namespace should match webidl Chrome enum"); static_assert(NUMBER_OF_NAMESPACES == CacheStorageNamespaceValues::Count, "Number of namespace should match webidl count"); Namespace ns = static_cast(aNamespace); nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); bool privateBrowsing = false; if (nsCOMPtr window = do_QueryInterface(global)) { RefPtr doc = window->GetExtantDoc(); if (doc) { nsCOMPtr loadContext = doc->GetLoadContext(); privateBrowsing = loadContext && loadContext->UsePrivateBrowsing(); } } if (privateBrowsing) { RefPtr ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR); return ref.forget(); } // Create a CacheStorage object bypassing the trusted origin checks // since this is a chrome-only constructor. return CreateOnMainThread(ns, global, aPrincipal, true /* force trusted origin */, aRv); } nsISupports* CacheStorage::GetParentObject() const { return mGlobal; } JSObject* CacheStorage::WrapObject(JSContext* aContext, JS::Handle aGivenProto) { return mozilla::dom::CacheStorage_Binding::Wrap(aContext, this, aGivenProto); } void CacheStorage::DestroyInternal(CacheStorageChild* aActor) { NS_ASSERT_OWNINGTHREAD(CacheStorage); MOZ_DIAGNOSTIC_ASSERT(mActor); MOZ_DIAGNOSTIC_ASSERT(mActor == aActor); MOZ_DIAGNOSTIC_ASSERT(!NS_FAILED(mStatus)); mActor->ClearListener(); mActor = nullptr; mStatus = NS_ERROR_UNEXPECTED; // Note that we will never get an actor again in case another request is // made before this object is destructed. } nsIGlobalObject* CacheStorage::GetGlobalObject() const { return mGlobal; } #ifdef DEBUG void CacheStorage::AssertOwningThread() const { NS_ASSERT_OWNINGTHREAD(CacheStorage); } #endif PBackgroundChild* CacheStorage::GetIPCManager() { // This is true because CacheStorage always uses IgnoreBody for requests. // So we should never need to get the IPC manager during Request or // Response serialization. MOZ_CRASH("CacheStorage does not implement TypeUtils::GetIPCManager()"); } CacheStorage::~CacheStorage() { NS_ASSERT_OWNINGTHREAD(CacheStorage); if (mActor) { mActor->StartDestroyFromListener(); // DestroyInternal() is called synchronously by StartDestroyFromListener(). // So we should have already cleared the mActor. MOZ_DIAGNOSTIC_ASSERT(!mActor); } } void CacheStorage::RunRequest(UniquePtr aEntry) { MOZ_ASSERT(mActor); AutoChildOpArgs args(this, aEntry->mArgs, 1); if (aEntry->mRequest) { ErrorResult rv; args.Add(*aEntry->mRequest, IgnoreBody, IgnoreInvalidScheme, rv); if (NS_WARN_IF(rv.Failed())) { aEntry->mPromise->MaybeReject(std::move(rv)); return; } } mActor->ExecuteOp(mGlobal, aEntry->mPromise, this, args.SendAsOpArgs()); } OpenMode CacheStorage::GetOpenMode() const { return mNamespace == CHROME_ONLY_NAMESPACE ? OpenMode::Eager : OpenMode::Lazy; } bool CacheStorage::HasStorageAccess(UseCounter aLabel, UseCounterWorker aLabelWorker) const { NS_ASSERT_OWNINGTHREAD(CacheStorage); if (NS_WARN_IF(!mGlobal)) { return false; } StorageAccess access = mGlobal->GetStorageAccess(); if (access == StorageAccess::ePrivateBrowsing) { if (NS_IsMainThread()) { SetUseCounter(mGlobal->GetGlobalJSObject(), aLabel); } else { SetUseCounter(aLabelWorker); } } // Deny storage access for private browsing. if (nsIPrincipal* principal = mGlobal->PrincipalOrNull()) { if (!principal->IsSystemPrincipal() && principal->GetPrivateBrowsingId() != nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) { return false; } } return access > StorageAccess::ePrivateBrowsing || (StaticPrefs:: privacy_partition_always_partition_third_party_non_cookie_storage() && ShouldPartitionStorage(access)); } } // namespace mozilla::dom::cache