blob: c61082b57d245a71314d229f95e3147b8cc824d1 [file] [log] [blame]
// Copyright 2019 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/web_package/prefetched_signed_exchange_cache.h"
#include <optional>
#include <string_view>
#include <utility>
#include "base/base64.h"
#include "base/compiler_specific.h"
#include "base/feature_list.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/strings/string_util.h"
#include "components/link_header_util/link_header_util.h"
#include "content/browser/loader/cross_origin_read_blocking_checker.h"
#include "content/browser/loader/navigation_loader_interceptor.h"
#include "content/browser/loader/response_head_update_params.h"
#include "content/browser/navigation_subresource_loader_params.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_package/signed_exchange_inner_response_url_loader.h"
#include "content/browser/web_package/signed_exchange_reporter.h"
#include "content/browser/web_package/signed_exchange_utils.h"
#include "content/browser/web_package/subresource_signed_exchange_url_loader_factory.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_features.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/base/network_anonymization_key.h"
#include "net/cookies/cookie_setting_override.h"
#include "net/http/http_cache.h"
#include "net/http/http_util.h"
#include "net/url_request/redirect_util.h"
#include "services/network/public/cpp/constants.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/initiator_lock_compatibility.h"
#include "services/network/public/cpp/orb/orb_api.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/single_request_url_loader_factory.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
#include "services/network/public/mojom/restricted_cookie_manager.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "storage/browser/blob/blob_data_handle.h"
#include "storage/browser/blob/blob_impl.h"
#include "storage/browser/blob/mojo_blob_reader.h"
namespace content {
namespace {
// The max number of cached entry per one frame. This limit is intended to
// prevent OOM crash of the browser process.
constexpr size_t kMaxEntrySize = 100u;
// A URLLoader which returns a synthesized redirect response for signed
// exchange's outer URL request.
class RedirectResponseURLLoader : public network::mojom::URLLoader {
public:
RedirectResponseURLLoader(
const network::ResourceRequest& url_request,
const GURL& inner_url,
const network::mojom::URLResponseHead& outer_response,
mojo::PendingRemote<network::mojom::URLLoaderClient> client)
: client_(std::move(client)) {
auto response_head = signed_exchange_utils::CreateRedirectResponseHead(
outer_response, false /* is_fallback_redirect */);
response_head->was_fetched_via_cache = true;
response_head->was_in_prefetch_cache = true;
SignedExchangeInnerResponseURLLoader::UpdateRequestResponseStartTime(
response_head.get());
client_->OnReceiveRedirect(signed_exchange_utils::CreateRedirectInfo(
inner_url, url_request, outer_response,
false /* is_fallback_redirect */),
std::move(response_head));
}
RedirectResponseURLLoader(const RedirectResponseURLLoader&) = delete;
RedirectResponseURLLoader& operator=(const RedirectResponseURLLoader&) =
delete;
~RedirectResponseURLLoader() override {}
private:
// network::mojom::URLLoader overrides:
void FollowRedirect(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
const net::HttpRequestHeaders& modified_cors_exempt_headers,
const std::optional<GURL>& new_url) override {
NOTREACHED();
}
void SetPriority(net::RequestPriority priority,
int intra_priority_value) override {
// There is nothing to do, because this class just calls OnReceiveRedirect.
}
mojo::Remote<network::mojom::URLLoaderClient> client_;
};
// A NavigationLoaderInterceptor which handles a request which matches the
// prefetched signed exchange that has been stored to a
// PrefetchedSignedExchangeCache.
class PrefetchedNavigationLoaderInterceptor
: public NavigationLoaderInterceptor {
public:
PrefetchedNavigationLoaderInterceptor(
std::unique_ptr<const PrefetchedSignedExchangeCacheEntry> exchange,
std::vector<blink::mojom::PrefetchedSignedExchangeInfoPtr> info_list,
mojo::Remote<network::mojom::RestrictedCookieManager> cookie_manager)
: exchange_(std::move(exchange)),
info_list_(std::move(info_list)),
cookie_manager_(std::move(cookie_manager)) {}
PrefetchedNavigationLoaderInterceptor(
const PrefetchedNavigationLoaderInterceptor&) = delete;
PrefetchedNavigationLoaderInterceptor& operator=(
const PrefetchedNavigationLoaderInterceptor&) = delete;
~PrefetchedNavigationLoaderInterceptor() override {}
void MaybeCreateLoader(
const network::ResourceRequest& tentative_resource_request,
BrowserContext* browser_context,
LoaderCallback callback,
FallbackCallback fallback_callback) override {
if (state_ == State::kInitial &&
tentative_resource_request.url == exchange_->outer_url()) {
state_ = State::kOuterRequestRequested;
std::move(callback).Run(NavigationLoaderInterceptor::Result(
base::MakeRefCounted<network::SingleRequestURLLoaderFactory>(
base::BindOnce(
&PrefetchedNavigationLoaderInterceptor::StartRedirectResponse,
weak_factory_.GetWeakPtr())),
/*subresource_loader_params=*/{}));
return;
}
if (tentative_resource_request.url == exchange_->inner_url()) {
DCHECK_EQ(State::kOuterRequestRequested, state_);
if (signed_exchange_utils::IsCookielessOnlyExchange(
*exchange_->inner_response()->headers)) {
DCHECK(cookie_manager_);
state_ = State::kCheckingCookies;
CheckAbsenceOfCookies(tentative_resource_request, std::move(callback));
return;
} else {
state_ = State::kInnerResponseRequested;
SubresourceLoaderParams params;
params.prefetched_signed_exchanges = std::move(info_list_);
std::move(callback).Run(NavigationLoaderInterceptor::Result(
base::MakeRefCounted<network::SingleRequestURLLoaderFactory>(
base::BindOnce(
&PrefetchedNavigationLoaderInterceptor::StartInnerResponse,
weak_factory_.GetWeakPtr())),
std::move(params)));
return;
}
}
DUMP_WILL_BE_NOTREACHED();
}
private:
enum class State {
kInitial,
kOuterRequestRequested,
kCheckingCookies,
kInnerResponseRequested
};
void CheckAbsenceOfCookies(const network::ResourceRequest& request,
LoaderCallback callback) {
auto match_options = network::mojom::CookieManagerGetOptions::New();
match_options->name = "";
match_options->match_type = network::mojom::CookieMatchType::STARTS_WITH;
cookie_manager_->GetAllForUrl(
request.url, request.trusted_params->isolation_info.site_for_cookies(),
*request.trusted_params->isolation_info.top_frame_origin(),
request.storage_access_api_status, std::move(match_options),
request.is_ad_tagged,
/*apply_devtools_overrides=*/false,
/*force_disable_third_party_cookies=*/false,
base::BindOnce(&PrefetchedNavigationLoaderInterceptor::OnGetCookies,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void OnGetCookies(LoaderCallback callback,
const std::vector<net::CookieWithAccessResult>& results) {
DCHECK_EQ(State::kCheckingCookies, state_);
if (!results.empty()) {
signed_exchange_utils::RecordLoadResultHistogram(
SignedExchangeLoadResult::kHadCookieForCookielessOnlySXG);
ResponseHeadUpdateParams head_update_params;
head_update_params.load_timing_info =
this->exchange_->outer_response()->load_timing;
// TODO(crbug.com/40266535) test workerStart in SXG scenarios
std::move(callback).Run(NavigationLoaderInterceptor::Result(
/*factory=*/nullptr, /*subresource_loader_params=*/{},
std::move(head_update_params)));
return;
}
state_ = State::kInnerResponseRequested;
SubresourceLoaderParams params;
params.prefetched_signed_exchanges = std::move(info_list_);
std::move(callback).Run(NavigationLoaderInterceptor::Result(
base::MakeRefCounted<network::SingleRequestURLLoaderFactory>(
base::BindOnce(
&PrefetchedNavigationLoaderInterceptor::StartInnerResponse,
weak_factory_.GetWeakPtr())),
std::move(params)));
}
void StartRedirectResponse(
const network::ResourceRequest& resource_request,
mojo::PendingReceiver<network::mojom::URLLoader> receiver,
mojo::PendingRemote<network::mojom::URLLoaderClient> client) {
mojo::MakeSelfOwnedReceiver(
std::make_unique<RedirectResponseURLLoader>(
resource_request, exchange_->inner_url(),
*exchange_->outer_response(), std::move(client)),
std::move(receiver));
}
void StartInnerResponse(
const network::ResourceRequest& resource_request,
mojo::PendingReceiver<network::mojom::URLLoader> receiver,
mojo::PendingRemote<network::mojom::URLLoaderClient> client) {
// `resource_request.request_initiator()` is trustworthy, because:
// 1) StartInnerResponse is only used from the navigation stack (via
// MaybeCreateLoader override of NavigationLoaderInterceptor)
// 2) navigation initiator is validated in IPCs from the renderer (e.g. see
// VerifyBeginNavigationCommonParams).
// Note that `request_initiator_origin_lock` below might be different from
// `url::Origin::Create(exchange_->inner_url())` - for example in the
// All/SignedExchangeRequestHandlerBrowserTest.Simple/3 testcase.
CHECK_EQ(network::mojom::RequestMode::kNavigate, resource_request.mode);
// PrefetchedNavigationLoaderInterceptor is only created for
// renderer-initiated navigations - therefore `request_initiator` is
// guaranteed to have a value here.
CHECK(resource_request.request_initiator.has_value());
// Okay to use separate/empty ORB state for each navigation request.
// (Because ORB doesn't apply to navigation requests.)
auto empty_orb_state = base::MakeRefCounted<
base::RefCountedData<network::orb::PerFactoryState>>();
mojo::MakeSelfOwnedReceiver(
std::make_unique<SignedExchangeInnerResponseURLLoader>(
resource_request, exchange_->inner_response().Clone(),
std::make_unique<const storage::BlobDataHandle>(
*exchange_->blob_data_handle()),
*exchange_->completion_status(), std::move(client),
true /* is_navigation_request */, std::move(empty_orb_state)),
std::move(receiver));
}
State state_ = State::kInitial;
const std::unique_ptr<const PrefetchedSignedExchangeCacheEntry> exchange_;
std::vector<blink::mojom::PrefetchedSignedExchangeInfoPtr> info_list_;
mojo::Remote<network::mojom::RestrictedCookieManager> cookie_manager_;
base::WeakPtrFactory<PrefetchedNavigationLoaderInterceptor> weak_factory_{
this};
};
bool CanStoreEntry(const PrefetchedSignedExchangeCacheEntry& entry) {
const net::HttpResponseHeaders* outer_headers =
entry.outer_response()->headers.get();
// We don't store responses with a "cache-control: no-store" header.
if (outer_headers->HasHeaderValue("cache-control", "no-store"))
return false;
// Generally we don't store responses with a "vary" header. We only allows
// "accept-encoding" vary header. This is because content decoding is handled
// by the network layer and PrefetchedSignedExchangeCache stores decoded
// response bodies, so we can safely ignore varying on the "Accept-Encoding"
// header.
std::optional<std::string_view> value;
size_t iter = 0;
while ((value = outer_headers->EnumerateHeader(&iter, "vary"))) {
if (!base::EqualsCaseInsensitiveASCII(*value, "accept-encoding")) {
return false;
}
}
return true;
}
bool CanUseEntry(const PrefetchedSignedExchangeCacheEntry& entry,
const base::Time& verification_time) {
if (entry.signature_expire_time() < verification_time)
return false;
auto& outer_response = entry.outer_response();
// Use the prefetched entry within kPrefetchReuseMins minutes without
// validation.
if (outer_response->headers->GetCurrentAge(outer_response->request_time,
outer_response->response_time,
verification_time) <
base::Minutes(net::HttpCache::kPrefetchReuseMins)) {
return true;
}
// We use the prefetched entry when we don't need the validation.
if (outer_response->headers->RequiresValidation(
outer_response->request_time, outer_response->response_time,
verification_time) != net::VALIDATION_NONE) {
return false;
}
return true;
}
// Deserializes a SHA256HashValue from a string. On error, returns false.
// This method support the form of "sha256-<base64-hash-value>".
bool ExtractSHA256HashValueFromString(std::string_view value,
net::SHA256HashValue* out) {
if (!base::StartsWith(value, "sha256-"))
return false;
const std::string_view base64_str = value.substr(7);
std::string decoded;
if (!base::Base64Decode(base64_str, &decoded) ||
decoded.size() != out->size()) {
return false;
}
UNSAFE_TODO(memcpy(out->data(), decoded.data(), out->size()));
return true;
}
// Returns a map of subresource URL to SHA256HashValue which are declared in the
// rel=allowd-alt-sxg link header of |main_exchange|'s inner response.
std::map<GURL, net::SHA256HashValue> GetAllowedAltSXG(
const PrefetchedSignedExchangeCacheEntry& main_exchange) {
std::map<GURL, net::SHA256HashValue> result;
std::string link_header = main_exchange.inner_response()
->headers->GetNormalizedHeader("link")
.value_or(std::string());
if (link_header.empty())
return result;
for (const auto& value : link_header_util::SplitLinkHeader(link_header)) {
std::unordered_map<std::string, std::optional<std::string>> link_params;
std::optional<std::string> link_url =
link_header_util::ParseLinkHeaderValue(value, link_params);
if (!link_url) {
continue;
}
auto rel = link_params.find("rel");
auto header_integrity = link_params.find(kHeaderIntegrity);
net::SHA256HashValue header_integrity_value;
if (rel == link_params.end() || header_integrity == link_params.end() ||
rel->second.value_or("") != std::string(kAllowedAltSxg) ||
!ExtractSHA256HashValueFromString(
std::string_view(header_integrity->second.value_or("")),
&header_integrity_value)) {
continue;
}
result[main_exchange.inner_url().Resolve(*link_url)] =
header_integrity_value;
}
return result;
}
} // namespace
PrefetchedSignedExchangeCache::PrefetchedSignedExchangeCache() = default;
PrefetchedSignedExchangeCache::~PrefetchedSignedExchangeCache() = default;
void PrefetchedSignedExchangeCache::Store(
std::unique_ptr<const PrefetchedSignedExchangeCacheEntry> cached_exchange) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (exchanges_.size() > kMaxEntrySize)
return;
DCHECK(cached_exchange->outer_url().is_valid());
DCHECK(cached_exchange->outer_response());
DCHECK(cached_exchange->header_integrity());
DCHECK(cached_exchange->inner_url().is_valid());
DCHECK(cached_exchange->inner_response());
DCHECK(cached_exchange->completion_status());
DCHECK(cached_exchange->blob_data_handle());
DCHECK(!cached_exchange->signature_expire_time().is_null());
if (!CanStoreEntry(*cached_exchange))
return;
const GURL outer_url = cached_exchange->outer_url();
exchanges_[outer_url] = std::move(cached_exchange);
for (TestObserver& observer : test_observers_)
observer.OnStored(this, outer_url);
}
void PrefetchedSignedExchangeCache::Clear() {
exchanges_.clear();
}
std::unique_ptr<NavigationLoaderInterceptor>
PrefetchedSignedExchangeCache::MaybeCreateInterceptor(
const GURL& outer_url,
FrameTreeNodeId frame_tree_node_id,
const net::IsolationInfo& isolation_info) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
const auto it = exchanges_.find(outer_url);
if (it == exchanges_.end())
return nullptr;
const base::Time verification_time =
signed_exchange_utils::GetVerificationTime();
const std::unique_ptr<const PrefetchedSignedExchangeCacheEntry>& exchange =
it->second;
if (!CanUseEntry(*exchange.get(), verification_time)) {
exchanges_.erase(it);
return nullptr;
}
auto info_list =
GetInfoListForNavigation(*exchange, verification_time, frame_tree_node_id,
isolation_info.network_anonymization_key());
mojo::Remote<network::mojom::RestrictedCookieManager> cookie_manager;
auto* frame = FrameTreeNode::GloballyFindByID(frame_tree_node_id);
if (frame) {
StoragePartition* storage_partition =
frame->current_frame_host()->GetProcess()->GetStoragePartition();
url::Origin inner_url_origin = url::Origin::Create(exchange->inner_url());
net::IsolationInfo inner_url_isolation_info =
isolation_info.CreateForRedirect(inner_url_origin);
RenderFrameHostImpl* render_frame_host = frame->current_frame_host();
static_cast<StoragePartitionImpl*>(storage_partition)
->CreateRestrictedCookieManager(
network::mojom::RestrictedCookieManagerRole::NETWORK,
inner_url_origin, inner_url_isolation_info,
/* is_service_worker = */ false,
render_frame_host
? render_frame_host->GetProcess()->GetDeprecatedID()
: -1,
render_frame_host ? render_frame_host->GetRoutingID()
: MSG_ROUTING_NONE,
/*cookie_setting_overrides=*/
render_frame_host ? render_frame_host->GetCookieSettingOverrides()
: net::CookieSettingOverrides(),
/*devtools_cookie_setting_overrides=*/net::CookieSettingOverrides(),
cookie_manager.BindNewPipeAndPassReceiver(),
render_frame_host ? render_frame_host->CreateCookieAccessObserver(
CookieAccessDetails::Source::kNonNavigation)
: mojo::NullRemote());
}
return std::make_unique<PrefetchedNavigationLoaderInterceptor>(
exchange->Clone(), std::move(info_list), std::move(cookie_manager));
}
const PrefetchedSignedExchangeCache::EntryMap&
PrefetchedSignedExchangeCache::GetExchanges() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
return exchanges_;
}
void PrefetchedSignedExchangeCache::RecordHistograms() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (exchanges_.empty())
return;
UMA_HISTOGRAM_COUNTS_100("PrefetchedSignedExchangeCache.Count",
exchanges_.size());
}
std::vector<blink::mojom::PrefetchedSignedExchangeInfoPtr>
PrefetchedSignedExchangeCache::GetInfoListForNavigation(
const PrefetchedSignedExchangeCacheEntry& main_exchange,
const base::Time& verification_time,
FrameTreeNodeId frame_tree_node_id,
const net::NetworkAnonymizationKey& network_anonymization_key) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
const url::Origin outer_url_origin =
url::Origin::Create(main_exchange.outer_url());
const url::Origin request_initiator_origin_lock =
url::Origin::Create(main_exchange.inner_url());
const auto inner_url_header_integrity_map = GetAllowedAltSXG(main_exchange);
std::vector<blink::mojom::PrefetchedSignedExchangeInfoPtr> info_list;
EntryMap::iterator exchanges_it = exchanges_.begin();
while (exchanges_it != exchanges_.end()) {
const std::unique_ptr<const PrefetchedSignedExchangeCacheEntry>& exchange =
exchanges_it->second;
if (!CanUseEntry(*exchange.get(), verification_time)) {
exchanges_.erase(exchanges_it++);
continue;
}
auto it = inner_url_header_integrity_map.find(exchange->inner_url());
if (it == inner_url_header_integrity_map.end()) {
++exchanges_it;
continue;
}
// Restrict the main SXG and the subresources SXGs to be served from the
// same origin.
if (!outer_url_origin.IsSameOriginWith(
url::Origin::Create(exchange->outer_url()))) {
++exchanges_it;
continue;
}
if (it->second != *exchange->header_integrity()) {
++exchanges_it;
auto reporter = SignedExchangeReporter::MaybeCreate(
exchange->outer_url(), main_exchange.outer_url().spec(),
*exchange->outer_response(), network_anonymization_key,
frame_tree_node_id);
if (reporter) {
reporter->set_cert_server_ip_address(
exchange->cert_server_ip_address());
reporter->set_inner_url(exchange->inner_url());
reporter->set_cert_url(exchange->cert_url());
reporter->ReportHeaderIntegrityMismatch();
}
continue;
}
mojo::PendingRemote<network::mojom::URLLoaderFactory>
pending_loader_factory;
new SubresourceSignedExchangeURLLoaderFactory(
pending_loader_factory.InitWithNewPipeAndPassReceiver(),
exchange->Clone(), request_initiator_origin_lock);
info_list.emplace_back(blink::mojom::PrefetchedSignedExchangeInfo::New(
exchange->outer_url(), *exchange->header_integrity(),
exchange->inner_url(), exchange->inner_response().Clone(),
std::move(pending_loader_factory)));
++exchanges_it;
}
return info_list;
}
void PrefetchedSignedExchangeCache::AddObserverForTesting(
TestObserver* observer) {
test_observers_.AddObserver(observer);
}
void PrefetchedSignedExchangeCache::RemoveObserverForTesting(
const TestObserver* observer) {
test_observers_.RemoveObserver(observer);
}
} // namespace content