diff options
Diffstat (limited to 'dom/cache/CacheStorage.cpp')
-rw-r--r-- | dom/cache/CacheStorage.cpp | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/dom/cache/CacheStorage.cpp b/dom/cache/CacheStorage.cpp new file mode 100644 index 000000000..937e3826f --- /dev/null +++ b/dom/cache/CacheStorage.cpp @@ -0,0 +1,628 @@ +/* -*- 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/Unused.h" +#include "mozilla/dom/CacheStorageBinding.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/CacheStorageChild.h" +#include "mozilla/dom/cache/CacheWorkerHolder.h" +#include "mozilla/dom/cache/PCacheChild.h" +#include "mozilla/dom/cache/ReadStream.h" +#include "mozilla/dom/cache/TypeUtils.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsContentUtils.h" +#include "nsIDocument.h" +#include "nsIGlobalObject.h" +#include "nsIScriptSecurityManager.h" +#include "nsURLParsers.h" +#include "WorkerPrivate.h" + +namespace mozilla { +namespace dom { +namespace cache { + +using mozilla::Unused; +using mozilla::ErrorResult; +using mozilla::dom::workers::WorkerPrivate; +using mozilla::ipc::BackgroundChild; +using mozilla::ipc::PBackgroundChild; +using mozilla::ipc::IProtocol; +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_ENTRY(nsIIPCBackgroundChildCreateCallback) +NS_INTERFACE_MAP_END + +// We cannot reference IPC types in a webidl binding implementation header. So +// define this in the .cpp and use heap storage in the mPendingRequests list. +struct CacheStorage::Entry final +{ + RefPtr<Promise> mPromise; + CacheOpArgs mArgs; + // We cannot add the requests until after the actor is present. So store + // the request data separately for now. + RefPtr<InternalRequest> 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. + // + // Also, an unknown appId means that this principal was created for the + // codebase without all the security information from the end document or + // worker. We require exact knowledge of this information before allowing + // the caller to touch the disk using the Cache API. + if (NS_WARN_IF(aPrincipalInfo.type() != PrincipalInfo::TContentPrincipalInfo || + aPrincipalInfo.get_ContentPrincipalInfo().attrs().mAppId == + nsIScriptSecurityManager::UNKNOWN_APP_ID)) { + return false; + } + + // If we're in testing mode, then don't do any more work to determing 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* url = flatURL.get(); + + // off the main thread URL parsing using nsStdURLParser. + nsCOMPtr<nsIURLParser> urlParser = new nsStdURLParser(); + + uint32_t schemePos; + int32_t schemeLen; + uint32_t authPos; + int32_t authLen; + nsresult rv = urlParser->ParseURL(url, flatURL.Length(), + &schemePos, &schemeLen, + &authPos, &authLen, + nullptr, nullptr); // ignore path + if (NS_WARN_IF(NS_FAILED(rv))) { return false; } + + nsAutoCString scheme(Substring(flatURL, schemePos, schemeLen)); + if (scheme.LowerCaseEqualsLiteral("https") || + scheme.LowerCaseEqualsLiteral("file")) { + return true; + } + + uint32_t hostPos; + int32_t hostLen; + + rv = urlParser->ParseAuthority(url + authPos, authLen, + nullptr, nullptr, // ignore username + nullptr, nullptr, // ignore password + &hostPos, &hostLen, + nullptr); // ignore port + if (NS_WARN_IF(NS_FAILED(rv))) { return false; } + + nsDependentCSubstring hostname(url + authPos + hostPos, hostLen); + + return hostname.EqualsLiteral("localhost") || + hostname.EqualsLiteral("127.0.0.1") || + hostname.EqualsLiteral("::1"); +} + +} // namespace + +// static +already_AddRefed<CacheStorage> +CacheStorage::CreateOnMainThread(Namespace aNamespace, nsIGlobalObject* aGlobal, + nsIPrincipal* aPrincipal, bool aStorageDisabled, + bool aForceTrustedOrigin, ErrorResult& aRv) +{ + MOZ_DIAGNOSTIC_ASSERT(aGlobal); + MOZ_DIAGNOSTIC_ASSERT(aPrincipal); + MOZ_ASSERT(NS_IsMainThread()); + + if (aStorageDisabled) { + NS_WARNING("CacheStorage has been disabled."); + RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR); + return ref.forget(); + } + + PrincipalInfo principalInfo; + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return nullptr; + } + + bool testingEnabled = aForceTrustedOrigin || + Preferences::GetBool("dom.caches.testing.enabled", false) || + Preferences::GetBool("dom.serviceWorkers.testing.enabled", false); + + if (!IsTrusted(principalInfo, testingEnabled)) { + NS_WARNING("CacheStorage not supported on untrusted origins."); + RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR); + return ref.forget(); + } + + RefPtr<CacheStorage> ref = new CacheStorage(aNamespace, aGlobal, + principalInfo, nullptr); + return ref.forget(); +} + +// static +already_AddRefed<CacheStorage> +CacheStorage::CreateOnWorker(Namespace aNamespace, nsIGlobalObject* aGlobal, + WorkerPrivate* aWorkerPrivate, ErrorResult& aRv) +{ + MOZ_DIAGNOSTIC_ASSERT(aGlobal); + MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + if (!aWorkerPrivate->IsStorageAllowed()) { + NS_WARNING("CacheStorage is not allowed."); + RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR); + return ref.forget(); + } + + if (aWorkerPrivate->GetOriginAttributes().mPrivateBrowsingId > 0) { + NS_WARNING("CacheStorage not supported during private browsing."); + RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR); + return ref.forget(); + } + + RefPtr<CacheWorkerHolder> workerHolder = + CacheWorkerHolder::Create(aWorkerPrivate); + if (!workerHolder) { + NS_WARNING("Worker thread is shutting down."); + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + const PrincipalInfo& principalInfo = aWorkerPrivate->GetPrincipalInfo(); + + // 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 = aWorkerPrivate->DOMCachesTestingEnabled() || + aWorkerPrivate->ServiceWorkersTestingEnabled() || + aWorkerPrivate->ServiceWorkersTestingInWindow() || + aWorkerPrivate->IsServiceWorker(); + + if (!IsTrusted(principalInfo, testingEnabled)) { + NS_WARNING("CacheStorage not supported on untrusted origins."); + RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR); + return ref.forget(); + } + + RefPtr<CacheStorage> ref = new CacheStorage(aNamespace, aGlobal, + principalInfo, workerHolder); + return ref.forget(); +} + +// static +bool +CacheStorage::DefineCaches(JSContext* aCx, JS::Handle<JSObject*> aGlobal) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(js::GetObjectClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL, + "Passed object is not a global object!"); + js::AssertSameCompartment(aCx, aGlobal); + + if (NS_WARN_IF(!CacheStorageBinding::GetConstructorObject(aCx) || + !CacheBinding::GetConstructorObject(aCx))) { + return false; + } + + nsIPrincipal* principal = nsContentUtils::ObjectPrincipal(aGlobal); + MOZ_DIAGNOSTIC_ASSERT(principal); + + ErrorResult rv; + RefPtr<CacheStorage> storage = + CreateOnMainThread(DEFAULT_NAMESPACE, xpc::NativeGlobal(aGlobal), principal, + false, /* private browsing */ + true, /* force trusted */ + rv); + if (NS_WARN_IF(rv.MaybeSetPendingException(aCx))) { + return false; + } + + JS::Rooted<JS::Value> 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, + CacheWorkerHolder* aWorkerHolder) + : mNamespace(aNamespace) + , mGlobal(aGlobal) + , mPrincipalInfo(MakeUnique<PrincipalInfo>(aPrincipalInfo)) + , mWorkerHolder(aWorkerHolder) + , 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::GetForCurrentThread(); + if (actor) { + ActorCreated(actor); + return; + } + + // Otherwise we must begin the PBackground initialization process and + // wait for the async ActorCreated() callback. + MOZ_ASSERT(NS_IsMainThread()); + bool ok = BackgroundChild::GetOrCreateForCurrentThread(this); + if (NS_WARN_IF(!ok)) { + ActorFailed(); + } +} + +CacheStorage::CacheStorage(nsresult aFailureResult) + : mNamespace(INVALID_NAMESPACE) + , mActor(nullptr) + , mStatus(aFailureResult) +{ + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mStatus)); +} + +already_AddRefed<Promise> +CacheStorage::Match(const RequestOrUSVString& aRequest, + const CacheQueryOptions& aOptions, ErrorResult& aRv) +{ + NS_ASSERT_OWNINGTHREAD(CacheStorage); + + if (NS_WARN_IF(NS_FAILED(mStatus))) { + aRv.Throw(mStatus); + return nullptr; + } + + RefPtr<InternalRequest> request = ToInternalRequest(aRequest, IgnoreBody, + aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); + if (NS_WARN_IF(!promise)) { + return nullptr; + } + + CacheQueryParams params; + ToCacheQueryParams(params, aOptions); + + nsAutoPtr<Entry> entry(new Entry()); + entry->mPromise = promise; + entry->mArgs = StorageMatchArgs(CacheRequest(), params); + entry->mRequest = request; + + mPendingRequests.AppendElement(entry.forget()); + MaybeRunPendingRequests(); + + return promise.forget(); +} + +already_AddRefed<Promise> +CacheStorage::Has(const nsAString& aKey, ErrorResult& aRv) +{ + NS_ASSERT_OWNINGTHREAD(CacheStorage); + + if (NS_WARN_IF(NS_FAILED(mStatus))) { + aRv.Throw(mStatus); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); + if (NS_WARN_IF(!promise)) { + return nullptr; + } + + nsAutoPtr<Entry> entry(new Entry()); + entry->mPromise = promise; + entry->mArgs = StorageHasArgs(nsString(aKey)); + + mPendingRequests.AppendElement(entry.forget()); + MaybeRunPendingRequests(); + + return promise.forget(); +} + +already_AddRefed<Promise> +CacheStorage::Open(const nsAString& aKey, ErrorResult& aRv) +{ + NS_ASSERT_OWNINGTHREAD(CacheStorage); + + if (NS_WARN_IF(NS_FAILED(mStatus))) { + aRv.Throw(mStatus); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); + if (NS_WARN_IF(!promise)) { + return nullptr; + } + + nsAutoPtr<Entry> entry(new Entry()); + entry->mPromise = promise; + entry->mArgs = StorageOpenArgs(nsString(aKey)); + + mPendingRequests.AppendElement(entry.forget()); + MaybeRunPendingRequests(); + + return promise.forget(); +} + +already_AddRefed<Promise> +CacheStorage::Delete(const nsAString& aKey, ErrorResult& aRv) +{ + NS_ASSERT_OWNINGTHREAD(CacheStorage); + + if (NS_WARN_IF(NS_FAILED(mStatus))) { + aRv.Throw(mStatus); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); + if (NS_WARN_IF(!promise)) { + return nullptr; + } + + nsAutoPtr<Entry> entry(new Entry()); + entry->mPromise = promise; + entry->mArgs = StorageDeleteArgs(nsString(aKey)); + + mPendingRequests.AppendElement(entry.forget()); + MaybeRunPendingRequests(); + + return promise.forget(); +} + +already_AddRefed<Promise> +CacheStorage::Keys(ErrorResult& aRv) +{ + NS_ASSERT_OWNINGTHREAD(CacheStorage); + + if (NS_WARN_IF(NS_FAILED(mStatus))) { + aRv.Throw(mStatus); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); + if (NS_WARN_IF(!promise)) { + return nullptr; + } + + nsAutoPtr<Entry> entry(new Entry()); + entry->mPromise = promise; + entry->mArgs = StorageKeysArgs(); + + mPendingRequests.AppendElement(entry.forget()); + MaybeRunPendingRequests(); + + return promise.forget(); +} + +// static +bool +CacheStorage::PrefEnabled(JSContext* aCx, JSObject* aObj) +{ + return Cache::PrefEnabled(aCx, aObj); +} + +// static +already_AddRefed<CacheStorage> +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 == (uint32_t)CacheStorageNamespace::EndGuard_, + "Number of namespace should match webidl endguard enum"); + + Namespace ns = static_cast<Namespace>(aNamespace); + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + + bool privateBrowsing = false; + if (nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global)) { + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (doc) { + nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext(); + privateBrowsing = loadContext && loadContext->UsePrivateBrowsing(); + } + } + + // Create a CacheStorage object bypassing the trusted origin checks + // since this is a chrome-only constructor. + return CreateOnMainThread(ns, global, aPrincipal, privateBrowsing, + true /* force trusted origin */, aRv); +} + +nsISupports* +CacheStorage::GetParentObject() const +{ + return mGlobal; +} + +JSObject* +CacheStorage::WrapObject(JSContext* aContext, JS::Handle<JSObject*> aGivenProto) +{ + return mozilla::dom::CacheStorageBinding::Wrap(aContext, this, aGivenProto); +} + +void +CacheStorage::ActorCreated(PBackgroundChild* aActor) +{ + NS_ASSERT_OWNINGTHREAD(CacheStorage); + MOZ_DIAGNOSTIC_ASSERT(aActor); + + if (NS_WARN_IF(mWorkerHolder && mWorkerHolder->Notified())) { + ActorFailed(); + return; + } + + // WorkerHolder ownership is passed to the CacheStorageChild actor and any + // actors it may create. The WorkerHolder will keep the worker thread alive + // until the actors can gracefully shutdown. + CacheStorageChild* newActor = new CacheStorageChild(this, mWorkerHolder); + PCacheStorageChild* constructedActor = + aActor->SendPCacheStorageConstructor(newActor, mNamespace, *mPrincipalInfo); + + if (NS_WARN_IF(!constructedActor)) { + ActorFailed(); + return; + } + + mWorkerHolder = nullptr; + + MOZ_DIAGNOSTIC_ASSERT(constructedActor == newActor); + mActor = newActor; + + MaybeRunPendingRequests(); + MOZ_DIAGNOSTIC_ASSERT(mPendingRequests.IsEmpty()); +} + +void +CacheStorage::ActorFailed() +{ + NS_ASSERT_OWNINGTHREAD(CacheStorage); + MOZ_DIAGNOSTIC_ASSERT(!NS_FAILED(mStatus)); + + mStatus = NS_ERROR_UNEXPECTED; + mWorkerHolder = nullptr; + + for (uint32_t i = 0; i < mPendingRequests.Length(); ++i) { + nsAutoPtr<Entry> entry(mPendingRequests[i].forget()); + entry->mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + } + mPendingRequests.Clear(); +} + +void +CacheStorage::DestroyInternal(CacheStorageChild* aActor) +{ + NS_ASSERT_OWNINGTHREAD(CacheStorage); + MOZ_DIAGNOSTIC_ASSERT(mActor); + MOZ_DIAGNOSTIC_ASSERT(mActor == aActor); + mActor->ClearListener(); + mActor = nullptr; + + // Note that we will never get an actor again in case another request is + // made before this object is destructed. + ActorFailed(); +} + +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::MaybeRunPendingRequests() +{ + if (!mActor) { + return; + } + + for (uint32_t i = 0; i < mPendingRequests.Length(); ++i) { + ErrorResult rv; + nsAutoPtr<Entry> entry(mPendingRequests[i].forget()); + AutoChildOpArgs args(this, entry->mArgs, 1); + if (entry->mRequest) { + args.Add(entry->mRequest, IgnoreBody, IgnoreInvalidScheme, rv); + } + if (NS_WARN_IF(rv.Failed())) { + entry->mPromise->MaybeReject(rv); + continue; + } + mActor->ExecuteOp(mGlobal, entry->mPromise, this, args.SendAsOpArgs()); + } + mPendingRequests.Clear(); +} + +} // namespace cache +} // namespace dom +} // namespace mozilla |