forked from mirrors/gecko-dev
381 lines
13 KiB
C++
381 lines
13 KiB
C++
/* 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<EarlyHintPreloader> aPreloader) {
|
|
return mOngoingPreloads.InsertOrUpdate(aKey, aPreloader);
|
|
}
|
|
|
|
//=============================================================================
|
|
// EarlyHintPreloader
|
|
//=============================================================================
|
|
|
|
EarlyHintPreloader::EarlyHintPreloader(nsIURI* aURI) : mURI(aURI) {}
|
|
|
|
/* static */
|
|
Maybe<PreloadHashKey> 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<nsIURI> 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<PreloadHashKey> hashKey =
|
|
GenerateHashKey(static_cast<ASDestination>(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<nsIReferrerInfo> referrerInfo =
|
|
new dom::ReferrerInfo(aBaseURI, referrerPolicy);
|
|
|
|
RefPtr<EarlyHintPreloader> earlyHintPreloader =
|
|
RefPtr(new EarlyHintPreloader(uri));
|
|
|
|
nsSecurityFlags securityFlags = EarlyHintPreloader::ComputeSecurityFlags(
|
|
corsMode, static_cast<ASDestination>(as.GetEnumValue()),
|
|
aHeader.mType.LowerCaseEqualsASCII("module"));
|
|
|
|
NS_ENSURE_SUCCESS_VOID(earlyHintPreloader->OpenChannel(
|
|
aTriggeringPrincipal, securityFlags, contentPolicyType, referrerInfo,
|
|
aCookieJarSettings));
|
|
|
|
DebugOnly<bool> 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<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));
|
|
|
|
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<nsICacheInfoChannel> 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<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;
|
|
}
|
|
|
|
// 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<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(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<nsIChannelEventSink*>(this);
|
|
return NS_OK;
|
|
}
|
|
|
|
if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) {
|
|
NS_ADDREF_THIS();
|
|
*aResult = static_cast<nsIRedirectResultListener*>(this);
|
|
return NS_OK;
|
|
}
|
|
|
|
return NS_ERROR_NO_INTERFACE;
|
|
}
|
|
|
|
} // namespace mozilla::net
|