author | Honza Bambas <honzab.moz@firemni.cz> |
Mon, 18 May 2020 12:18:14 +0000 | |
changeset 530585 | 4068b1f7a903cb0de7316cf2947dce6798587711 |
parent 530584 | 17b0c4bfc37d533a90af30839b85058615015c30 |
child 530586 | f9edbff92af587d0a2978f8392e30d209200e349 |
push id | 37428 |
push user | nbeleuzu@mozilla.com |
push date | Mon, 18 May 2020 21:48:24 +0000 |
treeherder | mozilla-central@a3941f42e662 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | baku |
bugs | 1618543 |
milestone | 78.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/dom/fetch/FetchDriver.cpp +++ b/dom/fetch/FetchDriver.cpp @@ -351,16 +351,94 @@ FetchDriver::FetchDriver(SafeRefPtr<Inte FetchDriver::~FetchDriver() { AssertIsOnMainThread(); // We assert this since even on failures, we should call // FailWithNetworkError(). MOZ_ASSERT(mResponseAvailableCalled); } +already_AddRefed<PreloaderBase> FetchDriver::FindPreload(nsIURI* aURI) { + // Decide if we allow reuse of an existing <link rel=preload as=fetch> + // response for this request. First examine this fetch requets itself if it + // is 'pure' enough to use the response and then try to find a preload. + + if (!mDocument) { + // Preloads are mapped on the document, no document, no preload. + return nullptr; + } + CORSMode cors; + switch (mRequest->Mode()) { + case RequestMode::No_cors: + cors = CORSMode::CORS_NONE; + break; + case RequestMode::Cors: + cors = mRequest->GetCredentialsMode() == RequestCredentials::Include + ? CORSMode::CORS_USE_CREDENTIALS + : CORSMode::CORS_ANONYMOUS; + break; + default: + // Can't be satisfied by a preload because preload cannot define any of + // remaining modes. + return nullptr; + } + if (!mRequest->Headers()->HasOnlySimpleHeaders()) { + // Preload can't set any headers. + return nullptr; + } + if (!mRequest->GetIntegrity().IsEmpty()) { + // There is currently no support for SRI checking in the fetch preloader. + return nullptr; + } + if (mRequest->GetCacheMode() != RequestCache::Default) { + // Preload can only go with the default caching mode. + return nullptr; + } + if (mRequest->SkipServiceWorker()) { + // Preload can't be forbidden interception. + return nullptr; + } + if (mRequest->GetRedirectMode() != RequestRedirect::Follow) { + // Preload always follows redirects. + return nullptr; + } + nsAutoCString method; + mRequest->GetMethod(method); + if (!method.EqualsLiteral("GET")) { + // Preload can only do GET, this also eliminates the case we do upload, so + // no need to check if the request has any body to send out. + return nullptr; + } + + // OK, this request can be satisfied by a preloaded response, try to find one. + + // TODO - check if we need to perform step 5 and 6 before using + // mRequest->ReferrerPolicy_() here. + auto preloadKey = + PreloadHashKey::CreateAsFetch(aURI, cors, mRequest->ReferrerPolicy_()); + return mDocument->Preloads().LookupPreload(&preloadKey); +} + +void FetchDriver::UpdateReferrerInfoFromNewChannel(nsIChannel* aChannel) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + if (!httpChannel) { + return; + } + + nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo(); + if (!referrerInfo) { + return; + } + + nsAutoString computedReferrerSpec; + mRequest->SetReferrerPolicy(referrerInfo->ReferrerPolicy()); + Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec); + mRequest->SetReferrer(computedReferrerSpec); +} + nsresult FetchDriver::Fetch(AbortSignalImpl* aSignalImpl, FetchDriverObserver* aObserver) { AssertIsOnMainThread(); #ifdef DEBUG MOZ_ASSERT(!mFetchCalled); mFetchCalled = true; #endif @@ -433,16 +511,48 @@ nsresult FetchDriver::HttpFetch( if (IsBlobURI(uri)) { nsAutoCString method; mRequest->GetMethod(method); if (!method.EqualsLiteral("GET")) { return NS_ERROR_DOM_NETWORK_ERR; } } + RefPtr<PreloaderBase> fetchPreload = FindPreload(uri); + if (fetchPreload) { + fetchPreload->RemoveSelf(mDocument); + fetchPreload->NotifyUsage(PreloaderBase::LoadBackground::Keep); + + rv = fetchPreload->AsyncConsume(this); + if (NS_SUCCEEDED(rv)) { + mFromPreload = true; + + mChannel = fetchPreload->Channel(); + if (mChannel) { + // Still in progress, monitor redirects. + mChannel->SetNotificationCallbacks(this); + } + + // Copied from AsyncOnChannelRedirect. + for (const auto& redirect : fetchPreload->Redirects()) { + if (redirect.Flags() & nsIChannelEventSink::REDIRECT_INTERNAL) { + mRequest->SetURLForInternalRedirect(redirect.Flags(), redirect.Spec(), + redirect.Fragment()); + } else { + mRequest->AddURL(redirect.Spec(), redirect.Fragment()); + } + } + + return NS_OK; + } + + // The preload failed to be consumed. Behave like there were no preload. + fetchPreload = nullptr; + } + // Step 2 deals with letting ServiceWorkers intercept requests. This is // handled by Necko after the channel is opened. // FIXME(nsm): Bug 1119026: The channel's skip service worker flag should be // set based on the Request's flag. // Step 3.1 "If the CORS preflight flag is set and one of these conditions is // true..." is handled by the CORS proxy. // @@ -784,16 +894,26 @@ void FetchDriver::FailWithNetworkError(n NS_IMETHODIMP FetchDriver::OnStartRequest(nsIRequest* aRequest) { AssertIsOnMainThread(); // Note, this can be called multiple times if we are doing an opaqueredirect. // In that case we will get a simulated OnStartRequest() and then the real // channel will call in with an errored OnStartRequest(). + if (mFromPreload && !mChannel) { + if (mAborted) { + aRequest->Cancel(NS_BINDING_ABORTED); + return NS_BINDING_ABORTED; + } + + mChannel = do_QueryInterface(aRequest); + UpdateReferrerInfoFromNewChannel(mChannel); + } + if (!mChannel) { MOZ_ASSERT(!mObserver); return NS_BINDING_ABORTED; } nsresult rv; aRequest->GetStatus(&rv); if (NS_FAILED(rv)) { @@ -1352,29 +1472,17 @@ FetchDriver::AsyncOnChannelRedirect(nsIC } else { // Overwrite the URL only when the request is redirected by a service // worker. mRequest->SetURLForInternalRedirect(aFlags, spec, fragment); } // In redirect, httpChannel already took referrer-policy into account, so // updates request’s associated referrer policy from channel. - if (newHttpChannel) { - nsAutoString computedReferrerSpec; - nsCOMPtr<nsIReferrerInfo> referrerInfo = newHttpChannel->GetReferrerInfo(); - if (referrerInfo) { - mRequest->SetReferrerPolicy(referrerInfo->ReferrerPolicy()); - Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec); - } - - // Step 8 https://fetch.spec.whatwg.org/#main-fetch - // If request’s referrer is not "no-referrer" (empty), set request’s - // referrer to the result of invoking determine request’s referrer. - mRequest->SetReferrer(computedReferrerSpec); - } + UpdateReferrerInfoFromNewChannel(aNewChannel); aCallback->OnRedirectVerifyCallback(NS_OK); return NS_OK; } NS_IMETHODIMP FetchDriver::CheckListenerChain() { return NS_OK; } @@ -1482,12 +1590,14 @@ void FetchDriver::Abort() { mObserver->OnResponseEnd(FetchDriverObserver::eAborted); mObserver = nullptr; } if (mChannel) { mChannel->Cancel(NS_BINDING_ABORTED); mChannel = nullptr; } + + mAborted = true; } } // namespace dom } // namespace mozilla
--- a/dom/fetch/FetchDriver.h +++ b/dom/fetch/FetchDriver.h @@ -157,28 +157,42 @@ class FetchDriver final : public nsIStre // that these do not overlap. bool mNeedToObserveOnDataAvailable; bool mIsTrackingFetch; RefPtr<AlternativeDataStreamListener> mAltDataListener; bool mOnStopRequestCalled; + // This flag is true when this fetch has found a matching preload and is being + // satisfied by a its response. + bool mFromPreload = false; + // This flag is set in call to Abort() and spans the possible window this + // fetch doesn't have mChannel (to be cancelled) between reuse of the matching + // preload, that has already finished and dropped reference to its channel, + // and OnStartRequest notification. It let's us cancel the load when we get + // the channel in OnStartRequest. + bool mAborted = false; + #ifdef DEBUG bool mResponseAvailableCalled; bool mFetchCalled; #endif friend class AlternativeDataStreamListener; FetchDriver() = delete; FetchDriver(const FetchDriver&) = delete; FetchDriver& operator=(const FetchDriver&) = delete; ~FetchDriver(); + already_AddRefed<PreloaderBase> FindPreload(nsIURI* aURI); + + void UpdateReferrerInfoFromNewChannel(nsIChannel* aChannel); + nsresult HttpFetch( const nsACString& aPreferredAlternativeDataType = EmptyCString()); // Returns the filtered response sent to the observer. already_AddRefed<InternalResponse> BeginAndGetFilteredResponse( InternalResponse* aResponse, bool aFoundOpaqueRedirect); // Utility since not all cases need to do any post processing of the filtered // response. void FailWithNetworkError(nsresult rv);
--- a/uriloader/preload/PreloaderBase.cpp +++ b/uriloader/preload/PreloaderBase.cpp @@ -25,16 +25,22 @@ PreloaderBase::RedirectSink::RedirectSin NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink, nsIInterfaceRequestor, nsIChannelEventSink, nsIRedirectResultListener) NS_IMETHODIMP PreloaderBase::RedirectSink::AsyncOnChannelRedirect( nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, nsIAsyncVerifyRedirectCallback* aCallback) { mRedirectChannel = aNewChannel; + // Deliberately adding this before confirmation. + nsCOMPtr<nsIURI> uri; + aNewChannel->GetOriginalURI(getter_AddRefs(uri)); + mPreloader->mRedirectRecords.AppendElement( + RedirectRecord(aFlags, uri.forget())); + if (mCallbacks) { nsCOMPtr<nsIChannelEventSink> sink(do_GetInterface(mCallbacks)); if (sink) { return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback); } } @@ -108,18 +114,18 @@ void PreloaderBase::NotifyOpen(PreloadHa // * Start the usage timer if aIsPreload. nsCOMPtr<nsIInterfaceRequestor> callbacks; mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); RefPtr<RedirectSink> sink(new RedirectSink(this, callbacks)); mChannel->SetNotificationCallbacks(sink); } -void PreloaderBase::NotifyUsage() { - if (!mIsUsed && mChannel) { +void PreloaderBase::NotifyUsage(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) { @@ -139,22 +145,25 @@ void PreloaderBase::NotifyUsage() { } } mIsUsed = true; // * Cancel the usage timer. } -void PreloaderBase::NotifyRestart(dom::Document* aDocument, - PreloaderBase* aNewPreloader) { +void PreloaderBase::RemoveSelf(dom::Document* aDocument) { if (aDocument) { aDocument->Preloads().DeregisterPreload(&mKey); } +} +void PreloaderBase::NotifyRestart(dom::Document* aDocument, + PreloaderBase* aNewPreloader) { + RemoveSelf(aDocument); mKey = PreloadHashKey(); if (aNewPreloader) { aNewPreloader->mNodes = std::move(mNodes); } } void PreloaderBase::NotifyStart(nsIRequest* aRequest) { @@ -224,17 +233,17 @@ void PreloaderBase::RemoveLinkPreloadNod 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); - aNode->OwnerDoc()->Preloads().DeregisterPreload(&mKey); + RemoveSelf(aNode->OwnerDoc()); if (mChannel) { mChannel->Cancel(NS_BINDING_ABORTED); } } } void PreloaderBase::NotifyNodeEvent(nsINode* aNode) { @@ -243,9 +252,24 @@ void PreloaderBase::NotifyNodeEvent(nsIN } 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; +} + } // namespace mozilla
--- a/uriloader/preload/PreloaderBase.h +++ b/uriloader/preload/PreloaderBase.h @@ -74,20 +74,27 @@ class PreloaderBase : public SupportsWea void NotifyValidating(); // Called when the validation process has been done. This will notify // associated link DOM nodes. void NotifyValidated(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. - void NotifyUsage(); + // @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(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); @@ -104,16 +111,33 @@ class PreloaderBase : public SupportsWea 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; } + protected: virtual ~PreloaderBase(); private: void NotifyNodeEvent(nsINode* aNode); // A helper class that will update the PreloaderBase.mChannel member when a // redirect happens, so that we can reprioritize or cancel when needed. @@ -141,16 +165,19 @@ class PreloaderBase : public SupportsWea nsCOMPtr<nsIChannel> mRedirectChannel; }; private: // Reference to HTMLLinkElement DOM nodes to deliver onload and onerror // notifications to. nsTArray<nsWeakPtr> mNodes; + // History of redirects. + nsTArray<RedirectRecord> mRedirectRecords; + // The loading channel. This will update when a redirect occurs. nsCOMPtr<nsIChannel> mChannel; // 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'.