/* 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 aPreloader) { if (!mStartedPreloads.Contains(aKey)) { mStartedPreloads.Insert(aKey); mPreloaders.AppendElement(aPreloader); return true; } return false; } void OngoingEarlyHints::RegisterLinksAndGetConnectArgs( dom::ContentParentId aCpId, nsTArray& 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 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(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 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 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: ; 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 referrerInfo = new dom::ReferrerInfo(aBaseURI, finalReferrerPolicy); RefPtr 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 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 csp = new nsCSPContext(); nsresult rv = csp->SetRequestContextWithPrincipal( aPrincipal, aBaseURI, ""_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 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 httpChannelObject = do_QueryObject(mChannel); if (!httpChannelObject) { mChannel = nullptr; return NS_ERROR_ABORT; } // configure HTTP specific stuff nsCOMPtr httpChannel = do_QueryInterface(mChannel); if (!httpChannel) { mChannel = nullptr; return NS_ERROR_ABORT; } DebugOnly 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 loadInfo = mChannel->LoadInfo(); static_cast(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 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 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 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 obs = services::GetObserverService(); if (obs) { obs->NotifyObservers(mChannel, "earlyhints-connectback", nullptr); } mParent = aParent; if (mTimer) { mTimer->Cancel(); mTimer = nullptr; } RefPtr registrar = EarlyHintRegistrar::GetOrCreate(); registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); if (mOnStartRequestCalled) { SetParentChannel(); InvokeStreamListenerFunctions(); } } void EarlyHintPreloader::SetParentChannel() { RefPtr channel = do_QueryObject(mChannel); RefPtr 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 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 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 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 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 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 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(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 deathGrip(this); RefPtr 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(this); return NS_OK; } if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { NS_ADDREF_THIS(); *aResult = static_cast(this); return NS_OK; } if (aIID.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext != nullptr) { nsCOMPtr 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