/* 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 "EarlyHintsService.h" #include "ErrorList.h" #include "mozilla/CORSMode.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/ReferrerInfo.h" #include "mozilla/Logging.h" #include "nsAttrValue.h" #include "nsAttrValueInlines.h" #include "nsContentSecurityManager.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsIAsyncVerifyRedirectCallback.h" #include "nsICacheInfoChannel.h" #include "nsIChannel.h" #include "nsIHttpChannel.h" #include "nsIInputStream.h" #include "nsILoadInfo.h" #include "nsIReferrerInfo.h" #include "nsIURI.h" #include "nsStreamUtils.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 { //============================================================================= // OngoingEarlyHints //============================================================================= void OngoingEarlyHints::CancelAllOngoingPreloads() { for (auto& el : mOngoingPreloads) { el.GetData()->CancelChannel(nsresult::NS_ERROR_ABORT); } } bool OngoingEarlyHints::Contains(const PreloadHashKey& aKey) { return mOngoingPreloads.Contains(aKey); } bool OngoingEarlyHints::Add(const PreloadHashKey& aKey, RefPtr aPreloader) { return mOngoingPreloads.InsertOrUpdate(aKey, aPreloader); } //============================================================================= // EarlyHintPreloader //============================================================================= EarlyHintPreloader::EarlyHintPreloader(nsIURI* aURI) : mURI(aURI) {} /* static */ Maybe EarlyHintPreloader::GenerateHashKey( ASDestination aAs, nsIURI* aURI, nsIPrincipal* aPrincipal, CORSMode aCorsMode, const nsAString& aType) { if (aAs == ASDestination::DESTINATION_FONT) { return Some(PreloadHashKey::CreateAsFont(aURI, aCorsMode)); } if (aAs == ASDestination::DESTINATION_IMAGE) { return Some(PreloadHashKey::CreateAsImage(aURI, aPrincipal, aCorsMode)); } if (aAs == ASDestination::DESTINATION_SCRIPT) { JS::loader::ScriptKind scriptKind = JS::loader::ScriptKind::eClassic; if (aType.LowerCaseEqualsASCII("module")) { scriptKind = JS::loader::ScriptKind::eModule; } return Some(PreloadHashKey::CreateAsScript(aURI, aCorsMode, scriptKind)); } if (aAs == ASDestination::DESTINATION_STYLE) { return Some(PreloadHashKey::CreateAsStyle( aURI, aPrincipal, aCorsMode, css::SheetParsingMode::eAuthorSheetFeatures)); } if (aAs == ASDestination::DESTINATION_FETCH) { return Some(PreloadHashKey::CreateAsFetch(aURI, aCorsMode)); } return Nothing(); } /* static */ nsSecurityFlags EarlyHintPreloader::ComputeSecurityFlags(CORSMode aCORSMode, ASDestination aAs, bool aIsModule) { 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) { if (aIsModule) { return nsContentSecurityManager::ComputeSecurityFlags( aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: REQUIRE_CORS_CHECKS) | nsILoadInfo::SEC_ALLOW_CHROME; } 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& aHeader, nsIURI* aBaseURI, nsIPrincipal* aTriggeringPrincipal, nsICookieJarSettings* aCookieJarSettings) { if (!aHeader.mRel.LowerCaseEqualsASCII("preload")) { return; } nsAttrValue as; ParseAsValue(aHeader.mAs, as); if (as.GetEnumValue() == ASDestination::DESTINATION_INVALID) { // 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; } nsCOMPtr uri; // use the base uri NS_ENSURE_SUCCESS_VOID(aHeader.NewResolveHref(getter_AddRefs(uri), aBaseURI)); // only preload secure context urls if (!uri->SchemeIs("https")) { return; } CORSMode corsMode = dom::Element::StringToCORSMode(aHeader.mCrossOrigin); Maybe hashKey = GenerateHashKey(static_cast(as.GetEnumValue()), uri, aTriggeringPrincipal, corsMode, aHeader.mType); if (!hashKey) { return; } if (aOngoingEarlyHints->Contains(*hashKey)) { return; } nsContentPolicyType contentPolicyType = AsValueToContentPolicy(as); if (contentPolicyType == nsContentPolicyType::TYPE_INVALID) { return; } dom::ReferrerPolicy referrerPolicy = dom::ReferrerInfo::ReferrerPolicyAttributeFromString( aHeader.mReferrerPolicy); nsCOMPtr referrerInfo = new dom::ReferrerInfo(aBaseURI, referrerPolicy); RefPtr earlyHintPreloader = RefPtr(new EarlyHintPreloader(uri)); nsSecurityFlags securityFlags = EarlyHintPreloader::ComputeSecurityFlags( corsMode, static_cast(as.GetEnumValue()), aHeader.mType.LowerCaseEqualsASCII("module")); NS_ENSURE_SUCCESS_VOID(earlyHintPreloader->OpenChannel( aTriggeringPrincipal, securityFlags, contentPolicyType, referrerInfo, aCookieJarSettings)); DebugOnly result = aOngoingEarlyHints->Add(*hashKey, earlyHintPreloader); MOZ_ASSERT(result); } nsresult EarlyHintPreloader::OpenChannel( nsIPrincipal* aTriggeringPrincipal, nsSecurityFlags aSecurityFlags, nsContentPolicyType aContentPolicyType, nsIReferrerInfo* aReferrerInfo, nsICookieJarSettings* aCookieJarSettings) { 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), mURI, aTriggeringPrincipal, aSecurityFlags, aContentPolicyType, aCookieJarSettings, /* aPerformanceStorage */ nullptr, /* aLoadGroup */ nullptr, /* aCallbacks */ this, nsIRequest::LOAD_NORMAL); NS_ENSURE_SUCCESS(rv, rv); // 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)); return mChannel->AsyncOpen(this); } nsresult EarlyHintPreloader::CancelChannel(nsresult aStatus) { // clear redirect channel in case this channel is cleared between the call of // EarlyHintPreloader::AsyncOnChannelRedirect and // EarlyHintPreloader::OnRedirectResult mRedirectChannel = nullptr; if (mChannel) { mChannel->Cancel(aStatus); mChannel = nullptr; } return NS_OK; } //----------------------------------------------------------------------------- // EarlyHintPreloader::nsISupports //----------------------------------------------------------------------------- NS_IMPL_ISUPPORTS(EarlyHintPreloader, nsIRequestObserver, nsIStreamListener, nsIChannelEventSink, nsIInterfaceRequestor, nsIRedirectResultListener) //----------------------------------------------------------------------------- // EarlyHintPreloader::nsIStreamListener //----------------------------------------------------------------------------- NS_IMETHODIMP EarlyHintPreloader::OnStartRequest(nsIRequest* aRequest) { LOG(("EarlyHintPreloader::OnStartRequest\n")); nsCOMPtr cacheInfoChannel = do_QueryInterface(aRequest); if (!cacheInfoChannel) { return NS_ERROR_ABORT; } // no need to prefetch an asset that is already in the cache bool fromCache; if (NS_SUCCEEDED(cacheInfoChannel->IsFromCache(&fromCache)) && fromCache) { LOG(("document is already in the cache; canceling prefetch\n")); return NS_BINDING_ABORTED; } return NS_OK; } NS_IMETHODIMP EarlyHintPreloader::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, uint64_t aOffset, uint32_t aCount) { uint32_t bytesRead = 0; nsresult rv = aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead); LOG(("prefetched %u bytes [offset=%" PRIu64 "]\n", bytesRead, aOffset)); return rv; } NS_IMETHODIMP EarlyHintPreloader::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { LOG(("EarlyHintPreloader::OnStopRequest\n")); mChannel = nullptr; return NS_OK; } //----------------------------------------------------------------------------- // EarlyHintPreloader::nsIChannelEventSink //----------------------------------------------------------------------------- NS_IMETHODIMP EarlyHintPreloader::AsyncOnChannelRedirect( nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, nsIAsyncVerifyRedirectCallback* callback) { 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; } // abort the request if redirecting to insecure context if (!newURI->SchemeIs("https")) { callback->OnRedirectVerifyCallback(NS_ERROR_ABORT); 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(bool aProceeding) { if (aProceeding && mRedirectChannel) { mChannel = mRedirectChannel; } mRedirectChannel = nullptr; 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; } return NS_ERROR_NO_INTERFACE; } } // namespace mozilla::net