diff options
Diffstat (limited to '')
-rw-r--r-- | uriloader/preload/FetchPreloader.cpp | 365 | ||||
-rw-r--r-- | uriloader/preload/FetchPreloader.h | 108 | ||||
-rw-r--r-- | uriloader/preload/PreloadHashKey.cpp | 213 | ||||
-rw-r--r-- | uriloader/preload/PreloadHashKey.h | 109 | ||||
-rw-r--r-- | uriloader/preload/PreloadService.cpp | 371 | ||||
-rw-r--r-- | uriloader/preload/PreloadService.h | 142 | ||||
-rw-r--r-- | uriloader/preload/PreloaderBase.cpp | 391 | ||||
-rw-r--r-- | uriloader/preload/PreloaderBase.h | 195 | ||||
-rw-r--r-- | uriloader/preload/gtest/TestFetchPreloader.cpp | 954 | ||||
-rw-r--r-- | uriloader/preload/gtest/moz.build | 18 | ||||
-rw-r--r-- | uriloader/preload/moz.build | 26 |
11 files changed, 2892 insertions, 0 deletions
diff --git a/uriloader/preload/FetchPreloader.cpp b/uriloader/preload/FetchPreloader.cpp new file mode 100644 index 0000000000..ac580aeabc --- /dev/null +++ b/uriloader/preload/FetchPreloader.cpp @@ -0,0 +1,365 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "FetchPreloader.h" + +#include "mozilla/Assertions.h" +#include "mozilla/CORSMode.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/dom/Document.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "nsContentPolicyUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIChildChannel.h" +#include "nsIClassOfService.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsISupportsPriority.h" +#include "nsITimedChannel.h" +#include "nsNetUtil.h" +#include "nsStringStream.h" +#include "nsIDocShell.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(FetchPreloader, nsIStreamListener, nsIRequestObserver) + +FetchPreloader::FetchPreloader() + : FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD) {} + +FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType) + : mContentPolicyType(aContentPolicyType) {} + +nsresult FetchPreloader::OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI, + const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, + dom::Document* aDocument, + uint64_t aEarlyHintPreloaderId, + int32_t aSupportsPriorityValue) { + nsresult rv; + nsCOMPtr<nsIChannel> channel; + + auto notify = MakeScopeExit([&]() { + if (NS_FAILED(rv)) { + // Make sure we notify any <link preload> elements when opening fails + // because of various technical or security reasons. + NotifyStart(channel); + // Using the non-channel overload of this method to make it work even + // before NotifyOpen has been called on this preload. We are not + // switching between channels, so it's safe to do so. + NotifyStop(rv); + } + }); + + nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup(); + nsCOMPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow(); + nsCOMPtr<nsIInterfaceRequestor> prompter; + if (window) { + nsIDocShell* docshell = window->GetDocShell(); + prompter = do_QueryInterface(docshell); + } + + rv = CreateChannel(getter_AddRefs(channel), aURI, aCORSMode, aReferrerPolicy, + aDocument, loadGroup, prompter, aEarlyHintPreloaderId, + aSupportsPriorityValue); + NS_ENSURE_SUCCESS(rv, rv); + + // Doing this now so that we have the channel and tainting set on it properly + // to notify the proper event (load or error) on the associated preload tags + // when the CSP check fails. + rv = CheckContentPolicy(aURI, aDocument); + if (NS_FAILED(rv)) { + return rv; + } + + FetchPreloader::PrioritizeAsPreload(channel); + AddLoadBackgroundFlag(channel); + + NotifyOpen(aKey, channel, aDocument, true); + + if (aEarlyHintPreloaderId) { + nsCOMPtr<nsIHttpChannelInternal> channelInternal = + do_QueryInterface(channel); + NS_ENSURE_TRUE(channelInternal != nullptr, NS_ERROR_FAILURE); + + rv = channelInternal->SetEarlyHintPreloaderId(aEarlyHintPreloaderId); + NS_ENSURE_SUCCESS(rv, rv); + } + return mAsyncConsumeResult = rv = channel->AsyncOpen(this); +} + +nsresult FetchPreloader::CreateChannel( + nsIChannel** aChannel, nsIURI* aURI, const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, dom::Document* aDocument, + nsILoadGroup* aLoadGroup, nsIInterfaceRequestor* aCallbacks, + uint64_t aEarlyHintPreloaderId, int32_t aSupportsPriorityValue) { + nsresult rv; + + nsSecurityFlags securityFlags = + nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS); + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannelWithTriggeringPrincipal( + getter_AddRefs(channel), aURI, aDocument, aDocument->NodePrincipal(), + securityFlags, nsIContentPolicy::TYPE_FETCH, nullptr, aLoadGroup, + aCallbacks); + if (NS_FAILED(rv)) { + return rv; + } + + AdjustPriority(channel, aSupportsPriorityValue); + + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) { + nsCOMPtr<nsIReferrerInfo> referrerInfo = new dom::ReferrerInfo( + aDocument->GetDocumentURIAsReferrer(), aReferrerPolicy); + rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(channel)) { + if (aEarlyHintPreloaderId) { + timedChannel->SetInitiatorType(u"early-hints"_ns); + } else { + timedChannel->SetInitiatorType(u"link"_ns); + } + } + + channel.forget(aChannel); + return NS_OK; +} + +// static +void FetchPreloader::AdjustPriority(nsIChannel* aChannel, + int32_t aSupportsPriorityValue) { + if (nsCOMPtr<nsISupportsPriority> supportsPriority{ + do_QueryInterface(aChannel)}) { + supportsPriority->SetPriority(aSupportsPriorityValue); + } +} + +nsresult FetchPreloader::CheckContentPolicy(nsIURI* aURI, + dom::Document* aDocument) { + if (!aDocument) { + return NS_OK; + } + + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo( + aDocument->NodePrincipal(), aDocument->NodePrincipal(), aDocument, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, mContentPolicyType); + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + nsresult rv = NS_CheckContentLoadPolicy(aURI, secCheckLoadInfo, &shouldLoad, + nsContentUtils::GetContentPolicy()); + if (NS_SUCCEEDED(rv) && NS_CP_ACCEPTED(shouldLoad)) { + return NS_OK; + } + + return NS_ERROR_CONTENT_BLOCKED; +} + +// PreloaderBase + +nsresult FetchPreloader::AsyncConsume(nsIStreamListener* aListener) { + if (NS_FAILED(mAsyncConsumeResult)) { + // Already being consumed or failed to open. + return mAsyncConsumeResult; + } + + // Prevent duplicate calls. + mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE; + + if (!mConsumeListener) { + // Called before we are getting response from the channel. + mConsumeListener = aListener; + } else { + // Channel already started, push cached calls to this listener. + // Can't be anything else than the `Cache`, hence a safe static_cast. + Cache* cache = static_cast<Cache*>(mConsumeListener.get()); + cache->AsyncConsume(aListener); + } + + return NS_OK; +} + +// static +void FetchPreloader::PrioritizeAsPreload(nsIChannel* aChannel) { + if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(aChannel)) { + cos->AddClassFlags(nsIClassOfService::Unblocked); + } +} + +// nsIRequestObserver + nsIStreamListener + +NS_IMETHODIMP FetchPreloader::OnStartRequest(nsIRequest* request) { + NotifyStart(request); + + if (!mConsumeListener) { + // AsyncConsume not called yet. + mConsumeListener = new Cache(); + } + + return mConsumeListener->OnStartRequest(request); +} + +NS_IMETHODIMP FetchPreloader::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, uint32_t count) { + return mConsumeListener->OnDataAvailable(request, input, offset, count); +} + +NS_IMETHODIMP FetchPreloader::OnStopRequest(nsIRequest* request, + nsresult status) { + mConsumeListener->OnStopRequest(request, status); + + // We want 404 or other types of server responses to be treated as 'error'. + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request)) { + uint32_t responseStatus = 0; + Unused << httpChannel->GetResponseStatus(&responseStatus); + if (responseStatus / 100 != 2) { + status = NS_ERROR_FAILURE; + } + } + + // Fetch preloader wants to keep the channel around so that consumers like XHR + // can access it even after the preload is done. + nsCOMPtr<nsIChannel> channel = mChannel; + NotifyStop(request, status); + mChannel.swap(channel); + return NS_OK; +} + +// FetchPreloader::Cache + +NS_IMPL_ISUPPORTS(FetchPreloader::Cache, nsIStreamListener, nsIRequestObserver) + +NS_IMETHODIMP FetchPreloader::Cache::OnStartRequest(nsIRequest* request) { + mRequest = request; + + if (mFinalListener) { + return mFinalListener->OnStartRequest(mRequest); + } + + mCalls.AppendElement(Call{VariantIndex<0>{}, StartRequest{}}); + return NS_OK; +} + +NS_IMETHODIMP FetchPreloader::Cache::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, + uint32_t count) { + if (mFinalListener) { + return mFinalListener->OnDataAvailable(mRequest, input, offset, count); + } + + DataAvailable data; + if (!data.mData.SetLength(count, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t read; + nsresult rv = input->Read(data.mData.BeginWriting(), count, &read); + if (NS_FAILED(rv)) { + return rv; + } + + mCalls.AppendElement(Call{VariantIndex<1>{}, std::move(data)}); + return NS_OK; +} + +NS_IMETHODIMP FetchPreloader::Cache::OnStopRequest(nsIRequest* request, + nsresult status) { + if (mFinalListener) { + return mFinalListener->OnStopRequest(mRequest, status); + } + + mCalls.AppendElement(Call{VariantIndex<2>{}, StopRequest{status}}); + return NS_OK; +} + +void FetchPreloader::Cache::AsyncConsume(nsIStreamListener* aListener) { + // Must dispatch for two reasons: + // 1. This is called directly from FetchLoader::AsyncConsume, which must + // behave the same way as nsIChannel::AsyncOpen. + // 2. In case there are already stream listener events scheduled on the main + // thread we preserve the order - those will still end up in Cache. + + // * The `Cache` object is fully main thread only for now, doesn't support + // retargeting, but it can be improved to allow it. + + nsCOMPtr<nsIStreamListener> listener(aListener); + NS_DispatchToMainThread(NewRunnableMethod<nsCOMPtr<nsIStreamListener>>( + "FetchPreloader::Cache::Consume", this, &FetchPreloader::Cache::Consume, + listener)); +} + +void FetchPreloader::Cache::Consume(nsCOMPtr<nsIStreamListener> aListener) { + MOZ_ASSERT(!mFinalListener, "Duplicate call"); + + mFinalListener = std::move(aListener); + + // Status of the channel read after each call. + nsresult status = NS_OK; + nsCOMPtr<nsIChannel> channel(do_QueryInterface(mRequest)); + + RefPtr<Cache> self(this); + for (auto& call : mCalls) { + nsresult rv = call.match( + [&](const StartRequest& startRequest) mutable { + return self->mFinalListener->OnStartRequest(self->mRequest); + }, + [&](const DataAvailable& dataAvailable) mutable { + if (NS_FAILED(status)) { + // Channel has been cancelled during this mCalls loop. + return NS_OK; + } + + nsCOMPtr<nsIInputStream> input; + rv = NS_NewCStringInputStream(getter_AddRefs(input), + dataAvailable.mData); + if (NS_FAILED(rv)) { + return rv; + } + + return self->mFinalListener->OnDataAvailable( + self->mRequest, input, 0, dataAvailable.mData.Length()); + }, + [&](const StopRequest& stopRequest) { + // First cancellation overrides mStatus in nsHttpChannel. + nsresult stopStatus = + NS_FAILED(status) ? status : stopRequest.mStatus; + self->mFinalListener->OnStopRequest(self->mRequest, stopStatus); + self->mFinalListener = nullptr; + self->mRequest = nullptr; + return NS_OK; + }); + + if (!mRequest) { + // We are done! + break; + } + + bool isCancelled = false; + Unused << channel->GetCanceled(&isCancelled); + if (isCancelled) { + mRequest->GetStatus(&status); + } else if (NS_FAILED(rv)) { + status = rv; + mRequest->Cancel(status); + } + } + + mCalls.Clear(); +} + +} // namespace mozilla diff --git a/uriloader/preload/FetchPreloader.h b/uriloader/preload/FetchPreloader.h new file mode 100644 index 0000000000..24c3babd89 --- /dev/null +++ b/uriloader/preload/FetchPreloader.h @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef FetchPreloader_h_ +#define FetchPreloader_h_ + +#include "mozilla/PreloaderBase.h" +#include "mozilla/Variant.h" +#include "nsCOMPtr.h" +#include "nsIAsyncOutputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsIContentPolicy.h" +#include "nsIStreamListener.h" + +class nsIChannel; +class nsILoadGroup; +class nsIInterfaceRequestor; + +namespace mozilla { + +namespace dom { +enum class ReferrerPolicy : uint8_t; +} + +class FetchPreloader : public PreloaderBase, public nsIStreamListener { + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + FetchPreloader(); + + // @param aSupportsPriorityValue see <nsISupportsPriority.idl>. + nsresult OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI, + const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, + dom::Document* aDocument, uint64_t aEarlyHintPreloaderId, + int32_t aSupportsPriorityValue); + + // PreloaderBase + nsresult AsyncConsume(nsIStreamListener* aListener) override; + + static void PrioritizeAsPreload(nsIChannel* aChannel); + + protected: + explicit FetchPreloader(nsContentPolicyType aContentPolicyType); + virtual ~FetchPreloader() = default; + + // Create and setup the channel with necessary security properties and + // the nsISupportsPriority value. This is overridable by + // subclasses to allow different initial conditions. + // + // @param aSupportsPriorityValue see <nsISupportsPriority.idl>. + virtual nsresult CreateChannel( + nsIChannel** aChannel, nsIURI* aURI, const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, dom::Document* aDocument, + nsILoadGroup* aLoadGroup, nsIInterfaceRequestor* aCallbacks, + uint64_t aEarlyHintPreloaderId, int32_t aSupportsPriorityValue); + + private: + // @param aSupportsPriorityValue see <nsISupportsPriority.idl>. + static void AdjustPriority(nsIChannel* aChannel, + int32_t aSupportsPriorityValue); + + nsresult CheckContentPolicy(nsIURI* aURI, dom::Document* aDocument); + + class Cache final : public nsIStreamListener { + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + void AsyncConsume(nsIStreamListener* aListener); + void Consume(nsCOMPtr<nsIStreamListener> aListener); + + private: + virtual ~Cache() = default; + + struct StartRequest {}; + struct DataAvailable { + nsCString mData; + }; + struct StopRequest { + nsresult mStatus; + }; + + typedef Variant<StartRequest, DataAvailable, StopRequest> Call; + nsCOMPtr<nsIRequest> mRequest; + nsCOMPtr<nsIStreamListener> mFinalListener; + nsTArray<Call> mCalls; + }; + + // The listener passed to AsyncConsume in case we haven't started getting the + // data from the channel yet. + nsCOMPtr<nsIStreamListener> mConsumeListener; + + // Returned by AsyncConsume when a failure. This remembers the result of + // opening the channel and prevents duplicate consumation. + nsresult mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE; + + // The CP type to check against. Derived classes have to override call to CSP + // constructor of this class. + nsContentPolicyType mContentPolicyType; +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/preload/PreloadHashKey.cpp b/uriloader/preload/PreloadHashKey.cpp new file mode 100644 index 0000000000..989bb16949 --- /dev/null +++ b/uriloader/preload/PreloadHashKey.cpp @@ -0,0 +1,213 @@ +/* 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 "PreloadHashKey.h" + +#include "mozilla/dom/Element.h" // StringToCORSMode +#include "mozilla/css/SheetLoadData.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "nsIPrincipal.h" +#include "nsIReferrerInfo.h" + +namespace mozilla { + +PreloadHashKey::PreloadHashKey(const nsIURI* aKey, ResourceType aAs) + : nsURIHashKey(aKey), mAs(aAs) {} + +PreloadHashKey::PreloadHashKey(const PreloadHashKey* aKey) + : nsURIHashKey(aKey->mKey) { + *this = *aKey; +} + +PreloadHashKey::PreloadHashKey(PreloadHashKey&& aToMove) + : nsURIHashKey(std::move(aToMove)) { + mAs = std::move(aToMove.mAs); + mCORSMode = std::move(aToMove.mCORSMode); + mPrincipal = std::move(aToMove.mPrincipal); + + switch (mAs) { + case ResourceType::SCRIPT: + mScript = std::move(aToMove.mScript); + break; + case ResourceType::STYLE: + mStyle = std::move(aToMove.mStyle); + break; + case ResourceType::IMAGE: + break; + case ResourceType::FONT: + break; + case ResourceType::FETCH: + break; + case ResourceType::NONE: + break; + } +} + +PreloadHashKey& PreloadHashKey::operator=(const PreloadHashKey& aOther) { + MOZ_ASSERT(mAs == ResourceType::NONE || aOther.mAs == ResourceType::NONE, + "Assigning more than once, only reset is allowed"); + + nsURIHashKey::operator=(aOther); + + mAs = aOther.mAs; + mCORSMode = aOther.mCORSMode; + mPrincipal = aOther.mPrincipal; + + switch (mAs) { + case ResourceType::SCRIPT: + mScript = aOther.mScript; + break; + case ResourceType::STYLE: + mStyle = aOther.mStyle; + break; + case ResourceType::IMAGE: + break; + case ResourceType::FONT: + break; + case ResourceType::FETCH: + break; + case ResourceType::NONE: + break; + } + + return *this; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsScript( + nsIURI* aURI, CORSMode aCORSMode, JS::loader::ScriptKind aScriptKind) { + PreloadHashKey key(aURI, ResourceType::SCRIPT); + key.mCORSMode = aCORSMode; + + key.mScript.mScriptKind = aScriptKind; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsScript(nsIURI* aURI, + const nsAString& aCrossOrigin, + const nsAString& aType) { + JS::loader::ScriptKind scriptKind = JS::loader::ScriptKind::eClassic; + if (aType.LowerCaseEqualsASCII("module")) { + scriptKind = JS::loader::ScriptKind::eModule; + } + CORSMode crossOrigin = dom::Element::StringToCORSMode(aCrossOrigin); + return CreateAsScript(aURI, crossOrigin, scriptKind); +} + +// static +PreloadHashKey PreloadHashKey::CreateAsStyle( + nsIURI* aURI, nsIPrincipal* aPrincipal, CORSMode aCORSMode, + css::SheetParsingMode aParsingMode) { + PreloadHashKey key(aURI, ResourceType::STYLE); + key.mCORSMode = aCORSMode; + key.mPrincipal = aPrincipal; + + key.mStyle.mParsingMode = aParsingMode; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsStyle( + css::SheetLoadData& aSheetLoadData) { + return CreateAsStyle(aSheetLoadData.mURI, aSheetLoadData.mTriggeringPrincipal, + aSheetLoadData.mSheet->GetCORSMode(), + aSheetLoadData.mSheet->ParsingMode()); +} + +// static +PreloadHashKey PreloadHashKey::CreateAsImage(nsIURI* aURI, + nsIPrincipal* aPrincipal, + CORSMode aCORSMode) { + PreloadHashKey key(aURI, ResourceType::IMAGE); + key.mCORSMode = aCORSMode; + key.mPrincipal = aPrincipal; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode) { + PreloadHashKey key(aURI, ResourceType::FETCH); + key.mCORSMode = aCORSMode; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI, + const nsAString& aCrossOrigin) { + return CreateAsFetch(aURI, dom::Element::StringToCORSMode(aCrossOrigin)); +} + +PreloadHashKey PreloadHashKey::CreateAsFont(nsIURI* aURI, CORSMode aCORSMode) { + PreloadHashKey key(aURI, ResourceType::FONT); + key.mCORSMode = aCORSMode; + + return key; +} + +bool PreloadHashKey::KeyEquals(KeyTypePointer aOther) const { + if (mAs != aOther->mAs || mCORSMode != aOther->mCORSMode) { + return false; + } + + if (!mPrincipal != !aOther->mPrincipal) { + // One or the other has a principal, but not both... not equal + return false; + } + + if (mPrincipal && !mPrincipal->Equals(aOther->mPrincipal)) { + return false; + } + + if (!nsURIHashKey::KeyEquals( + static_cast<const nsURIHashKey*>(aOther)->GetKey())) { + return false; + } + + switch (mAs) { + case ResourceType::SCRIPT: + if (mScript.mScriptKind != aOther->mScript.mScriptKind) { + return false; + } + break; + case ResourceType::STYLE: { + if (mStyle.mParsingMode != aOther->mStyle.mParsingMode) { + return false; + } + break; + } + case ResourceType::IMAGE: + // No more checks needed. The image cache key consists of the document + // (which we are scoped into), origin attributes (which we compare as part + // of the principal check) and the URL. imgLoader::ValidateEntry compares + // CORS, referrer info and principal, which we do by default. + break; + case ResourceType::FONT: + break; + case ResourceType::FETCH: + // No more checks needed. + break; + case ResourceType::NONE: + break; + } + + return true; +} + +// static +PLDHashNumber PreloadHashKey::HashKey(KeyTypePointer aKey) { + PLDHashNumber hash = nsURIHashKey::HashKey(aKey->mKey); + + // Enough to use the common attributes + hash = AddToHash(hash, static_cast<uint32_t>(aKey->mAs)); + hash = AddToHash(hash, static_cast<uint32_t>(aKey->mCORSMode)); + + return hash; +} + +} // namespace mozilla diff --git a/uriloader/preload/PreloadHashKey.h b/uriloader/preload/PreloadHashKey.h new file mode 100644 index 0000000000..8db49c1b18 --- /dev/null +++ b/uriloader/preload/PreloadHashKey.h @@ -0,0 +1,109 @@ +/* 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/. */ + +#ifndef PreloadHashKey_h__ +#define PreloadHashKey_h__ + +#include "mozilla/CORSMode.h" +#include "mozilla/css/SheetParsingMode.h" +#include "js/loader/ScriptKind.h" +#include "nsURIHashKey.h" + +class nsIPrincipal; + +namespace JS::loader { +enum class ScriptKind; +} + +namespace mozilla { + +namespace css { +class SheetLoadData; +} + +/** + * This key is used for coalescing and lookup of preloading or regular + * speculative loads. It consists of: + * - the resource type, which is the value of the 'as' attribute + * - the URI of the resource + * - set of attributes that is common to all resource types + * - resource-type-specific attributes that we use to distinguish loads that has + * to be treated separately, some of these attributes may remain at their + * default values + */ +class PreloadHashKey : public nsURIHashKey { + public: + enum class ResourceType : uint8_t { NONE, SCRIPT, STYLE, IMAGE, FONT, FETCH }; + + using KeyType = const PreloadHashKey&; + using KeyTypePointer = const PreloadHashKey*; + + PreloadHashKey() = default; + PreloadHashKey(const nsIURI* aKey, ResourceType aAs); + explicit PreloadHashKey(const PreloadHashKey* aKey); + PreloadHashKey(PreloadHashKey&& aToMove); + + PreloadHashKey& operator=(const PreloadHashKey& aOther); + + // Construct key for "script" + static PreloadHashKey CreateAsScript(nsIURI* aURI, CORSMode aCORSMode, + JS::loader::ScriptKind aScriptKind); + static PreloadHashKey CreateAsScript(nsIURI* aURI, + const nsAString& aCrossOrigin, + const nsAString& aType); + + // Construct key for "style" + static PreloadHashKey CreateAsStyle(nsIURI* aURI, nsIPrincipal* aPrincipal, + CORSMode aCORSMode, + css::SheetParsingMode aParsingMode); + static PreloadHashKey CreateAsStyle(css::SheetLoadData&); + + // Construct key for "image" + static PreloadHashKey CreateAsImage(nsIURI* aURI, nsIPrincipal* aPrincipal, + CORSMode aCORSMode); + + // Construct key for "fetch" + static PreloadHashKey CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode); + static PreloadHashKey CreateAsFetch(nsIURI* aURI, + const nsAString& aCrossOrigin); + + // Construct key for "font" + static PreloadHashKey CreateAsFont(nsIURI* aURI, CORSMode aCORSMode); + + KeyType GetKey() const { return *this; } + KeyTypePointer GetKeyPointer() const { return this; } + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + + bool KeyEquals(KeyTypePointer aOther) const; + static PLDHashNumber HashKey(KeyTypePointer aKey); + ResourceType As() const { return mAs; } + +#ifdef MOZILLA_INTERNAL_API + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + // Bug 1627752 + return 0; + } +#endif + + enum { ALLOW_MEMMOVE = true }; + + private: + // Attributes common to all resource types + ResourceType mAs = ResourceType::NONE; + + CORSMode mCORSMode = CORS_NONE; + nsCOMPtr<nsIPrincipal> mPrincipal; + + struct { + JS::loader::ScriptKind mScriptKind = JS::loader::ScriptKind::eClassic; + } mScript; + + struct { + css::SheetParsingMode mParsingMode = css::eAuthorSheetFeatures; + } mStyle; +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/preload/PreloadService.cpp b/uriloader/preload/PreloadService.cpp new file mode 100644 index 0000000000..5fc59760f0 --- /dev/null +++ b/uriloader/preload/PreloadService.cpp @@ -0,0 +1,371 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "PreloadService.h" + +#include "FetchPreloader.h" +#include "PreloaderBase.h" +#include "mozilla/Assertions.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Maybe.h" +#include "mozilla/dom/FetchPriority.h" +#include "mozilla/dom/HTMLLinkElement.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/Encoding.h" +#include "mozilla/FontPreloader.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsGenericHTMLElement.h" +#include "nsNetUtil.h" + +namespace mozilla { + +using namespace dom; + +static LazyLogModule sPreloadServiceLog{"PreloadService"}; + +PreloadService::PreloadService(dom::Document* aDoc) : mDocument(aDoc) {} +PreloadService::~PreloadService() = default; + +bool PreloadService::RegisterPreload(const PreloadHashKey& aKey, + PreloaderBase* aPreload) { + return mPreloads.WithEntryHandle(aKey, [&](auto&& lookup) { + if (lookup) { + lookup.Data() = aPreload; + return true; + } + lookup.Insert(aPreload); + return false; + }); +} + +void PreloadService::DeregisterPreload(const PreloadHashKey& aKey) { + mPreloads.Remove(aKey); +} + +void PreloadService::ClearAllPreloads() { mPreloads.Clear(); } + +bool PreloadService::PreloadExists(const PreloadHashKey& aKey) { + return mPreloads.Contains(aKey); +} + +already_AddRefed<PreloaderBase> PreloadService::LookupPreload( + const PreloadHashKey& aKey) const { + return mPreloads.Get(aKey); +} + +already_AddRefed<nsIURI> PreloadService::GetPreloadURI(const nsAString& aURL) { + nsIURI* base = BaseURIForPreload(); + auto encoding = mDocument->GetDocumentCharacterSet(); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, encoding, base); + if (NS_FAILED(rv)) { + return nullptr; + } + + return uri.forget(); +} + +already_AddRefed<PreloaderBase> PreloadService::PreloadLinkElement( + dom::HTMLLinkElement* aLinkElement, nsContentPolicyType aPolicyType) { + if (aPolicyType == nsIContentPolicy::TYPE_INVALID) { + MOZ_ASSERT_UNREACHABLE("Caller should check"); + return nullptr; + } + + nsAutoString as, charset, crossOrigin, integrity, referrerPolicy, + fetchPriority, rel, srcset, sizes, type, url; + + nsCOMPtr<nsIURI> uri = aLinkElement->GetURI(); + aLinkElement->GetCharset(charset); + aLinkElement->GetImageSrcset(srcset); + aLinkElement->GetImageSizes(sizes); + aLinkElement->GetHref(url); + aLinkElement->GetCrossOrigin(crossOrigin); + aLinkElement->GetIntegrity(integrity); + aLinkElement->GetReferrerPolicy(referrerPolicy); + aLinkElement->GetFetchPriority(fetchPriority); + aLinkElement->GetRel(rel); + + nsAutoString nonce; + if (nsString* cspNonce = + static_cast<nsString*>(aLinkElement->GetProperty(nsGkAtoms::nonce))) { + nonce = *cspNonce; + } + + if (rel.LowerCaseEqualsASCII("modulepreload")) { + as = u"script"_ns; + type = u"module"_ns; + } else { + aLinkElement->GetAs(as); + aLinkElement->GetType(type); + } + + auto result = PreloadOrCoalesce(uri, url, aPolicyType, as, type, charset, + srcset, sizes, nonce, integrity, crossOrigin, + referrerPolicy, fetchPriority, + /* aFromHeader = */ false, 0); + + if (!result.mPreloader) { + NotifyNodeEvent(aLinkElement, result.mAlreadyComplete); + return nullptr; + } + + result.mPreloader->AddLinkPreloadNode(aLinkElement); + return result.mPreloader.forget(); +} + +void PreloadService::PreloadLinkHeader( + nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType, + const nsAString& aAs, const nsAString& aType, const nsAString& aNonce, + const nsAString& aIntegrity, const nsAString& aSrcset, + const nsAString& aSizes, const nsAString& aCORS, + const nsAString& aReferrerPolicy, uint64_t aEarlyHintPreloaderId, + const nsAString& aFetchPriority) { + if (aPolicyType == nsIContentPolicy::TYPE_INVALID) { + MOZ_ASSERT_UNREACHABLE("Caller should check"); + return; + } + + PreloadOrCoalesce(aURI, aURL, aPolicyType, aAs, aType, u""_ns, aSrcset, + aSizes, aNonce, aIntegrity, aCORS, aReferrerPolicy, + aFetchPriority, + /* aFromHeader = */ true, aEarlyHintPreloaderId); +} + +// The mapping is specified as implementation-defined, see step 15 of +// <https://fetch.spec.whatwg.org/#concept-fetch>. For web-compatibility, +// Chromium's mapping described at +// <https://web.dev/articles/fetch-priority#browser_priority_and_fetchpriority> +// is chosen. +class SupportsPriorityValueFor { + public: + static int32_t LinkRelPreloadFont(const FetchPriority aFetchPriority) { + if (!StaticPrefs::network_fetchpriority_enabled()) { + return nsISupportsPriority::PRIORITY_HIGH; + } + + switch (aFetchPriority) { + case FetchPriority::Auto: + return nsISupportsPriority::PRIORITY_HIGH; + case FetchPriority::High: + return nsISupportsPriority::PRIORITY_HIGH; + case FetchPriority::Low: + return nsISupportsPriority::PRIORITY_LOW; + } + + MOZ_ASSERT_UNREACHABLE(); + return nsISupportsPriority::PRIORITY_HIGH; + } + + static int32_t LinkRelPreloadFetch(const FetchPriority aFetchPriority) { + if (!StaticPrefs::network_fetchpriority_enabled()) { + return nsISupportsPriority::PRIORITY_NORMAL; + } + + switch (aFetchPriority) { + case FetchPriority::Auto: + return nsISupportsPriority::PRIORITY_NORMAL; + case FetchPriority::High: + return nsISupportsPriority::PRIORITY_HIGH; + case FetchPriority::Low: + return nsISupportsPriority::PRIORITY_LOW; + } + + MOZ_ASSERT_UNREACHABLE(); + return nsISupportsPriority::PRIORITY_NORMAL; + } +}; + +PreloadService::PreloadOrCoalesceResult PreloadService::PreloadOrCoalesce( + nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType, + const nsAString& aAs, const nsAString& aType, const nsAString& aCharset, + const nsAString& aSrcset, const nsAString& aSizes, const nsAString& aNonce, + const nsAString& aIntegrity, const nsAString& aCORS, + const nsAString& aReferrerPolicy, const nsAString& aFetchPriority, + bool aFromHeader, uint64_t aEarlyHintPreloaderId) { + if (!aURI) { + MOZ_ASSERT_UNREACHABLE("Should not pass null nsIURI"); + return {nullptr, false}; + } + + bool isImgSet = false; + PreloadHashKey preloadKey; + nsCOMPtr<nsIURI> uri = aURI; + + if (aAs.LowerCaseEqualsASCII("script")) { + preloadKey = PreloadHashKey::CreateAsScript(uri, aCORS, aType); + } else if (aAs.LowerCaseEqualsASCII("style")) { + preloadKey = PreloadHashKey::CreateAsStyle( + uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS), + css::eAuthorSheetFeatures /* see Loader::LoadSheet */); + } else if (aAs.LowerCaseEqualsASCII("image")) { + uri = mDocument->ResolvePreloadImage(BaseURIForPreload(), aURL, aSrcset, + aSizes, &isImgSet); + if (!uri) { + return {nullptr, false}; + } + + preloadKey = PreloadHashKey::CreateAsImage( + uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS)); + } else if (aAs.LowerCaseEqualsASCII("font")) { + preloadKey = PreloadHashKey::CreateAsFont( + uri, dom::Element::StringToCORSMode(aCORS)); + } else if (aAs.LowerCaseEqualsASCII("fetch")) { + preloadKey = PreloadHashKey::CreateAsFetch( + uri, dom::Element::StringToCORSMode(aCORS)); + } else { + return {nullptr, false}; + } + + if (RefPtr<PreloaderBase> preload = LookupPreload(preloadKey)) { + return {std::move(preload), false}; + } + + if (aAs.LowerCaseEqualsASCII("script")) { + PreloadScript(uri, aType, aCharset, aCORS, aReferrerPolicy, aNonce, + aFetchPriority, aIntegrity, true /* isInHead - TODO */, + aEarlyHintPreloaderId); + } else if (aAs.LowerCaseEqualsASCII("style")) { + auto status = mDocument->PreloadStyle( + aURI, Encoding::ForLabel(aCharset), aCORS, + PreloadReferrerPolicy(aReferrerPolicy), aNonce, aIntegrity, + aFromHeader ? css::StylePreloadKind::FromLinkRelPreloadHeader + : css::StylePreloadKind::FromLinkRelPreloadElement, + aEarlyHintPreloaderId, aFetchPriority); + switch (status) { + case dom::SheetPreloadStatus::AlreadyComplete: + return {nullptr, /* already_complete = */ true}; + case dom::SheetPreloadStatus::Errored: + case dom::SheetPreloadStatus::InProgress: + break; + } + } else if (aAs.LowerCaseEqualsASCII("image")) { + PreloadImage(uri, aCORS, aReferrerPolicy, isImgSet, aEarlyHintPreloaderId); + } else if (aAs.LowerCaseEqualsASCII("font")) { + PreloadFont(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId, + aFetchPriority); + } else if (aAs.LowerCaseEqualsASCII("fetch")) { + PreloadFetch(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId, + aFetchPriority); + } + + RefPtr<PreloaderBase> preload = LookupPreload(preloadKey); + if (preload && aEarlyHintPreloaderId) { + preload->SetForEarlyHints(); + } + + return {preload, false}; +} + +void PreloadService::PreloadScript( + nsIURI* aURI, const nsAString& aType, const nsAString& aCharset, + const nsAString& aCrossOrigin, const nsAString& aReferrerPolicy, + const nsAString& aNonce, const nsAString& aFetchPriority, + const nsAString& aIntegrity, bool aScriptFromHead, + uint64_t aEarlyHintPreloaderId) { + mDocument->ScriptLoader()->PreloadURI( + aURI, aCharset, aType, aCrossOrigin, aNonce, aFetchPriority, aIntegrity, + aScriptFromHead, false, false, true, + PreloadReferrerPolicy(aReferrerPolicy), aEarlyHintPreloaderId); +} + +void PreloadService::PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aImageReferrerPolicy, + bool aIsImgSet, + uint64_t aEarlyHintPreloaderId) { + mDocument->PreLoadImage(aURI, aCrossOrigin, + PreloadReferrerPolicy(aImageReferrerPolicy), + aIsImgSet, true, aEarlyHintPreloaderId); +} + +void PreloadService::PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId, + const nsAString& aFetchPriority) { + CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin); + auto key = PreloadHashKey::CreateAsFont(aURI, cors); + + if (PreloadExists(key)) { + return; + } + + const auto fetchPriority = + nsGenericHTMLElement::ToFetchPriority(aFetchPriority); + const auto supportsPriorityValue = + SupportsPriorityValueFor::LinkRelPreloadFont(fetchPriority); + LogPriorityMapping(sPreloadServiceLog, fetchPriority, supportsPriorityValue); + + RefPtr<FontPreloader> preloader = new FontPreloader(); + dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy); + preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument, + aEarlyHintPreloaderId, supportsPriorityValue); +} + +void PreloadService::PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId, + const nsAString& aFetchPriority) { + CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin); + auto key = PreloadHashKey::CreateAsFetch(aURI, cors); + + if (PreloadExists(key)) { + return; + } + + RefPtr<FetchPreloader> preloader = new FetchPreloader(); + dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy); + + const auto fetchPriority = + nsGenericHTMLElement::ToFetchPriority(aFetchPriority); + const int32_t supportsPriorityValue = + SupportsPriorityValueFor::LinkRelPreloadFetch(fetchPriority); + if (supportsPriorityValue) { + LogPriorityMapping(sPreloadServiceLog, fetchPriority, + supportsPriorityValue); + } + + preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument, + aEarlyHintPreloaderId, supportsPriorityValue); +} + +// static +void PreloadService::NotifyNodeEvent(nsINode* aNode, bool aSuccess) { + if (!aNode->IsInComposedDoc()) { + return; + } + + // We don't dispatch synchronously since |node| might be in a DocGroup + // that we're not allowed to touch. (Our network request happens in the + // DocGroup of one of the mSources nodes--not necessarily this one). + + RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher( + aNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo); + + dispatcher->RequireNodeInDocument(); + dispatcher->PostDOMEvent(); +} + +dom::ReferrerPolicy PreloadService::PreloadReferrerPolicy( + const nsAString& aReferrerPolicy) { + dom::ReferrerPolicy referrerPolicy = + dom::ReferrerInfo::ReferrerPolicyAttributeFromString(aReferrerPolicy); + if (referrerPolicy == dom::ReferrerPolicy::_empty) { + referrerPolicy = mDocument->GetPreloadReferrerInfo()->ReferrerPolicy(); + } + + return referrerPolicy; +} + +nsIURI* PreloadService::BaseURIForPreload() { + nsIURI* documentURI = mDocument->GetDocumentURI(); + nsIURI* documentBaseURI = mDocument->GetDocBaseURI(); + return (documentURI == documentBaseURI) + ? (mSpeculationBaseURI ? mSpeculationBaseURI.get() : documentURI) + : documentBaseURI; +} + +} // namespace mozilla diff --git a/uriloader/preload/PreloadService.h b/uriloader/preload/PreloadService.h new file mode 100644 index 0000000000..5f6566ea34 --- /dev/null +++ b/uriloader/preload/PreloadService.h @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef PreloadService_h__ +#define PreloadService_h__ + +#include "nsIContentPolicy.h" +#include "nsIURI.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/PreloadHashKey.h" + +class nsINode; + +namespace mozilla { + +class PreloaderBase; + +namespace dom { + +class HTMLLinkElement; +class Document; +enum class ReferrerPolicy : uint8_t; + +} // namespace dom + +/** + * Intended to scope preloads + * (https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload) + * and speculative loads initiated by the parser under one roof. This class + * is intended to be a member of dom::Document. Provides registration of + * speculative loads via a `key` which is defined to consist of the URL, + * resource type, and resource-specific attributes that are further + * distinguishing loads with otherwise same type and url. + */ +class PreloadService { + public: + explicit PreloadService(dom::Document*); + ~PreloadService(); + + // Called by resource loaders to register a running resource load. This is + // called for a speculative load when it's started the first time, being it + // either regular speculative load or a preload. + // + // Returns false and does nothing if a preload is already registered under + // this key, true otherwise. + bool RegisterPreload(const PreloadHashKey& aKey, PreloaderBase* aPreload); + + // Called when the load is about to be cancelled. Exact behavior is to be + // determined yet. + void DeregisterPreload(const PreloadHashKey& aKey); + + // Called when the scope is to go away. + void ClearAllPreloads(); + + // True when there is a preload registered under the key. + bool PreloadExists(const PreloadHashKey& aKey); + + // Returns an existing preload under the key or null, when there is none + // registered under the key. + already_AddRefed<PreloaderBase> LookupPreload( + const PreloadHashKey& aKey) const; + + void SetSpeculationBase(nsIURI* aURI) { mSpeculationBaseURI = aURI; } + already_AddRefed<nsIURI> GetPreloadURI(const nsAString& aURL); + + already_AddRefed<PreloaderBase> PreloadLinkElement( + dom::HTMLLinkElement* aLink, nsContentPolicyType aPolicyType); + + // a non-zero aEarlyHintPreloaderId tells this service that a preload for this + // link was started by the EarlyHintPreloader and the preloaders should + // connect back by setting earlyHintPreloaderId in nsIChannelInternal before + // AsyncOpen. + void PreloadLinkHeader(nsIURI* aURI, const nsAString& aURL, + nsContentPolicyType aPolicyType, const nsAString& aAs, + const nsAString& aType, const nsAString& aNonce, + const nsAString& aIntegrity, const nsAString& aSrcset, + const nsAString& aSizes, const nsAString& aCORS, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId, + const nsAString& aFetchPriority); + + void PreloadScript(nsIURI* aURI, const nsAString& aType, + const nsAString& aCharset, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, const nsAString& aNonce, + const nsAString& aFetchPriority, + const nsAString& aIntegrity, bool aScriptFromHead, + uint64_t aEarlyHintPreloaderId); + + void PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aImageReferrerPolicy, bool aIsImgSet, + uint64_t aEarlyHintPreloaderId); + + void PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId, + const nsAString& aFetchPriority); + + void PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + uint64_t aEarlyHintPreloaderId, + const nsAString& aFetchPriority); + + static void NotifyNodeEvent(nsINode* aNode, bool aSuccess); + + void SetEarlyHintUsed() { mEarlyHintUsed = true; } + bool GetEarlyHintUsed() const { return mEarlyHintUsed; } + + private: + dom::ReferrerPolicy PreloadReferrerPolicy(const nsAString& aReferrerPolicy); + nsIURI* BaseURIForPreload(); + + struct PreloadOrCoalesceResult { + RefPtr<PreloaderBase> mPreloader; + bool mAlreadyComplete; + }; + + PreloadOrCoalesceResult PreloadOrCoalesce( + nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType, + const nsAString& aAs, const nsAString& aType, const nsAString& aCharset, + const nsAString& aSrcset, const nsAString& aSizes, + const nsAString& aNonce, const nsAString& aIntegrity, + const nsAString& aCORS, const nsAString& aReferrerPolicy, + const nsAString& aFetchPriority, bool aFromHeader, + uint64_t aEarlyHintPreloaderId); + + private: + nsRefPtrHashtable<PreloadHashKey, PreloaderBase> mPreloads; + + // Raw pointer only, we are intended to be a direct member of dom::Document + dom::Document* mDocument; + + // Set by `nsHtml5TreeOpExecutor::SetSpeculationBase`. + nsCOMPtr<nsIURI> mSpeculationBaseURI; + + bool mEarlyHintUsed = false; +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/preload/PreloaderBase.cpp b/uriloader/preload/PreloaderBase.cpp new file mode 100644 index 0000000000..2c3d4fb60b --- /dev/null +++ b/uriloader/preload/PreloaderBase.cpp @@ -0,0 +1,391 @@ +/* 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 "PreloaderBase.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/Telemetry.h" +#include "nsContentUtils.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIHttpChannel.h" +#include "nsILoadGroup.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIRedirectResultListener.h" +#include "nsNetUtil.h" + +// Change this if we want to cancel and remove the associated preload on removal +// of all <link rel=preload> tags from the tree. +constexpr static bool kCancelAndRemovePreloadOnZeroReferences = false; + +namespace mozilla { + +PreloaderBase::UsageTimer::UsageTimer(PreloaderBase* aPreload, + dom::Document* aDocument) + : mDocument(aDocument), mPreload(aPreload) {} + +class PreloaderBase::RedirectSink final : public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsIRedirectResultListener { + RedirectSink() = delete; + virtual ~RedirectSink(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIREDIRECTRESULTLISTENER + + RedirectSink(PreloaderBase* aPreloader, nsIInterfaceRequestor* aCallbacks); + + private: + MainThreadWeakPtr<PreloaderBase> mPreloader; + nsCOMPtr<nsIInterfaceRequestor> mCallbacks; + nsCOMPtr<nsIChannel> mRedirectChannel; +}; + +PreloaderBase::RedirectSink::RedirectSink(PreloaderBase* aPreloader, + nsIInterfaceRequestor* aCallbacks) + : mPreloader(aPreloader), mCallbacks(aCallbacks) {} + +PreloaderBase::RedirectSink::~RedirectSink() = default; + +NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink, nsIInterfaceRequestor, + nsIChannelEventSink, nsIRedirectResultListener) + +NS_IMETHODIMP PreloaderBase::RedirectSink::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCallback) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + mRedirectChannel = aNewChannel; + + // Deliberately adding this before confirmation. + nsCOMPtr<nsIURI> uri; + aNewChannel->GetOriginalURI(getter_AddRefs(uri)); + if (mPreloader) { + mPreloader->mRedirectRecords.AppendElement( + RedirectRecord(aFlags, uri.forget())); + } + + if (mCallbacks) { + nsCOMPtr<nsIChannelEventSink> sink(do_GetInterface(mCallbacks)); + if (sink) { + return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, + aCallback); + } + } + + aCallback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP PreloaderBase::RedirectSink::OnRedirectResult(nsresult status) { + if (NS_SUCCEEDED(status) && mRedirectChannel) { + mPreloader->mChannel = std::move(mRedirectChannel); + } else { + mRedirectChannel = nullptr; + } + + if (mCallbacks) { + nsCOMPtr<nsIRedirectResultListener> sink(do_GetInterface(mCallbacks)); + if (sink) { + return sink->OnRedirectResult(status); + } + } + + return NS_OK; +} + +NS_IMETHODIMP PreloaderBase::RedirectSink::GetInterface(const nsIID& aIID, + void** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink)) || + aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + return QueryInterface(aIID, aResult); + } + + if (mCallbacks) { + return mCallbacks->GetInterface(aIID, aResult); + } + + *aResult = nullptr; + return NS_ERROR_NO_INTERFACE; +} + +PreloaderBase::~PreloaderBase() { MOZ_ASSERT(NS_IsMainThread()); } + +// static +void PreloaderBase::AddLoadBackgroundFlag(nsIChannel* aChannel) { + nsLoadFlags loadFlags; + aChannel->GetLoadFlags(&loadFlags); + aChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND); +} + +void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, + dom::Document* aDocument, bool aIsPreload, + bool aIsModule) { + if (aDocument) { + DebugOnly<bool> alreadyRegistered = + aDocument->Preloads().RegisterPreload(aKey, this); + // This means there is already a preload registered under this key in this + // document. We only allow replacement when this is a regular load. + // Otherwise, this should never happen and is a suspected misuse of the API. + MOZ_ASSERT_IF(alreadyRegistered, !aIsPreload); + } + + mKey = aKey; + mIsUsed = !aIsPreload; + + // Start usage timer for rel="preload", but not for rel="modulepreload" + // because modules may be loaded for functionality the user does not + // immediately interact with after page load (e.g. a docs search box) + if (!aIsModule && !mIsUsed && !mUsageTimer) { + auto callback = MakeRefPtr<UsageTimer>(this, aDocument); + NS_NewTimerWithCallback(getter_AddRefs(mUsageTimer), callback, 10000, + nsITimer::TYPE_ONE_SHOT); + } + + ReportUsageTelemetry(); +} + +void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel, + dom::Document* aDocument, bool aIsPreload, + bool aIsModule) { + NotifyOpen(aKey, aDocument, aIsPreload, aIsModule); + mChannel = aChannel; + + nsCOMPtr<nsIInterfaceRequestor> callbacks; + mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); + RefPtr<RedirectSink> sink(new RedirectSink(this, callbacks)); + mChannel->SetNotificationCallbacks(sink); +} + +void PreloaderBase::NotifyUsage(dom::Document* aDocument, + LoadBackground aLoadBackground) { + if (!mIsUsed && mChannel && aLoadBackground == LoadBackground::Drop) { + nsLoadFlags loadFlags; + mChannel->GetLoadFlags(&loadFlags); + + // Preloads are initially set the LOAD_BACKGROUND flag. When becoming + // regular loads by hitting its consuming tag, we need to drop that flag, + // which also means to re-add the request from/to it's loadgroup to reflect + // that flag change. + if (loadFlags & nsIRequest::LOAD_BACKGROUND) { + nsCOMPtr<nsILoadGroup> loadGroup; + mChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + + if (loadGroup) { + nsresult status; + mChannel->GetStatus(&status); + + nsresult rv = loadGroup->RemoveRequest(mChannel, nullptr, status); + mChannel->SetLoadFlags(loadFlags & ~nsIRequest::LOAD_BACKGROUND); + if (NS_SUCCEEDED(rv)) { + loadGroup->AddRequest(mChannel, nullptr); + } + } + } + } + + mIsUsed = true; + ReportUsageTelemetry(); + CancelUsageTimer(); + if (mIsEarlyHintsPreload) { + aDocument->Preloads().SetEarlyHintUsed(); + } +} + +void PreloaderBase::RemoveSelf(dom::Document* aDocument) { + if (aDocument) { + aDocument->Preloads().DeregisterPreload(mKey); + } +} + +void PreloaderBase::NotifyRestart(dom::Document* aDocument, + PreloaderBase* aNewPreloader) { + RemoveSelf(aDocument); + mKey = PreloadHashKey(); + + CancelUsageTimer(); + + if (aNewPreloader) { + aNewPreloader->mNodes = std::move(mNodes); + } +} + +void PreloaderBase::NotifyStart(nsIRequest* aRequest) { + // If there is no channel assigned on this preloader, we are not between + // channel switching, so we can freely update the mShouldFireLoadEvent using + // the given channel. + if (mChannel && !SameCOMIdentity(aRequest, mChannel)) { + return; + } + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + if (!httpChannel) { + return; + } + + // if the load is cross origin without CORS, or the CORS access is rejected, + // always fire load event to avoid leaking site information. + nsresult rv; + nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo(); + mShouldFireLoadEvent = + loadInfo->GetTainting() == LoadTainting::Opaque || + (loadInfo->GetTainting() == LoadTainting::CORS && + (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv))); +} + +void PreloaderBase::NotifyStop(nsIRequest* aRequest, nsresult aStatus) { + // Filter out notifications that may be arriving from the old channel before + // restarting this request. + if (!SameCOMIdentity(aRequest, mChannel)) { + return; + } + + NotifyStop(aStatus); +} + +void PreloaderBase::NotifyStop(nsresult aStatus) { + mOnStopStatus.emplace(aStatus); + + nsTArray<nsWeakPtr> nodes = std::move(mNodes); + + for (nsWeakPtr& weak : nodes) { + nsCOMPtr<nsINode> node = do_QueryReferent(weak); + if (node) { + NotifyNodeEvent(node); + } + } + + mChannel = nullptr; +} + +void PreloaderBase::AddLinkPreloadNode(nsINode* aNode) { + if (mOnStopStatus) { + return NotifyNodeEvent(aNode); + } + + mNodes.AppendElement(do_GetWeakReference(aNode)); +} + +void PreloaderBase::RemoveLinkPreloadNode(nsINode* aNode) { + // Note that do_GetWeakReference returns the internal weak proxy, which is + // always the same, so we can use it to search the array using default + // comparator. + nsWeakPtr node = do_GetWeakReference(aNode); + mNodes.RemoveElement(node); + + if (kCancelAndRemovePreloadOnZeroReferences && mNodes.Length() == 0 && + !mIsUsed) { + // Keep a reference, because the following call may release us. The caller + // may use a WeakPtr to access this. + RefPtr<PreloaderBase> self(this); + RemoveSelf(aNode->OwnerDoc()); + + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "PreloaderBase::RemoveLinkPreloadNode"_ns); + } + } +} + +void PreloaderBase::NotifyNodeEvent(nsINode* aNode) { + PreloadService::NotifyNodeEvent( + aNode, mShouldFireLoadEvent || NS_SUCCEEDED(*mOnStopStatus)); +} + +void PreloaderBase::CancelUsageTimer() { + if (mUsageTimer) { + mUsageTimer->Cancel(); + mUsageTimer = nullptr; + } +} + +void PreloaderBase::ReportUsageTelemetry() { + if (mUsageTelementryReported) { + return; + } + mUsageTelementryReported = true; + + if (mKey.As() == PreloadHashKey::ResourceType::NONE) { + return; + } + + // The labels are structured as type1-used, type1-unused, type2-used, ... + // The first "as" resource type is NONE with value 0. + auto index = (static_cast<uint32_t>(mKey.As()) - 1) * 2; + if (!mIsUsed) { + ++index; + } + + auto label = static_cast<Telemetry::LABELS_REL_PRELOAD_MISS_RATIO>(index); + Telemetry::AccumulateCategorical(label); +} + +nsresult PreloaderBase::AsyncConsume(nsIStreamListener* aListener) { + // We want to return an error so that consumers can't ever use a preload to + // consume data unless it's properly implemented. + return NS_ERROR_NOT_IMPLEMENTED; +} + +// PreloaderBase::RedirectRecord + +nsCString PreloaderBase::RedirectRecord::Spec() const { + nsCOMPtr<nsIURI> noFragment; + NS_GetURIWithoutRef(mURI, getter_AddRefs(noFragment)); + MOZ_ASSERT(noFragment); + return noFragment->GetSpecOrDefault(); +} + +nsCString PreloaderBase::RedirectRecord::Fragment() const { + nsCString fragment; + mURI->GetRef(fragment); + return fragment; +} + +// PreloaderBase::UsageTimer + +NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer, nsITimerCallback, nsINamed) + +NS_IMETHODIMP PreloaderBase::UsageTimer::Notify(nsITimer* aTimer) { + if (!mPreload || !mDocument) { + return NS_OK; + } + + MOZ_ASSERT(aTimer == mPreload->mUsageTimer); + mPreload->mUsageTimer = nullptr; + + if (mPreload->IsUsed()) { + // Left in the hashtable, but marked as used. This is a valid case, and we + // don't want to emit a warning for this preload then. + return NS_OK; + } + + mPreload->ReportUsageTelemetry(); + + // PreloadHashKey overrides GetKey, we need to use the nsURIHashKey one to get + // the URI. + nsIURI* uri = static_cast<nsURIHashKey*>(&mPreload->mKey)->GetKey(); + if (!uri) { + return NS_OK; + } + + nsString spec; + NS_GetSanitizedURIStringFromURI(uri, spec); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + mDocument, nsContentUtils::eDOM_PROPERTIES, + "UnusedLinkPreloadPending", + nsTArray<nsString>({std::move(spec)})); + return NS_OK; +} + +NS_IMETHODIMP +PreloaderBase::UsageTimer::GetName(nsACString& aName) { + aName.AssignLiteral("PreloaderBase::UsageTimer"); + return NS_OK; +} + +} // namespace mozilla diff --git a/uriloader/preload/PreloaderBase.h b/uriloader/preload/PreloaderBase.h new file mode 100644 index 0000000000..d5d232b73b --- /dev/null +++ b/uriloader/preload/PreloaderBase.h @@ -0,0 +1,195 @@ +/* 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/. */ + +#ifndef PreloaderBase_h__ +#define PreloaderBase_h__ + +#include "mozilla/Maybe.h" +#include "mozilla/PreloadHashKey.h" +#include "mozilla/WeakPtr.h" +#include "nsCOMPtr.h" +#include "nsISupports.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsIWeakReferenceUtils.h" +#include "nsTArray.h" + +class nsIChannel; +class nsINode; +class nsIRequest; +class nsIStreamListener; + +namespace mozilla { + +namespace dom { + +class Document; + +} // namespace dom + +/** + * A half-abstract base class that resource loaders' respective + * channel-listening classes should derive from. Provides a unified API to + * register the load or preload in a document scoped service, links <link + * rel="preload"> DOM nodes with the load progress and provides API to possibly + * consume the data by later, dynamically discovered consumers. + * + * This class is designed to be used only on the main thread. + */ +class PreloaderBase : public SupportsWeakPtr, public nsISupports { + public: + PreloaderBase() = default; + + // Called by resource loaders to register this preload in the document's + // preload service to provide coalescing, and access to the preload when it + // should be used for an actual load. + void NotifyOpen(const PreloadHashKey& aKey, dom::Document* aDocument, + bool aIsPreload, bool aIsModule = false); + void NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel, + dom::Document* aDocument, bool aIsPreload, + bool aIsModule = false); + + // Called when the load is about to be started all over again and thus this + // PreloaderBase will be registered again with the same key. This method + // taks care to deregister this preload prior to that. + // @param aNewPreloader: If there is a new request and loader created for the + // restarted load, we will pass any necessary information into it. + void NotifyRestart(dom::Document* aDocument, + PreloaderBase* aNewPreloader = nullptr); + + // Called by the loading object when the channel started to load + // (OnStartRequest or equal) and when it finished (OnStopRequest or equal) + void NotifyStart(nsIRequest* aRequest); + void NotifyStop(nsIRequest* aRequest, nsresult aStatus); + // Use this variant only in complement to NotifyOpen without providing a + // channel. + void NotifyStop(nsresult aStatus); + + // Called by resource loaders or any suitable component to notify the preload + // has been used for an actual load. This is intended to stop any usage + // timers. + // @param aDropLoadBackground: If `Keep` then the loading channel, if still in + // progress, will not be removed the LOAD_BACKGROUND flag, for instance XHR is + // the user here. + enum class LoadBackground { Keep, Drop }; + void NotifyUsage(dom::Document* aDocument, + LoadBackground aLoadBackground = LoadBackground::Drop); + // Whether this preloader has been used for a regular/actual load or not. + bool IsUsed() const { return mIsUsed; } + + // Removes itself from the document's preloads hashtable + void RemoveSelf(dom::Document* aDocument); + + // When a loader starting an actual load finds a preload, the data can be + // delivered using this method. It will deliver stream listener notifications + // as if it were coming from the resource loading channel. The |request| + // argument will be the channel that loaded/loads the resource. + // This method must keep to the nsIChannel.AsyncOpen contract. A loader is + // not obligated to re-implement this method when not necessarily needed. + virtual nsresult AsyncConsume(nsIStreamListener* aListener); + + // Accessor to the resource loading channel. + nsIChannel* Channel() const { return mChannel; } + + // Helper function to set the LOAD_BACKGROUND flag on channel initiated by + // <link rel=preload>. This MUST be used before the channel is AsyncOpen'ed. + static void AddLoadBackgroundFlag(nsIChannel* aChannel); + + // These are linking this preload to <link rel="preload"> DOM nodes. If we + // are already loaded, immediately notify events on the node, otherwise wait + // for NotifyStop() call. + void AddLinkPreloadNode(nsINode* aNode); + void RemoveLinkPreloadNode(nsINode* aNode); + + // A collection of redirects, the main consumer is fetch. + class RedirectRecord { + public: + RedirectRecord(uint32_t aFlags, already_AddRefed<nsIURI> aURI) + : mFlags(aFlags), mURI(aURI) {} + + uint32_t Flags() const { return mFlags; } + nsCString Spec() const; + nsCString Fragment() const; + + private: + uint32_t mFlags; + nsCOMPtr<nsIURI> mURI; + }; + + const nsTArray<RedirectRecord>& Redirects() { return mRedirectRecords; } + + void SetForEarlyHints() { mIsEarlyHintsPreload = true; } + + protected: + virtual ~PreloaderBase(); + + // The loading channel. This will update when a redirect occurs. + nsCOMPtr<nsIChannel> mChannel; + + private: + void NotifyNodeEvent(nsINode* aNode); + void CancelUsageTimer(); + + void ReportUsageTelemetry(); + + // A helper class that will update the PreloaderBase.mChannel member when a + // redirect happens, so that we can reprioritize or cancel when needed. + // Having a separate class instead of implementing this on PreloaderBase + // directly is to keep PreloaderBase as simple as possible so that derived + // classes don't have to deal with calling super when implementing these + // interfaces from some reason as well. + class RedirectSink; + + // A timer callback to trigger the unuse warning for this preload + class UsageTimer final : public nsITimerCallback, public nsINamed { + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + UsageTimer(PreloaderBase* aPreload, dom::Document* aDocument); + + private: + ~UsageTimer() = default; + + WeakPtr<dom::Document> mDocument; + WeakPtr<PreloaderBase> mPreload; + }; + + private: + // Reference to HTMLLinkElement DOM nodes to deliver onload and onerror + // notifications to. + nsTArray<nsWeakPtr> mNodes; + + // History of redirects. + nsTArray<RedirectRecord> mRedirectRecords; + + // Usage timer, emits warning when the preload is not used in time. Started + // in NotifyOpen and stopped in NotifyUsage. + nsCOMPtr<nsITimer> mUsageTimer; + + // The key this preload has been registered under. We want to remember it to + // be able to deregister itself from the document's preloads. + PreloadHashKey mKey; + + // This overrides the final event we send to DOM nodes to be always 'load'. + // Modified in NotifyStart based on LoadInfo data of the loading channel. + bool mShouldFireLoadEvent = false; + + // True after call to NotifyUsage. + bool mIsUsed = false; + + // True after we have reported the usage telemetry. Prevent duplicates. + bool mUsageTelementryReported = false; + + // True when this is used to Early Hints preload. + bool mIsEarlyHintsPreload = false; + + // Emplaced when the data delivery has finished, in NotifyStop, holds the + // result of the load. + Maybe<nsresult> mOnStopStatus; +}; + +} // namespace mozilla + +#endif // !PreloaderBase_h__ diff --git a/uriloader/preload/gtest/TestFetchPreloader.cpp b/uriloader/preload/gtest/TestFetchPreloader.cpp new file mode 100644 index 0000000000..9f0614f84a --- /dev/null +++ b/uriloader/preload/gtest/TestFetchPreloader.cpp @@ -0,0 +1,954 @@ +#include "gtest/gtest.h" + +#include "mozilla/CORSMode.h" +#include "mozilla/dom/XMLDocument.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "mozilla/FetchPreloader.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/PreloadHashKey.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsIStreamListener.h" +#include "nsISupportsPriority.h" +#include "nsThreadUtils.h" +#include "nsStringStream.h" + +namespace { + +auto const ERROR_CANCEL = NS_ERROR_ABORT; +auto const ERROR_ONSTOP = NS_ERROR_NET_INTERRUPT; +auto const ERROR_THROW = NS_ERROR_FAILURE; + +class FakeChannel : public nsIChannel { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUEST + + nsresult Start() { return mListener->OnStartRequest(this); } + nsresult Data(const nsACString& aData) { + if (NS_FAILED(mStatus)) { + return mStatus; + } + nsCOMPtr<nsIInputStream> is; + NS_NewCStringInputStream(getter_AddRefs(is), aData); + return mListener->OnDataAvailable(this, is, 0, aData.Length()); + } + nsresult Stop(nsresult status) { + if (NS_SUCCEEDED(mStatus)) { + mStatus = status; + } + mListener->OnStopRequest(this, mStatus); + mListener = nullptr; + return mStatus; + } + + private: + virtual ~FakeChannel() = default; + bool mIsPending = false; + bool mCanceled = false; + nsresult mStatus = NS_OK; + nsCOMPtr<nsIStreamListener> mListener; +}; + +NS_IMPL_ISUPPORTS(FakeChannel, nsIChannel, nsIRequest) + +NS_IMETHODIMP FakeChannel::GetName(nsACString& result) { return NS_OK; } +NS_IMETHODIMP FakeChannel::IsPending(bool* result) { + *result = mIsPending; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetStatus(nsresult* status) { + *status = mStatus; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} +NS_IMETHODIMP FakeChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} +NS_IMETHODIMP FakeChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} +NS_IMETHODIMP FakeChannel::Cancel(nsresult status) { + if (!mCanceled) { + mCanceled = true; + mStatus = status; + } + return NS_OK; +} +NS_IMETHODIMP FakeChannel::Suspend() { return NS_OK; } +NS_IMETHODIMP FakeChannel::Resume() { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = 0; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP FakeChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP FakeChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetOriginalURI(nsIURI** aURI) { return NS_OK; } +NS_IMETHODIMP FakeChannel::SetOriginalURI(nsIURI* aURI) { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetURI(nsIURI** aURI) { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetOwner(nsISupports** aOwner) { return NS_OK; } +NS_IMETHODIMP FakeChannel::SetOwner(nsISupports* aOwner) { return NS_OK; } +NS_IMETHODIMP FakeChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetIsDocument(bool* aIsDocument) { + *aIsDocument = false; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentType(nsACString& aContentType) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentType(const nsACString& aContentType) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentCharset(nsACString& aContentCharset) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentCharset( + const nsACString& aContentCharset) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentDisposition( + uint32_t* aContentDisposition) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} +NS_IMETHODIMP FakeChannel::GetContentLength(int64_t* aContentLength) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentLength(int64_t aContentLength) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetCanceled(bool* aCanceled) { + *aCanceled = mCanceled; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::Open(nsIInputStream** aStream) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP +FakeChannel::AsyncOpen(nsIStreamListener* aListener) { + mIsPending = true; + mListener = aListener; + return NS_OK; +} + +class FakePreloader : public mozilla::FetchPreloader { + public: + explicit FakePreloader(FakeChannel* aChannel) : mDrivingChannel(aChannel) {} + + private: + RefPtr<FakeChannel> mDrivingChannel; + + virtual nsresult CreateChannel( + nsIChannel** aChannel, nsIURI* aURI, const mozilla::CORSMode aCORSMode, + const mozilla::dom::ReferrerPolicy& aReferrerPolicy, + mozilla::dom::Document* aDocument, nsILoadGroup* aLoadGroup, + nsIInterfaceRequestor* aCallbacks, uint64_t aHttpChannelId, + int32_t aSupportsPriorityValue) override { + mDrivingChannel.forget(aChannel); + return NS_OK; + } +}; + +class FakeListener : public nsIStreamListener { + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + enum { Never, OnStart, OnData, OnStop } mCancelIn = Never; + + nsresult mOnStartResult = NS_OK; + nsresult mOnDataResult = NS_OK; + nsresult mOnStopResult = NS_OK; + + bool mOnStart = false; + nsCString mOnData; + mozilla::Maybe<nsresult> mOnStop; + + private: + virtual ~FakeListener() = default; +}; + +NS_IMPL_ISUPPORTS(FakeListener, nsIStreamListener, nsIRequestObserver) + +NS_IMETHODIMP FakeListener::OnStartRequest(nsIRequest* request) { + EXPECT_FALSE(mOnStart); + mOnStart = true; + + if (mCancelIn == OnStart) { + request->Cancel(ERROR_CANCEL); + } + + return mOnStartResult; +} +NS_IMETHODIMP FakeListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, uint32_t count) { + nsAutoCString data; + data.SetLength(count); + + uint32_t read; + input->Read(data.BeginWriting(), count, &read); + mOnData += data; + + if (mCancelIn == OnData) { + request->Cancel(ERROR_CANCEL); + } + + return mOnDataResult; +} +NS_IMETHODIMP FakeListener::OnStopRequest(nsIRequest* request, + nsresult status) { + EXPECT_FALSE(mOnStop); + mOnStop.emplace(status); + + if (mCancelIn == OnStop) { + request->Cancel(ERROR_CANCEL); + } + + return mOnStopResult; +} + +bool eventInProgress = true; + +void Await() { + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "uriloader:TestFetchPreloader:Await"_ns, [&]() { + bool yield = !eventInProgress; + eventInProgress = true; // Just for convenience + return yield; + })); +} + +// WinBase.h defines this. +#undef Yield +void Yield() { eventInProgress = false; } + +} // namespace + +// **************************************************************************** +// Test body +// **************************************************************************** + +// Caching with all good results (data + NS_OK) +TEST(TestFetchPreloader, CacheNoneBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheStartBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartOfDataBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllDataBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + + // Request consumation of the preload... + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Get data before the channel fails +TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelError) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_FAILED(channel->Stop(ERROR_ONSTOP)); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_ONSTOP); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Cancel the channel between caching and consuming +TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelCancel) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + channel->Cancel(ERROR_CANCEL); + EXPECT_NS_FAILED(channel->Stop(ERROR_CANCEL)); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + // XXX - This is hard to solve; the data is there but we won't deliver it. + // This is a bit different case than e.g. a network error. We want to + // deliver some data in that case. Cancellation probably happens because of + // navigation or a demand to not consume the channel anyway. + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Let the listener throw while data is already cached +TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStartRequest) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mOnStartResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnDataAvailable) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mOnDataResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStopRequest) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mOnStopResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + // Throwing from OnStopRequest is generally ignored. + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Cancel the channel in various callbacks +TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnStartRequest) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnStart; + // check that throwing from OnStartRequest doesn't affect the cancellation + // status. + listener->mOnStartResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnDataAvailable) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnData; + // check that throwing from OnStartRequest doesn't affect the cancellation + // status. + listener->mOnDataResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Corner cases +TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailable) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnData; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_NS_FAILED(channel->Data("three"_ns)); + EXPECT_NS_FAILED(channel->Stop(NS_OK)); + + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnStartRequestAndRace) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + // This has to simulate a possibiilty when stream listener notifications from + // the channel are already pending in the queue while AsyncConsume is called. + // At this moment the listener has not been notified yet. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + })); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnStart; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + // Check listener's been fed properly. Expected is to NOT get any data and + // propagate the cancellation code and not being called duplicated + // OnStopRequest. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailableAndRace) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + // This has to simulate a possibiilty when stream listener notifications from + // the channel are already pending in the queue while AsyncConsume is called. + // At this moment the listener has not been notified yet. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + })); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnData; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + // Check listener's been fed properly. Expected is to NOT get anything after + // the first OnData and propagate the cancellation code and not being called + // duplicated OnStopRequest. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartlyBeforeConsumeThrowFromOnStartRequestAndRace) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc), nullptr, nullptr); + + EXPECT_TRUE(NS_SUCCEEDED(preloader->OpenChannel( + key, uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty, doc, + 0, nsISupportsPriority::PRIORITY_NORMAL))); + + EXPECT_NS_SUCCEEDED(channel->Start()); + EXPECT_NS_SUCCEEDED(channel->Data("one"_ns)); + EXPECT_NS_SUCCEEDED(channel->Data("two"_ns)); + + // This has to simulate a possibiilty when stream listener notifications from + // the channel are already pending in the queue while AsyncConsume is called. + // At this moment the listener has not been notified yet. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_NS_SUCCEEDED(channel->Data("three"_ns)); + EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK)); + })); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mOnStartResult = ERROR_THROW; + + EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener)); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + // Check listener's been fed properly. Expected is to NOT get any data and + // propagate the throwing code and not being called duplicated OnStopRequest. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} diff --git a/uriloader/preload/gtest/moz.build b/uriloader/preload/gtest/moz.build new file mode 100644 index 0000000000..72c2afa539 --- /dev/null +++ b/uriloader/preload/gtest/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + "TestFetchPreloader.cpp", +] + +LOCAL_INCLUDES += [ + "/netwerk/base", + "/xpcom/tests/gtest", +] + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += ["!/xpcom", "/xpcom/components"] diff --git a/uriloader/preload/moz.build b/uriloader/preload/moz.build new file mode 100644 index 0000000000..27a2f8cf00 --- /dev/null +++ b/uriloader/preload/moz.build @@ -0,0 +1,26 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking") + +TEST_DIRS += ["gtest"] + +EXPORTS.mozilla += [ + "FetchPreloader.h", + "PreloaderBase.h", + "PreloadHashKey.h", + "PreloadService.h", +] + +UNIFIED_SOURCES += [ + "FetchPreloader.cpp", + "PreloaderBase.cpp", + "PreloadHashKey.cpp", + "PreloadService.cpp", +] + +FINAL_LIBRARY = "xul" |