blob: 07ea9571f8c59d6429a31db7e317f55635c7492c [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/loader/navigation_early_hints_manager.h"
#include "base/memory/raw_ref.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/global_request_id.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/url_loader_throttles.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/referrer.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/system/data_pipe_drainer.h"
#include "net/base/load_flags.h"
#include "net/base/schemeful_site.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/url_request_job.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.h"
#include "services/network/public/cpp/content_security_policy/csp_source_list.h"
#include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/connection_change_observer_client.mojom.h"
#include "services/network/public/mojom/content_security_policy.mojom-shared.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/loader/network_utils.h"
#include "third_party/blink/public/common/loader/throttling_url_loader.h"
#include "third_party/blink/public/common/loader/url_loader_throttle.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"
#include "url/origin.h"
namespace content {
namespace {
const net::NetworkTrafficAnnotationTag kEarlyHintsPreloadTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("early_hints_preload",
R"(
semantics {
sender: "Early Hints"
description:
"This request is issued during a main frame navigation to "
"speculatively fetch resources that will likely be used in the frame."
trigger:
"A 103 Early Hints HTTP informational response is received during "
"navigation."
data:
"Arbitrary site-controlled data can be included in the URL."
"Requests may include cookies and site-specific credentials."
destination: WEBSITE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting:
"This feature cannot be disabled by Settings. This feature is not "
"enabled by default yet. TODO(crbug.com/40496584): Update this "
"description once the feature is ready."
chrome_policy {
URLBlocklist {
URLBlocklist: { entries: '*' }
}
}
chrome_policy {
URLAllowlist {
URLAllowlist { }
}
}
}
comments:
"Chrome uses this type of request during navigation and it cannot be "
"disabled. Using either URLBlocklist or URLAllowlist (or a combination "
"of both) limits the scope of these requests."
)");
network::mojom::CSPDirectiveName LinkAsAttributeToCSPDirective(
network::mojom::LinkAsAttribute attr) {
// https://w3c.github.io/webappsec-csp/#csp-directives
switch (attr) {
case network::mojom::LinkAsAttribute::kUnspecified:
return network::mojom::CSPDirectiveName::Unknown;
case network::mojom::LinkAsAttribute::kImage:
return network::mojom::CSPDirectiveName::ImgSrc;
case network::mojom::LinkAsAttribute::kFont:
return network::mojom::CSPDirectiveName::FontSrc;
case network::mojom::LinkAsAttribute::kScript:
return network::mojom::CSPDirectiveName::ScriptSrcElem;
case network::mojom::LinkAsAttribute::kStyleSheet:
return network::mojom::CSPDirectiveName::StyleSrcElem;
case network::mojom::LinkAsAttribute::kFetch:
return network::mojom::CSPDirectiveName::ConnectSrc;
}
NOTREACHED();
}
bool CheckContentSecurityPolicyForPreload(
const network::mojom::LinkHeaderPtr& link,
const std::vector<network::mojom::ContentSecurityPolicyPtr>&
content_security_policies) {
DCHECK(link->rel == network::mojom::LinkRelAttribute::kPreload ||
link->rel == network::mojom::LinkRelAttribute::kModulePreload);
network::mojom::CSPDirectiveName directive =
LinkAsAttributeToCSPDirective(link->as);
for (network::mojom::CSPDirectiveName effective_directive = directive;
effective_directive != network::mojom::CSPDirectiveName::Unknown;
effective_directive =
network::CSPFallbackDirective(effective_directive, directive)) {
for (auto& policy : content_security_policies) {
const auto& it = policy->directives.find(effective_directive);
if (it == policy->directives.end()) {
continue;
}
if (!network::CheckCSPSourceList(directive, *it->second, link->href,
*(policy->self_origin),
/*has_followed_redirect=*/false,
/*is_opaque_fenced_frame=*/false)) {
// TODO(crbug.com/40218207): Report CSP violation once the final
// response is received.
return false;
}
}
}
return true;
}
std::optional<network::mojom::RequestDestination>
LinkAsAttributeToRequestDestination(const network::mojom::LinkHeaderPtr& link) {
// https://fetch.spec.whatwg.org/#concept-potential-destination-translate
switch (link->as) {
case network::mojom::LinkAsAttribute::kUnspecified:
// For modulepreload, the request destination should be "script" when `as`
// is not specified.
// https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload
if (link->rel == network::mojom::LinkRelAttribute::kModulePreload) {
return network::mojom::RequestDestination::kScript;
}
return std::nullopt;
case network::mojom::LinkAsAttribute::kImage:
return network::mojom::RequestDestination::kImage;
case network::mojom::LinkAsAttribute::kFont:
return network::mojom::RequestDestination::kFont;
case network::mojom::LinkAsAttribute::kScript:
return network::mojom::RequestDestination::kScript;
case network::mojom::LinkAsAttribute::kStyleSheet:
return network::mojom::RequestDestination::kStyle;
case network::mojom::LinkAsAttribute::kFetch:
return network::mojom::RequestDestination::kEmpty;
}
}
network::mojom::RequestMode CalculateRequestMode(
const network::mojom::LinkHeaderPtr& link) {
if (link->rel == network::mojom::LinkRelAttribute::kModulePreload) {
// When fetching a module script, mode is always "cors".
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script
return network::mojom::RequestMode::kCors;
}
switch (link->cross_origin) {
case network::mojom::CrossOriginAttribute::kUnspecified:
return network::mojom::RequestMode::kNoCors;
case network::mojom::CrossOriginAttribute::kAnonymous:
case network::mojom::CrossOriginAttribute::kUseCredentials:
return network::mojom::RequestMode::kCors;
}
NOTREACHED();
}
network::mojom::CredentialsMode CalculateCredentialsMode(
const network::mojom::LinkHeaderPtr& link) {
switch (link->cross_origin) {
case network::mojom::CrossOriginAttribute::kUnspecified:
// For modulepreload credentials mode should be "same-origin" when
// `cross-origin` is not specified.
if (link->rel == network::mojom::LinkRelAttribute::kModulePreload) {
return network::mojom::CredentialsMode::kSameOrigin;
} else {
return network::mojom::CredentialsMode::kInclude;
}
case network::mojom::CrossOriginAttribute::kUseCredentials:
return network::mojom::CredentialsMode::kInclude;
case network::mojom::CrossOriginAttribute::kAnonymous:
return network::mojom::CredentialsMode::kSameOrigin;
}
NOTREACHED();
}
} // namespace
NavigationEarlyHintsManagerParams::NavigationEarlyHintsManagerParams(
const url::Origin& origin,
net::IsolationInfo isolation_info,
mojo::Remote<network::mojom::URLLoaderFactory> loader_factory)
: origin(origin),
isolation_info(std::move(isolation_info)),
loader_factory(std::move(loader_factory)) {}
NavigationEarlyHintsManagerParams::~NavigationEarlyHintsManagerParams() =
default;
NavigationEarlyHintsManagerParams::NavigationEarlyHintsManagerParams(
NavigationEarlyHintsManagerParams&&) = default;
NavigationEarlyHintsManagerParams& NavigationEarlyHintsManagerParams::operator=(
NavigationEarlyHintsManagerParams&&) = default;
// Represents a preconnect.
struct NavigationEarlyHintsManager::PreconnectEntry {
PreconnectEntry(const url::Origin& origin,
network::mojom::CrossOriginAttribute cross_origin);
~PreconnectEntry();
PreconnectEntry(const PreconnectEntry&);
PreconnectEntry& operator=(const PreconnectEntry&);
bool operator==(const PreconnectEntry&);
bool operator<(const PreconnectEntry&) const;
url::Origin origin;
network::mojom::CrossOriginAttribute cross_origin;
};
NavigationEarlyHintsManager::PreconnectEntry::PreconnectEntry(
const url::Origin& origin,
network::mojom::CrossOriginAttribute cross_origin)
: origin(origin), cross_origin(cross_origin) {}
NavigationEarlyHintsManager::PreconnectEntry::~PreconnectEntry() = default;
NavigationEarlyHintsManager::PreconnectEntry::PreconnectEntry(
const PreconnectEntry&) = default;
NavigationEarlyHintsManager::PreconnectEntry&
NavigationEarlyHintsManager::PreconnectEntry::operator=(
const PreconnectEntry&) = default;
bool NavigationEarlyHintsManager::PreconnectEntry::operator==(
const PreconnectEntry& other) {
return origin == other.origin && cross_origin == other.cross_origin;
}
bool NavigationEarlyHintsManager::PreconnectEntry::operator<(
const PreconnectEntry& other) const {
if (origin == other.origin) {
return cross_origin < other.cross_origin;
}
return origin < other.origin;
}
NavigationEarlyHintsManager::PreloadedResource::PreloadedResource() = default;
NavigationEarlyHintsManager::PreloadedResource::~PreloadedResource() = default;
NavigationEarlyHintsManager::PreloadedResource::PreloadedResource(
const PreloadedResource&) = default;
NavigationEarlyHintsManager::PreloadedResource&
NavigationEarlyHintsManager::PreloadedResource::operator=(
const PreloadedResource&) = default;
NavigationEarlyHintsManager::InflightPreload::InflightPreload(
std::unique_ptr<blink::ThrottlingURLLoader> loader,
std::unique_ptr<PreloadURLLoaderClient> client)
: client(std::move(client)), loader(std::move(loader)) {}
NavigationEarlyHintsManager::InflightPreload::~InflightPreload() = default;
// A URLLoaderClient which drains the content of a request to put a
// response into the disk cache. If the response was already in the cache,
// this tries to cancel reading body to avoid further disk access.
class NavigationEarlyHintsManager::PreloadURLLoaderClient
: public network::mojom::URLLoaderClient,
public mojo::DataPipeDrainer::Client {
public:
PreloadURLLoaderClient(NavigationEarlyHintsManager& owner,
const network::ResourceRequest& request)
: owner_(owner), url_(request.url) {}
~PreloadURLLoaderClient() override = default;
PreloadURLLoaderClient(const PreloadURLLoaderClient&) = delete;
PreloadURLLoaderClient& operator=(const PreloadURLLoaderClient&) = delete;
PreloadURLLoaderClient(PreloadURLLoaderClient&&) = delete;
PreloadURLLoaderClient& operator=(PreloadURLLoaderClient&&) = delete;
private:
// mojom::URLLoaderClient overrides:
void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override {
}
void OnReceiveResponse(
network::mojom::URLResponseHeadPtr head,
mojo::ScopedDataPipeConsumerHandle body,
std::optional<mojo_base::BigBuffer> cached_metadata) override {
if (!head->network_accessed && head->was_fetched_via_cache) {
// Cancel the client since the response is already stored in the cache.
result_.was_canceled = true;
MaybeCompletePreload();
return;
}
if (!body) {
return;
}
if (response_body_drainer_) {
mojo::ReportBadMessage("NEHM_BAD_RESPONSE_BODY");
return;
}
response_body_drainer_ =
std::make_unique<mojo::DataPipeDrainer>(this, std::move(body));
}
void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr head) override {}
void OnUploadProgress(int64_t current_position,
int64_t total_size,
OnUploadProgressCallback callback) override {
NOTREACHED();
}
void OnTransferSizeUpdated(int32_t transfer_size_diff) override {
network::RecordOnTransferSizeUpdatedUMA(
network::OnTransferSizeUpdatedFrom::kPreloadURLLoaderClient);
}
void OnComplete(const network::URLLoaderCompletionStatus& status) override {
if (result_.was_canceled || result_.error_code.has_value()) {
mojo::ReportBadMessage("NEHM_BAD_COMPLETE");
return;
}
result_.error_code = status.error_code;
result_.cors_error_status = status.cors_error_status;
MaybeCompletePreload();
}
// mojo::DataPipeDrainer::Client overrides:
void OnDataAvailable(base::span<const uint8_t> data) override {}
void OnDataComplete() override {
DCHECK(response_body_drainer_);
response_body_drainer_.reset();
MaybeCompletePreload();
}
bool CanCompletePreload() {
if (result_.was_canceled) {
return true;
}
if (result_.error_code.has_value() && !response_body_drainer_) {
return true;
}
return false;
}
void MaybeCompletePreload() {
if (CanCompletePreload()) {
// Delete `this`.
owner_->OnPreloadComplete(url_, result_);
}
}
const raw_ref<NavigationEarlyHintsManager> owner_;
const GURL url_;
PreloadedResource result_;
std::unique_ptr<mojo::DataPipeDrainer> response_body_drainer_;
};
NavigationEarlyHintsManager::NavigationEarlyHintsManager(
BrowserContext& browser_context,
StoragePartition& storage_partition,
FrameTreeNodeId frame_tree_node_id,
NavigationEarlyHintsManagerParams params)
: browser_context_(browser_context),
storage_partition_(storage_partition),
frame_tree_node_id_(frame_tree_node_id),
loader_factory_(std::move(params.loader_factory)),
origin_(params.origin),
isolation_info_(std::move(params.isolation_info)) {
shared_loader_factory_ =
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
loader_factory_.get());
}
NavigationEarlyHintsManager::~NavigationEarlyHintsManager() = default;
void NavigationEarlyHintsManager::HandleEarlyHints(
network::mojom::EarlyHintsPtr early_hints,
const network::ResourceRequest& request_for_navigation) {
// Ignore the second and subsequent responses to avoid situations where
// policies such as CSP are inconsistent among the first and following
// responses. This behavior is specified by the step 19.5 of
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#create-navigation-params-by-fetching
if (first_early_hints_receive_time_) {
return;
}
first_early_hints_receive_time_ = base::TimeTicks::Now();
net::ReferrerPolicy referrer_policy =
Referrer::ReferrerPolicyForUrlRequest(early_hints->referrer_policy);
for (const auto& link : early_hints->headers->link_headers) {
// TODO(crbug.com/40496584): Support other `rel` attributes.
if (link->rel == network::mojom::LinkRelAttribute::kPreconnect) {
MaybePreconnect(link);
} else if (link->rel == network::mojom::LinkRelAttribute::kPreload ||
link->rel == network::mojom::LinkRelAttribute::kModulePreload) {
MaybePreloadHintedResource(link, request_for_navigation,
early_hints->headers->content_security_policy,
referrer_policy);
}
}
}
bool NavigationEarlyHintsManager::WasResourceHintsReceived() const {
return was_resource_hints_received_;
}
std::vector<GURL> NavigationEarlyHintsManager::TakePreloadedResourceURLs() {
return std::move(preloaded_urls_);
}
bool NavigationEarlyHintsManager::HasInflightPreloads() const {
return inflight_preloads_.size() > 0;
}
void NavigationEarlyHintsManager::WaitForPreloadsFinishedForTesting(
base::OnceCallback<void(PreloadedResources)> callback) {
DCHECK(!preloads_completion_callback_for_testing_);
if (inflight_preloads_.empty()) {
std::move(callback).Run(preloaded_resources_);
} else {
preloads_completion_callback_for_testing_ = std::move(callback);
}
}
void NavigationEarlyHintsManager::SetNetworkContextForTesting(
network::mojom::NetworkContext* network_context) {
DCHECK(!network_context_for_testing_);
DCHECK(network_context);
network_context_for_testing_ = network_context;
}
network::mojom::NetworkContext*
NavigationEarlyHintsManager::GetNetworkContext() {
if (network_context_for_testing_) {
return network_context_for_testing_;
}
return storage_partition_->GetNetworkContext();
}
void NavigationEarlyHintsManager::MaybePreconnect(
const network::mojom::LinkHeaderPtr& link) {
was_resource_hints_received_ = true;
if (!ShouldHandleResourceHints(link)) {
return;
}
PreconnectEntry entry(url::Origin::Create(link->href), link->cross_origin);
if (preconnect_entries_.contains(entry)) {
return;
}
network::mojom::NetworkContext* network_context = GetNetworkContext();
if (!network_context) {
return;
}
bool allow_credentials =
link->cross_origin != network::mojom::CrossOriginAttribute::kAnonymous;
network_context->PreconnectSockets(
/*num_streams=*/1, link->href,
allow_credentials ? network::mojom::CredentialsMode::kInclude
: network::mojom::CredentialsMode::kOmit,
isolation_info_.network_anonymization_key(),
net::MutableNetworkTrafficAnnotationTag(
kEarlyHintsPreloadTrafficAnnotation),
/*keepalive_config=*/std::nullopt, mojo::NullRemote());
preconnect_entries_.insert(std::move(entry));
}
void NavigationEarlyHintsManager::MaybePreloadHintedResource(
const network::mojom::LinkHeaderPtr& link,
const network::ResourceRequest& request_for_navigation,
const std::vector<network::mojom::ContentSecurityPolicyPtr>&
content_security_policies,
net::ReferrerPolicy referrer_policy) {
DCHECK(request_for_navigation.is_outermost_main_frame);
DCHECK(request_for_navigation.url.SchemeIsHTTPOrHTTPS());
was_resource_hints_received_ = true;
if (!ShouldHandleResourceHints(link)) {
return;
}
// Step 2. If options's destination is not a destination, then return null.
// https://html.spec.whatwg.org/multipage/semantics.html#create-a-link-request
std::optional<network::mojom::RequestDestination> destination =
LinkAsAttributeToRequestDestination(link);
if (!destination) {
return;
}
if (!CheckContentSecurityPolicyForPreload(link, content_security_policies)) {
return;
}
if (inflight_preloads_.contains(link->href) ||
preloaded_resources_.contains(link->href)) {
return;
}
auto preload_origin = url::Origin::Create(link->href);
net::SiteForCookies site_for_cookies =
net::SiteForCookies::FromOrigin(origin_);
network::ResourceRequest request;
request.method = net::HttpRequestHeaders::kGetMethod;
request.priority = CalculateRequestPriority(link);
request.destination = *destination;
request.url = link->href;
request.site_for_cookies = site_for_cookies;
request.request_initiator = origin_;
request.referrer = net::URLRequestJob::ComputeReferrerForPolicy(
referrer_policy, request_for_navigation.url, request.url);
request.referrer_policy = referrer_policy;
request.load_flags = net::LOAD_NORMAL;
request.resource_type =
static_cast<int>(blink::mojom::ResourceType::kSubResource);
request.mode = CalculateRequestMode(link);
request.credentials_mode = CalculateCredentialsMode(link);
blink::network_utils::SetAcceptHeader(request.headers, request.destination);
std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles =
CreateContentBrowserURLLoaderThrottles(
request, &*browser_context_,
base::BindRepeating(&WebContents::FromFrameTreeNodeId,
frame_tree_node_id_),
/*navigation_ui_data=*/nullptr, frame_tree_node_id_,
/*navigation_id=*/std::nullopt);
auto loader_client = std::make_unique<PreloadURLLoaderClient>(*this, request);
auto loader = blink::ThrottlingURLLoader::CreateLoaderAndStart(
shared_loader_factory_, std::move(throttles),
content::GlobalRequestID::MakeBrowserInitiated().request_id,
network::mojom::kURLLoadOptionNone, &request, loader_client.get(),
kEarlyHintsPreloadTrafficAnnotation,
base::SingleThreadTaskRunner::GetCurrentDefault());
inflight_preloads_[request.url] = std::make_unique<InflightPreload>(
std::move(loader), std::move(loader_client));
preloaded_urls_.push_back(request.url);
}
bool NavigationEarlyHintsManager::ShouldHandleResourceHints(
const network::mojom::LinkHeaderPtr& link) {
if (!link->href.SchemeIsHTTPOrHTTPS()) {
return false;
}
return true;
}
void NavigationEarlyHintsManager::OnPreloadComplete(
const GURL& url,
const PreloadedResource& result) {
DCHECK(inflight_preloads_.contains(url));
DCHECK(!preloaded_resources_.contains(url));
preloaded_resources_[url] = result;
inflight_preloads_.erase(url);
if (inflight_preloads_.empty() && preloads_completion_callback_for_testing_) {
std::move(preloads_completion_callback_for_testing_)
.Run(preloaded_resources_);
}
// TODO(crbug.com/40496584): Consider to delete `this` when there is no
// inflight preloads.
}
// Used to determine a priority for a speculative subresource request.
// TODO(crbug.com/40496584): This is almost the same as GetRequestPriority() in
// loading_predictor_tab_helper.cc and the purpose is the same. Consider merging
// them if the logic starts to be more mature.
// platform/loader/fetch/README.md in blink contains more details on
// prioritization as well as links to all of the relevant places in the code
// where priority is determined. If the priority logic is updated here, be sure
// to update the other code as needed.
net::RequestPriority NavigationEarlyHintsManager::CalculateRequestPriority(
const network::mojom::LinkHeaderPtr& link) {
// When fetchPriority is explicitly specified for preload, independent of
// most content types, the blink priority matches the fetchpriority value.
// In net priority terms that maps to MEDIUM for "high" LOWEST for "low".
// https://web.dev/priority-hints/#browser-priority-and-fetchpriority
switch (link->fetch_priority) {
case network::mojom::FetchPriorityAttribute::kHigh:
switch (link->as) {
case network::mojom::LinkAsAttribute::kStyleSheet:
return net::HIGHEST;
default:
return net::MEDIUM;
}
case network::mojom::FetchPriorityAttribute::kLow:
return net::LOWEST;
case network::mojom::FetchPriorityAttribute::kAuto:
switch (link->as) {
case network::mojom::LinkAsAttribute::kStyleSheet:
return net::HIGHEST;
case network::mojom::LinkAsAttribute::kFont:
case network::mojom::LinkAsAttribute::kScript:
return net::MEDIUM;
case network::mojom::LinkAsAttribute::kImage:
case network::mojom::LinkAsAttribute::kFetch:
return net::LOWEST;
case network::mojom::LinkAsAttribute::kUnspecified:
return net::IDLE;
}
}
NOTREACHED();
}
} // namespace content