| // Copyright 2018 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/prefetch_url_loader.h" |
| |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "content/browser/web_package/prefetched_signed_exchange_cache.h" |
| #include "content/browser/web_package/prefetched_signed_exchange_cache_adapter.h" |
| #include "content/browser/web_package/signed_exchange_prefetch_handler.h" |
| #include "content/browser/web_package/signed_exchange_utils.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/frame_accept_header.h" |
| #include "content/public/common/content_features.h" |
| #include "net/base/load_flags.h" |
| #include "net/base/network_anonymization_key.h" |
| #include "net/http/http_request_headers.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/record_ontransfersizeupdate_utils.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/mojom/early_hints.mojom.h" |
| #include "services/network/public/mojom/fetch_api.mojom-shared.h" |
| #include "third_party/blink/public/common/features.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr char kSignedExchangeEnabledAcceptHeaderForCrossOriginPrefetch[] = |
| "application/signed-exchange;v=b3;q=0.7,*/*;q=0.8"; |
| |
| } // namespace |
| |
| PrefetchURLLoader::PrefetchURLLoader( |
| int32_t request_id, |
| uint32_t options, |
| FrameTreeNodeId frame_tree_node_id, |
| const network::ResourceRequest& resource_request, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client, |
| const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, |
| scoped_refptr<network::SharedURLLoaderFactory> network_loader_factory, |
| URLLoaderThrottlesGetter url_loader_throttles_getter, |
| BrowserContext* browser_context, |
| scoped_refptr<PrefetchedSignedExchangeCache> |
| prefetched_signed_exchange_cache, |
| const std::string& accept_langs, |
| RecursivePrefetchTokenGenerator recursive_prefetch_token_generator) |
| : frame_tree_node_id_(frame_tree_node_id), |
| resource_request_(resource_request), |
| network_anonymization_key_(network_anonymization_key), |
| network_loader_factory_(std::move(network_loader_factory)), |
| forwarding_client_(std::move(client)), |
| url_loader_throttles_getter_(url_loader_throttles_getter), |
| accept_langs_(accept_langs), |
| recursive_prefetch_token_generator_( |
| std::move(recursive_prefetch_token_generator)), |
| is_signed_exchange_handling_enabled_( |
| signed_exchange_utils::IsSignedExchangeHandlingEnabled( |
| browser_context)) { |
| DCHECK(network_loader_factory_); |
| CHECK(!resource_request.trusted_params || |
| resource_request.trusted_params->isolation_info.request_type() == |
| net::IsolationInfo::RequestType::kOther || |
| resource_request.trusted_params->isolation_info.request_type() == |
| net::IsolationInfo::RequestType::kMainFrame); |
| |
| if (is_signed_exchange_handling_enabled_) { |
| // Set the SignedExchange accept header. |
| // (https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#internet-media-type-applicationsigned-exchange). |
| |
| // TODO(crbug.com/40250488): find a solution for CORS requests, |
| // perhaps exempt the Accept header from the 128-byte rule |
| // (https://fetch.spec.whatwg.org/#cors-safelisted-request-header). For now, |
| // we use the frame Accept header for prefetches only in requests with a |
| // no-cors/same-origin mode to avoid an unintended preflight. |
| std::string accept_header = |
| resource_request_.mode == network::mojom::RequestMode::kCors |
| ? kSignedExchangeEnabledAcceptHeaderForCrossOriginPrefetch |
| : FrameAcceptHeaderValue(/*allow_sxg_responses=*/true, |
| browser_context); |
| resource_request_.headers.SetHeader(net::HttpRequestHeaders::kAccept, |
| std::move(accept_header)); |
| if (prefetched_signed_exchange_cache) { |
| prefetched_signed_exchange_cache_adapter_ = |
| std::make_unique<PrefetchedSignedExchangeCacheAdapter>( |
| std::move(prefetched_signed_exchange_cache), |
| browser_context->GetBlobStorageContext(), resource_request.url, |
| this); |
| } |
| } |
| |
| network_loader_factory_->CreateLoaderAndStart( |
| loader_.BindNewPipeAndPassReceiver(), request_id, options, |
| resource_request_, client_receiver_.BindNewPipeAndPassRemote(), |
| traffic_annotation); |
| client_receiver_.set_disconnect_handler(base::BindOnce( |
| &PrefetchURLLoader::OnNetworkConnectionError, base::Unretained(this))); |
| } |
| |
| PrefetchURLLoader::~PrefetchURLLoader() = default; |
| |
| void PrefetchURLLoader::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) { |
| DCHECK(modified_headers.IsEmpty()) |
| << "Redirect with modified headers was not supported yet. " |
| "crbug.com/845683"; |
| DCHECK(!new_url) << "Redirect with modified URL was not " |
| "supported yet. crbug.com/845683"; |
| if (signed_exchange_prefetch_handler_) { |
| // Rebind |client_receiver_| and |loader_|. |
| client_receiver_.Bind(signed_exchange_prefetch_handler_->FollowRedirect( |
| loader_.BindNewPipeAndPassReceiver())); |
| return; |
| } |
| |
| DCHECK(loader_); |
| loader_->FollowRedirect( |
| removed_headers, net::HttpRequestHeaders() /* modified_headers */, |
| net::HttpRequestHeaders() /* modified_cors_exempt_headers */, |
| std::nullopt); |
| } |
| |
| void PrefetchURLLoader::SetPriority(net::RequestPriority priority, |
| int intra_priority_value) { |
| if (loader_) { |
| loader_->SetPriority(priority, intra_priority_value); |
| } |
| } |
| |
| void PrefetchURLLoader::OnReceiveEarlyHints( |
| network::mojom::EarlyHintsPtr early_hints) { |
| forwarding_client_->OnReceiveEarlyHints(std::move(early_hints)); |
| } |
| |
| void PrefetchURLLoader::OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr response, |
| mojo::ScopedDataPipeConsumerHandle body, |
| std::optional<mojo_base::BigBuffer> cached_metadata) { |
| if (is_signed_exchange_handling_enabled_ && |
| signed_exchange_utils::ShouldHandleAsSignedHTTPExchange( |
| resource_request_.url, *response)) { |
| DCHECK(!signed_exchange_prefetch_handler_); |
| const bool keep_entry_for_prefetch_cache = |
| !!prefetched_signed_exchange_cache_adapter_; |
| |
| // Note that after this point this doesn't directly get upcalls from the |
| // network. (Until |this| calls the handler's FollowRedirect.) |
| signed_exchange_prefetch_handler_ = |
| std::make_unique<SignedExchangePrefetchHandler>( |
| frame_tree_node_id_, resource_request_, std::move(response), |
| std::move(body), loader_.Unbind(), client_receiver_.Unbind(), |
| network_loader_factory_, url_loader_throttles_getter_, this, |
| network_anonymization_key_, accept_langs_, |
| keep_entry_for_prefetch_cache); |
| return; |
| } |
| |
| // If the response is marked as a restricted cross-origin prefetch, we |
| // populate the response's |recursive_prefetch_token| member with a unique |
| // token. The renderer will propagate this token to recursive prefetches |
| // coming from this response, in the form of preload headers. This token is |
| // later used by the PrefetchURLLoaderService to recover the correct |
| // NetworkAnonymizationKey to use when fetching the request. In the Signed |
| // Exchange case, we do this after redirects from the outer response, because |
| // we redirect back here for the inner response. |
| if (resource_request_.load_flags & |
| net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME) { |
| DCHECK(!recursive_prefetch_token_generator_.is_null()); |
| base::UnguessableToken recursive_prefetch_token = |
| std::move(recursive_prefetch_token_generator_).Run(resource_request_); |
| response->recursive_prefetch_token = recursive_prefetch_token; |
| } |
| |
| // Just drop any cached metadata; we don't need to forward it to the renderer |
| // for prefetch. |
| cached_metadata.reset(); |
| |
| if (!body) { |
| forwarding_client_->OnReceiveResponse(std::move(response), |
| mojo::ScopedDataPipeConsumerHandle(), |
| std::nullopt); |
| return; |
| } |
| |
| response_ = std::move(response); |
| if (prefetched_signed_exchange_cache_adapter_ && |
| signed_exchange_prefetch_handler_) { |
| prefetched_signed_exchange_cache_adapter_->OnStartLoadingResponseBody( |
| std::move(body)); |
| return; |
| } |
| |
| // Just drain the original response's body here. |
| DCHECK(!pipe_drainer_); |
| pipe_drainer_ = |
| std::make_unique<mojo::DataPipeDrainer>(this, std::move(body)); |
| |
| SendEmptyBody(); |
| } |
| |
| void PrefetchURLLoader::OnReceiveRedirect( |
| const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr head) { |
| if (prefetched_signed_exchange_cache_adapter_ && |
| signed_exchange_prefetch_handler_) { |
| prefetched_signed_exchange_cache_adapter_->OnReceiveSignedExchange( |
| signed_exchange_prefetch_handler_ |
| ->TakePrefetchedSignedExchangeCacheEntry()); |
| } |
| |
| resource_request_.url = redirect_info.new_url; |
| resource_request_.site_for_cookies = redirect_info.new_site_for_cookies; |
| resource_request_.referrer = GURL(redirect_info.new_referrer); |
| resource_request_.referrer_policy = redirect_info.new_referrer_policy; |
| forwarding_client_->OnReceiveRedirect(redirect_info, std::move(head)); |
| } |
| |
| void PrefetchURLLoader::OnUploadProgress(int64_t current_position, |
| int64_t total_size, |
| base::OnceCallback<void()> callback) { |
| forwarding_client_->OnUploadProgress(current_position, total_size, |
| std::move(callback)); |
| } |
| |
| void PrefetchURLLoader::OnTransferSizeUpdated(int32_t transfer_size_diff) { |
| network::RecordOnTransferSizeUpdatedUMA( |
| network::OnTransferSizeUpdatedFrom::kPrefetchURLLoader); |
| forwarding_client_->OnTransferSizeUpdated(transfer_size_diff); |
| } |
| |
| void PrefetchURLLoader::OnComplete( |
| const network::URLLoaderCompletionStatus& status) { |
| if (prefetched_signed_exchange_cache_adapter_ && |
| signed_exchange_prefetch_handler_) { |
| prefetched_signed_exchange_cache_adapter_->OnComplete(status); |
| return; |
| } |
| |
| SendOnComplete(status); |
| } |
| |
| bool PrefetchURLLoader::SendEmptyBody() { |
| // Send an empty response's body. |
| mojo::ScopedDataPipeProducerHandle producer; |
| mojo::ScopedDataPipeConsumerHandle consumer; |
| if (CreateDataPipe(nullptr, producer, consumer) != MOJO_RESULT_OK) { |
| // No more resources available for creating a data pipe. Close the |
| // connection, which will in turn make this loader destroyed. |
| forwarding_client_->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES)); |
| forwarding_client_.reset(); |
| client_receiver_.reset(); |
| return false; |
| } |
| DCHECK(response_); |
| forwarding_client_->OnReceiveResponse(std::move(response_), |
| std::move(consumer), std::nullopt); |
| return true; |
| } |
| |
| void PrefetchURLLoader::SendOnComplete( |
| const network::URLLoaderCompletionStatus& completion_status) { |
| forwarding_client_->OnComplete(completion_status); |
| } |
| |
| void PrefetchURLLoader::OnNetworkConnectionError() { |
| // The network loader has an error; we should let the client know it's closed |
| // by dropping this, which will in turn make this loader destroyed. |
| forwarding_client_.reset(); |
| } |
| |
| } // namespace content |