diff options
Diffstat (limited to 'netwerk/protocol/http/EarlyHintPreloader.cpp')
-rw-r--r-- | netwerk/protocol/http/EarlyHintPreloader.cpp | 846 |
1 files changed, 846 insertions, 0 deletions
diff --git a/netwerk/protocol/http/EarlyHintPreloader.cpp b/netwerk/protocol/http/EarlyHintPreloader.cpp new file mode 100644 index 0000000000..920fea7fcd --- /dev/null +++ b/netwerk/protocol/http/EarlyHintPreloader.cpp @@ -0,0 +1,846 @@ +/* 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 "EarlyHintPreloader.h" + +#include "EarlyHintRegistrar.h" +#include "EarlyHintsService.h" +#include "ErrorList.h" +#include "HttpChannelParent.h" +#include "MainThreadUtils.h" +#include "NeckoCommon.h" +#include "gfxPlatform.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/Logging.h" +#include "mozilla/net/EarlyHintRegistrar.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "nsAttrValue.h" +#include "nsCOMPtr.h" +#include "nsContentPolicyUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsHttpChannel.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIChannel.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIHttpChannel.h" +#include "nsIInputStream.h" +#include "nsILoadContext.h" +#include "nsILoadInfo.h" +#include "nsIParentChannel.h" +#include "nsIReferrerInfo.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "ParentChannelListener.h" +#include "nsIChannel.h" +#include "nsInterfaceRequestorAgg.h" + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=EarlyHint:5 +// set MOZ_LOG_FILE=earlyhint.log +// +// this enables LogLevel::Debug level information and places all output in +// the file earlyhint.log +// +static mozilla::LazyLogModule gEarlyHintLog("EarlyHint"); + +#undef LOG +#define LOG(args) MOZ_LOG(gEarlyHintLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gEarlyHintLog, mozilla::LogLevel::Debug) + +namespace mozilla::net { + +namespace { +// This id uniquely identifies each early hint preloader in the +// EarlyHintRegistrar. Must only be accessed from main thread. +static uint64_t gEarlyHintPreloaderId{0}; +} // namespace + +//============================================================================= +// OngoingEarlyHints +//============================================================================= + +void OngoingEarlyHints::CancelAll(const nsACString& aReason) { + for (auto& preloader : mPreloaders) { + preloader->CancelChannel(NS_ERROR_ABORT, aReason, /* aDeleteEntry */ true); + } + mPreloaders.Clear(); + mStartedPreloads.Clear(); +} + +bool OngoingEarlyHints::Contains(const PreloadHashKey& aKey) { + return mStartedPreloads.Contains(aKey); +} + +bool OngoingEarlyHints::Add(const PreloadHashKey& aKey, + RefPtr<EarlyHintPreloader> aPreloader) { + if (!mStartedPreloads.Contains(aKey)) { + mStartedPreloads.Insert(aKey); + mPreloaders.AppendElement(aPreloader); + return true; + } + return false; +} + +void OngoingEarlyHints::RegisterLinksAndGetConnectArgs( + dom::ContentParentId aCpId, nsTArray<EarlyHintConnectArgs>& aOutLinks) { + // register all channels before returning + for (auto& preload : mPreloaders) { + EarlyHintConnectArgs args; + if (preload->Register(aCpId, args)) { + aOutLinks.AppendElement(std::move(args)); + } + } +} + +//============================================================================= +// EarlyHintPreloader +//============================================================================= + +EarlyHintPreloader::EarlyHintPreloader() { + AssertIsOnMainThread(); + mConnectArgs.earlyHintPreloaderId() = ++gEarlyHintPreloaderId; +}; + +EarlyHintPreloader::~EarlyHintPreloader() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + Telemetry::Accumulate(Telemetry::EH_STATE_OF_PRELOAD_REQUEST, mState); +} + +/* static */ +Maybe<PreloadHashKey> EarlyHintPreloader::GenerateHashKey( + ASDestination aAs, nsIURI* aURI, nsIPrincipal* aPrincipal, + CORSMode aCorsMode, bool aIsModulepreload) { + if (aIsModulepreload) { + return Some(PreloadHashKey::CreateAsScript( + aURI, aCorsMode, JS::loader::ScriptKind::eModule)); + } + if (aAs == ASDestination::DESTINATION_FONT && aCorsMode != CORS_NONE) { + return Some(PreloadHashKey::CreateAsFont(aURI, aCorsMode)); + } + if (aAs == ASDestination::DESTINATION_IMAGE) { + return Some(PreloadHashKey::CreateAsImage(aURI, aPrincipal, aCorsMode)); + } + if (aAs == ASDestination::DESTINATION_SCRIPT) { + return Some(PreloadHashKey::CreateAsScript( + aURI, aCorsMode, JS::loader::ScriptKind::eClassic)); + } + if (aAs == ASDestination::DESTINATION_STYLE) { + return Some(PreloadHashKey::CreateAsStyle( + aURI, aPrincipal, aCorsMode, + css::SheetParsingMode::eAuthorSheetFeatures)); + } + if (aAs == ASDestination::DESTINATION_FETCH && aCorsMode != CORS_NONE) { + return Some(PreloadHashKey::CreateAsFetch(aURI, aCorsMode)); + } + return Nothing(); +} + +/* static */ +nsSecurityFlags EarlyHintPreloader::ComputeSecurityFlags(CORSMode aCORSMode, + ASDestination aAs) { + if (aAs == ASDestination::DESTINATION_FONT) { + return nsContentSecurityManager::ComputeSecurityFlags( + CORSMode::CORS_NONE, + nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS); + } + if (aAs == ASDestination::DESTINATION_IMAGE) { + return nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_INHERITED_CONTEXT) | + nsILoadInfo::SEC_ALLOW_CHROME; + } + if (aAs == ASDestination::DESTINATION_SCRIPT) { + return nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS) | + nsILoadInfo::SEC_ALLOW_CHROME; + } + if (aAs == ASDestination::DESTINATION_STYLE) { + return nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_INHERITED_CONTEXT) | + nsILoadInfo::SEC_ALLOW_CHROME; + ; + } + if (aAs == ASDestination::DESTINATION_FETCH) { + return nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS); + } + MOZ_ASSERT(false, "Unexpected ASDestination"); + return nsContentSecurityManager::ComputeSecurityFlags( + CORSMode::CORS_NONE, + nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS); +} + +// static +void EarlyHintPreloader::MaybeCreateAndInsertPreload( + OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aLinkHeader, + nsIURI* aBaseURI, nsIPrincipal* aPrincipal, + nsICookieJarSettings* aCookieJarSettings, + const nsACString& aResponseReferrerPolicy, const nsACString& aCSPHeader, + uint64_t aBrowsingContextID, + dom::CanonicalBrowsingContext* aLoadingBrowsingContext, + bool aIsModulepreload) { + nsAttrValue as; + ParseAsValue(aLinkHeader.mAs, as); + + ASDestination destination = static_cast<ASDestination>(as.GetEnumValue()); + CollectResourcesTypeTelemetry(destination); + + if (!StaticPrefs::network_early_hints_enabled()) { + return; + } + + if (destination == ASDestination::DESTINATION_INVALID && !aIsModulepreload) { + // return early when it's definitly not an asset type we preload + // would be caught later as well, e.g. when creating the PreloadHashKey + return; + } + + if (destination == ASDestination::DESTINATION_FONT && + !gfxPlatform::GetPlatform()->DownloadableFontsEnabled()) { + return; + } + + nsCOMPtr<nsIURI> uri; + NS_ENSURE_SUCCESS_VOID( + NS_NewURI(getter_AddRefs(uri), aLinkHeader.mHref, nullptr, aBaseURI)); + // The link relation may apply to a different resource, specified + // in the anchor parameter. For the link relations supported so far, + // we simply abort if the link applies to a resource different to the + // one we've loaded + if (!nsContentUtils::LinkContextIsURI(aLinkHeader.mAnchor, uri)) { + return; + } + + // only preload secure context urls + if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) { + return; + } + + CORSMode corsMode = dom::Element::StringToCORSMode(aLinkHeader.mCrossOrigin); + + Maybe<PreloadHashKey> hashKey = + GenerateHashKey(destination, uri, aPrincipal, corsMode, aIsModulepreload); + if (!hashKey) { + return; + } + + if (aOngoingEarlyHints->Contains(*hashKey)) { + return; + } + + nsContentPolicyType contentPolicyType = + aIsModulepreload ? (IsScriptLikeOrInvalid(aLinkHeader.mAs) + ? nsContentPolicyType::TYPE_SCRIPT + : nsContentPolicyType::TYPE_INVALID) + : AsValueToContentPolicy(as); + + if (contentPolicyType == nsContentPolicyType::TYPE_INVALID) { + return; + } + + dom::ReferrerPolicy linkReferrerPolicy = + dom::ReferrerInfo::ReferrerPolicyAttributeFromString( + aLinkHeader.mReferrerPolicy); + + dom::ReferrerPolicy responseReferrerPolicy = + dom::ReferrerInfo::ReferrerPolicyAttributeFromString( + NS_ConvertUTF8toUTF16(aResponseReferrerPolicy)); + + // The early hint may have two referrer policies, one from the response header + // and one from the link element. + // + // For example, in this server response: + // HTTP/1.1 103 Early Hints + // Referrer-Policy : origin + // Link: </style.css>; rel=preload; as=style referrerpolicy=no-referrer + // + // The link header referrer policy, if present, will take precedence over + // the response referrer policy + dom::ReferrerPolicy finalReferrerPolicy = responseReferrerPolicy; + if (linkReferrerPolicy != dom::ReferrerPolicy::_empty) { + finalReferrerPolicy = linkReferrerPolicy; + } + nsCOMPtr<nsIReferrerInfo> referrerInfo = + new dom::ReferrerInfo(aBaseURI, finalReferrerPolicy); + + RefPtr<EarlyHintPreloader> earlyHintPreloader = new EarlyHintPreloader(); + + earlyHintPreloader->mLoadContext = aLoadingBrowsingContext; + + // Security flags for modulepreload's request mode are computed here directly + // until full support for worker destinations can be added. + // + // Implements "To fetch a single module script," + // Step 9. If destination is "worker", "sharedworker", or "serviceworker", + // and the top-level module fetch flag is set, then set request's + // mode to "same-origin". + nsSecurityFlags securityFlags = + aIsModulepreload + ? ((aLinkHeader.mAs.LowerCaseEqualsASCII("worker") || + aLinkHeader.mAs.LowerCaseEqualsASCII("sharedworker") || + aLinkHeader.mAs.LowerCaseEqualsASCII("serviceworker")) + ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED + : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) | + (corsMode == CORS_USE_CREDENTIALS + ? nsILoadInfo::SEC_COOKIES_INCLUDE + : nsILoadInfo::SEC_COOKIES_SAME_ORIGIN) | + nsILoadInfo::SEC_ALLOW_CHROME + : EarlyHintPreloader::ComputeSecurityFlags(corsMode, destination); + + // Verify that the resource should be loaded. + // This isn't the ideal way to test the resource against the CSP. + // The problem comes from the fact that at the stage of Early Hint + // processing we have not yet created a document where we would normally store + // the CSP. + + // First we will create a load info, + // nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new LoadInfo( + aPrincipal, // loading principal + aPrincipal, // triggering principal + nullptr /* aLoadingContext node */, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, contentPolicyType); + + if (aCSPHeader.Length() != 0) { + // If the CSP header is present then create a new CSP and apply the header + // directives to it + nsCOMPtr<nsIContentSecurityPolicy> csp = new nsCSPContext(); + nsresult rv = csp->SetRequestContextWithPrincipal( + aPrincipal, aBaseURI, u""_ns, 0 /* aInnerWindowId */); + NS_ENSURE_SUCCESS_VOID(rv); + rv = CSP_AppendCSPFromHeader(csp, NS_ConvertUTF8toUTF16(aCSPHeader), + false /* report only */); + NS_ENSURE_SUCCESS_VOID(rv); + + // We create a temporary ClientInfo. This is required on the loadInfo as + // that is how the CSP is queried. More specificially, as a hack to be able + // to call NS_CheckContentLoadPolicy on nsILoadInfo which exclusively + // accesses the CSP from the ClientInfo, we create a synthetic ClientInfo to + // hold the CSP we are creating. This is not a safe thing to do in any other + // circumstance because ClientInfos are always describing a ClientSource + // that corresponds to a global or potential global, so creating an info + // without a source is unsound. For the purposes of doing things before a + // global exists, fetch has the concept of a + // https://fetch.spec.whatwg.org/#concept-request-reserved-client and + // nsILoadInfo explicity has methods around GiveReservedClientSource which + // are primarily used by ClientChannelHelper. If you are trying to do real + // CSP stuff and the ClientInfo is not there yet, please enhance the logic + // around ClientChannelHelper. + + mozilla::ipc::PrincipalInfo principalInfo; + rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + NS_ENSURE_SUCCESS_VOID(rv); + dom::ClientInfo clientInfo(nsID::GenerateUUID(), dom::ClientType::Window, + principalInfo, TimeStamp::Now()); + + // Our newly-created CSP is set on the ClientInfo via the indirect route of + // first serializing to CSPInfo + ipc::CSPInfo cspInfo; + rv = CSPToCSPInfo(csp, &cspInfo); + NS_ENSURE_SUCCESS_VOID(rv); + clientInfo.SetCspInfo(cspInfo); + + // This ClientInfo is then set on the new loadInfo. + // It can now be used to test the resource against the policy + secCheckLoadInfo->SetClientInfo(clientInfo); + } + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + nsresult rv = NS_CheckContentLoadPolicy(uri, secCheckLoadInfo, &shouldLoad, + nsContentUtils::GetContentPolicy()); + + if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) { + return; + } + + NS_ENSURE_SUCCESS_VOID(earlyHintPreloader->OpenChannel( + uri, aPrincipal, securityFlags, contentPolicyType, referrerInfo, + aCookieJarSettings, aBrowsingContextID)); + + earlyHintPreloader->SetLinkHeader(aLinkHeader); + + DebugOnly<bool> result = + aOngoingEarlyHints->Add(*hashKey, earlyHintPreloader); + MOZ_ASSERT(result); +} + +nsresult EarlyHintPreloader::OpenChannel( + nsIURI* aURI, nsIPrincipal* aPrincipal, nsSecurityFlags aSecurityFlags, + nsContentPolicyType aContentPolicyType, nsIReferrerInfo* aReferrerInfo, + nsICookieJarSettings* aCookieJarSettings, uint64_t aBrowsingContextID) { + MOZ_ASSERT(aContentPolicyType == nsContentPolicyType::TYPE_IMAGE || + aContentPolicyType == + nsContentPolicyType::TYPE_INTERNAL_FETCH_PRELOAD || + aContentPolicyType == nsContentPolicyType::TYPE_SCRIPT || + aContentPolicyType == nsContentPolicyType::TYPE_STYLESHEET || + aContentPolicyType == nsContentPolicyType::TYPE_FONT); + + nsresult rv = + NS_NewChannel(getter_AddRefs(mChannel), aURI, aPrincipal, aSecurityFlags, + aContentPolicyType, aCookieJarSettings, + /* aPerformanceStorage */ nullptr, + /* aLoadGroup */ nullptr, + /* aCallbacks */ this, nsIRequest::LOAD_NORMAL); + + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsHttpChannel> httpChannelObject = do_QueryObject(mChannel); + if (!httpChannelObject) { + mChannel = nullptr; + return NS_ERROR_ABORT; + } + + // configure HTTP specific stuff + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (!httpChannel) { + mChannel = nullptr; + return NS_ERROR_ABORT; + } + DebugOnly<nsresult> success = httpChannel->SetReferrerInfo(aReferrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(success)); + success = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(success)); + + mParentListener = new ParentChannelListener(this, nullptr); + + PriorizeAsPreload(); + + rv = mChannel->AsyncOpen(mParentListener); + if (NS_FAILED(rv)) { + mParentListener = nullptr; + return rv; + } + + SetState(ePreloaderOpened); + + // Setting the BrowsingContextID here to let Early Hint requests show up in + // devtools. Normally that would automatically happen if we would pass the + // nsILoadGroup in ns_NewChannel above, but the nsILoadGroup is inaccessible + // here in the ParentProcess. The nsILoadGroup only exists in ContentProcess + // as part of the document and nsDocShell. It is also not yet determined which + // ContentProcess this load belongs to. + nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo(); + static_cast<LoadInfo*>(loadInfo.get()) + ->UpdateBrowsingContextID(aBrowsingContextID); + + return NS_OK; +} + +void EarlyHintPreloader::PriorizeAsPreload() { + nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL; + Unused << mChannel->GetLoadFlags(&loadFlags); + Unused << mChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND); + + if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(mChannel)) { + Unused << cos->AddClassFlags(nsIClassOfService::Unblocked); + } +} + +void EarlyHintPreloader::SetLinkHeader(const LinkHeader& aLinkHeader) { + mConnectArgs.link() = aLinkHeader; +} + +bool EarlyHintPreloader::IsFromContentParent(dom::ContentParentId aCpId) const { + return aCpId == mCpId; +} + +bool EarlyHintPreloader::Register(dom::ContentParentId aCpId, + EarlyHintConnectArgs& aOut) { + mCpId = aCpId; + + // Set minimum delay of 1ms to always start the timer after the function call + // completed. + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(mTimer), this, + std::max(StaticPrefs::network_early_hints_parent_connect_timeout(), + (uint32_t)1), + nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + MOZ_ASSERT(!mTimer); + CancelChannel(NS_ERROR_ABORT, "new-timer-failed"_ns, + /* aDeleteEntry */ false); + return false; + } + + // Create an entry in the redirect channel registrar + RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate(); + registrar->RegisterEarlyHint(mConnectArgs.earlyHintPreloaderId(), this); + + aOut = mConnectArgs; + return true; +} + +nsresult EarlyHintPreloader::CancelChannel(nsresult aStatus, + const nsACString& aReason, + bool aDeleteEntry) { + LOG(("EarlyHintPreloader::CancelChannel [this=%p]\n", this)); + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + if (aDeleteEntry) { + RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate(); + registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); + } + // clear redirect channel in case this channel is cleared between the call of + // EarlyHintPreloader::AsyncOnChannelRedirect and + // EarlyHintPreloader::OnRedirectResult + mRedirectChannel = nullptr; + if (mChannel) { + if (mSuspended) { + mChannel->Resume(); + } + mChannel->CancelWithReason(aStatus, aReason); + // Clearing mChannel is safe, because this EarlyHintPreloader is not in the + // EarlyHintRegistrar after this function call and we won't call + // SetHttpChannelFromEarlyHintPreloader nor OnStartRequest on mParent. + mChannel = nullptr; + SetState(ePreloaderCancelled); + } + return NS_OK; +} + +void EarlyHintPreloader::OnParentReady(nsIParentChannel* aParent) { + AssertIsOnMainThread(); + MOZ_ASSERT(aParent); + LOG(("EarlyHintPreloader::OnParentReady [this=%p]\n", this)); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + obs->NotifyObservers(mChannel, "earlyhints-connectback", nullptr); + } + + mParent = aParent; + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate(); + registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); + + if (mOnStartRequestCalled) { + SetParentChannel(); + InvokeStreamListenerFunctions(); + } +} + +void EarlyHintPreloader::SetParentChannel() { + RefPtr<HttpBaseChannel> channel = do_QueryObject(mChannel); + RefPtr<HttpChannelParent> parent = do_QueryObject(mParent); + parent->SetHttpChannelFromEarlyHintPreloader(channel); +} + +// Adapted from +// https://searchfox.org/mozilla-central/rev/b4150d1c6fae0c51c522df2d2c939cf5ad331d4c/netwerk/ipc/DocumentLoadListener.cpp#1311 +void EarlyHintPreloader::InvokeStreamListenerFunctions() { + AssertIsOnMainThread(); + RefPtr<EarlyHintPreloader> self(this); + + LOG(( + "EarlyHintPreloader::InvokeStreamListenerFunctions [this=%p parent=%p]\n", + this, mParent.get())); + + // If we failed to suspend the channel, then we might have received + // some messages while the redirected was being handled. + // Manually send them on now. + if (!mIsFinished) { + // This is safe to do, because OnStartRequest/OnStopRequest/OnDataAvailable + // are all called on the main thread. They can't be called until we worked + // through all functions in the streamListnerFunctions array. + mParentListener->SetListenerAfterRedirect(mParent); + } + nsTArray<StreamListenerFunction> streamListenerFunctions = + std::move(mStreamListenerFunctions); + + ForwardStreamListenerFunctions(std::move(streamListenerFunctions), mParent); + + // We don't expect to get new stream listener functions added + // via re-entrancy. If this ever happens, we should understand + // exactly why before allowing it. + NS_ASSERTION(mStreamListenerFunctions.IsEmpty(), + "Should not have added new stream listener function!"); + + if (mChannel && mSuspended) { + mChannel->Resume(); + } + mChannel = nullptr; + mParent = nullptr; + mParentListener = nullptr; + + SetState(ePreloaderUsed); +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(EarlyHintPreloader, nsIRequestObserver, nsIStreamListener, + nsIChannelEventSink, nsIInterfaceRequestor, + nsIRedirectResultListener, nsIMultiPartChannelListener, + nsINamed, nsITimerCallback); + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIStreamListener +//----------------------------------------------------------------------------- + +// Implementation copied and adapted from DocumentLoadListener::OnStartRequest +// https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2317-2508 +NS_IMETHODIMP +EarlyHintPreloader::OnStartRequest(nsIRequest* aRequest) { + LOG(("EarlyHintPreloader::OnStartRequest [this=%p]\n", this)); + AssertIsOnMainThread(); + + mOnStartRequestCalled = true; + + nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest); + if (multiPartChannel) { + multiPartChannel->GetBaseChannel(getter_AddRefs(mChannel)); + } else { + mChannel = do_QueryInterface(aRequest); + } + MOZ_DIAGNOSTIC_ASSERT(mChannel); + + nsresult status = NS_OK; + Unused << aRequest->GetStatus(&status); + + if (mParent) { + SetParentChannel(); + mParent->OnStartRequest(aRequest); + InvokeStreamListenerFunctions(); + } else { + // Don't suspend the chanel when the channel got cancelled with + // CancelChannel, because then OnStopRequest wouldn't get called and we + // wouldn't clean up the channel. + if (NS_SUCCEEDED(status)) { + mChannel->Suspend(); + mSuspended = true; + } + mStreamListenerFunctions.AppendElement( + AsVariant(OnStartRequestParams{aRequest})); + } + + // return error after adding the OnStartRequest forward. The OnStartRequest + // failure has to be forwarded to listener, because they called AsyncOpen on + // this channel + return status; +} + +// Implementation copied from DocumentLoadListener::OnStopRequest +// https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2510-2528 +NS_IMETHODIMP +EarlyHintPreloader::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + AssertIsOnMainThread(); + LOG(("EarlyHintPreloader::OnStopRequest [this=%p]\n", this)); + mStreamListenerFunctions.AppendElement( + AsVariant(OnStopRequestParams{aRequest, aStatusCode})); + + // If we're not a multi-part channel, then we're finished and we don't + // expect any further events. If we are, then this might be called again, + // so wait for OnAfterLastPart instead. + nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest); + if (!multiPartChannel) { + mIsFinished = true; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIStreamListener +//----------------------------------------------------------------------------- + +// Implementation copied from DocumentLoadListener::OnDataAvailable +// https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2530-2549 +NS_IMETHODIMP +EarlyHintPreloader::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + AssertIsOnMainThread(); + LOG(("EarlyHintPreloader::OnDataAvailable [this=%p]\n", this)); + // This isn't supposed to happen, since we suspended the channel, but + // sometimes Suspend just doesn't work. This can happen when we're routing + // through nsUnknownDecoder to sniff the content type, and it doesn't handle + // being suspended. Let's just store the data and manually forward it to our + // redirected channel when it's ready. + nsCString data; + nsresult rv = NS_ReadInputStreamToString(aInputStream, data, aCount); + NS_ENSURE_SUCCESS(rv, rv); + + mStreamListenerFunctions.AppendElement(AsVariant( + OnDataAvailableParams{aRequest, std::move(data), aOffset, aCount})); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIMultiPartChannelListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::OnAfterLastPart(nsresult aStatus) { + LOG(("EarlyHintPreloader::OnAfterLastPart [this=%p]", this)); + mStreamListenerFunctions.AppendElement( + AsVariant(OnAfterLastPartParams{aStatus})); + mIsFinished = true; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* callback) { + LOG(("EarlyHintPreloader::AsyncOnChannelRedirect [this=%p]", this)); + nsCOMPtr<nsIURI> newURI; + nsresult rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) { + callback->OnRedirectVerifyCallback(rv); + return NS_OK; + } + + // HTTP request headers are not automatically forwarded to the new channel. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(httpChannel); + + rv = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Assign to mChannel after we get notification about success of the + // redirect in OnRedirectResult. + mRedirectChannel = aNewChannel; + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIRedirectResultListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::OnRedirectResult(nsresult aStatus) { + LOG(("EarlyHintPreloader::OnRedirectResult [this=%p] aProceeding=0x%" PRIx32, + this, static_cast<uint32_t>(aStatus))); + if (NS_SUCCEEDED(aStatus) && mRedirectChannel) { + mChannel = mRedirectChannel; + } + + mRedirectChannel = nullptr; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsINamed +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::GetName(nsACString& aName) { + aName.AssignLiteral("EarlyHintPreloader"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsITimerCallback +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::Notify(nsITimer* timer) { + // Death grip, because we will most likely remove the last reference when + // deleting us from the EarlyHintRegistrar + RefPtr<EarlyHintPreloader> deathGrip(this); + + RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate(); + registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); + + mTimer = nullptr; + mRedirectChannel = nullptr; + if (mChannel) { + if (mSuspended) { + mChannel->Resume(); + } + mChannel->CancelWithReason(NS_ERROR_ABORT, "parent-connect-timeout"_ns); + mChannel = nullptr; + } + SetState(ePreloaderTimeout); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIChannelEventSink*>(this); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIRedirectResultListener*>(this); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext != nullptr) { + nsCOMPtr<nsILoadContext> loadContext = mLoadContext; + loadContext.forget(aResult); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +void EarlyHintPreloader::CollectResourcesTypeTelemetry( + ASDestination aASDestination) { + if (aASDestination == ASDestination::DESTINATION_FONT) { + glean::netwerk::early_hints.Get("font"_ns).Add(1); + } else if (aASDestination == ASDestination::DESTINATION_SCRIPT) { + glean::netwerk::early_hints.Get("script"_ns).Add(1); + } else if (aASDestination == ASDestination::DESTINATION_STYLE) { + glean::netwerk::early_hints.Get("stylesheet"_ns).Add(1); + } else if (aASDestination == ASDestination::DESTINATION_IMAGE) { + glean::netwerk::early_hints.Get("image"_ns).Add(1); + } else if (aASDestination == ASDestination::DESTINATION_FETCH) { + glean::netwerk::early_hints.Get("fetch"_ns).Add(1); + } else { + glean::netwerk::early_hints.Get("other"_ns).Add(1); + } +} +} // namespace mozilla::net |