| // 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/web_package/signed_exchange_loader.h" |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "components/web_package/web_bundle_utils.h" |
| #include "content/browser/web_package/prefetched_signed_exchange_cache_entry.h" |
| #include "content/browser/web_package/signed_exchange_cert_fetcher_factory.h" |
| #include "content/browser/web_package/signed_exchange_devtools_proxy.h" |
| #include "content/browser/web_package/signed_exchange_handler.h" |
| #include "content/browser/web_package/signed_exchange_prefetch_metric_recorder.h" |
| #include "content/browser/web_package/signed_exchange_reporter.h" |
| #include "content/browser/web_package/signed_exchange_utils.h" |
| #include "content/public/common/content_features.h" |
| #include "net/base/load_flags.h" |
| #include "net/base/net_errors.h" |
| #include "net/cert/cert_status_flags.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/data_pipe_to_source_stream.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/source_stream_to_data_pipe.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/mojom/early_hints.mojom.h" |
| #include "services/network/public/mojom/url_loader_factory.mojom.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "third_party/blink/public/common/loader/url_loader_throttle.h" |
| #include "third_party/blink/public/common/web_package/web_package_request_matcher.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr char kPrefetchLoadResultHistogram[] = |
| "SignedExchange.Prefetch.LoadResult2"; |
| |
| SignedExchangeHandlerFactory* g_signed_exchange_factory_for_testing_ = nullptr; |
| |
| } // namespace |
| |
| SignedExchangeLoader::SignedExchangeLoader( |
| const network::ResourceRequest& outer_request, |
| network::mojom::URLResponseHeadPtr outer_response_head, |
| mojo::ScopedDataPipeConsumerHandle outer_response_body, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> forwarding_client, |
| network::mojom::URLLoaderClientEndpointsPtr endpoints, |
| uint32_t url_loader_options, |
| bool should_redirect_on_failure, |
| std::unique_ptr<SignedExchangeDevToolsProxy> devtools_proxy, |
| std::unique_ptr<SignedExchangeReporter> reporter, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| URLLoaderThrottlesGetter url_loader_throttles_getter, |
| const net::NetworkIsolationKey& network_isolation_key, |
| int frame_tree_node_id, |
| scoped_refptr<SignedExchangePrefetchMetricRecorder> metric_recorder, |
| const std::string& accept_langs, |
| bool keep_entry_for_prefetch_cache) |
| : outer_request_(outer_request), |
| outer_response_head_(std::move(outer_response_head)), |
| forwarding_client_(std::move(forwarding_client)), |
| reporter_(std::move(reporter)), |
| url_loader_options_(url_loader_options), |
| should_redirect_on_failure_(should_redirect_on_failure), |
| devtools_proxy_(std::move(devtools_proxy)), |
| url_loader_factory_(std::move(url_loader_factory)), |
| url_loader_throttles_getter_(std::move(url_loader_throttles_getter)), |
| network_isolation_key_(network_isolation_key), |
| frame_tree_node_id_(frame_tree_node_id), |
| metric_recorder_(std::move(metric_recorder)), |
| accept_langs_(accept_langs) { |
| DCHECK(outer_request_.url.is_valid()); |
| |
| if (keep_entry_for_prefetch_cache) { |
| cache_entry_ = std::make_unique<PrefetchedSignedExchangeCacheEntry>(); |
| cache_entry_->SetOuterUrl(outer_request_.url); |
| cache_entry_->SetOuterResponse(outer_response_head_->Clone()); |
| } |
| |
| // |metric_recorder_| could be null in some tests. |
| if (!(outer_request_.load_flags & net::LOAD_PREFETCH) && metric_recorder_) { |
| metric_recorder_->OnSignedExchangeNonPrefetch( |
| outer_request_.url, outer_response_head_->response_time); |
| } |
| // Can't use HttpResponseHeaders::GetMimeType() because SignedExchangeHandler |
| // checks "v=" parameter. |
| outer_response_head_->headers->EnumerateHeader(nullptr, "content-type", |
| &content_type_); |
| |
| url_loader_.Bind(std::move(endpoints->url_loader)); |
| |
| // |outer_response_body| is valid, when it's a navigation request. |
| if (outer_response_body) |
| OnStartLoadingResponseBody(std::move(outer_response_body)); |
| |
| // Bind the endpoint with |this| to get the body DataPipe. |
| url_loader_client_receiver_.Bind(std::move(endpoints->url_loader_client)); |
| |
| // |client_| will be bound with a forwarding client by ConnectToClient(). |
| pending_client_receiver_ = client_.BindNewPipeAndPassReceiver(); |
| } |
| |
| SignedExchangeLoader::~SignedExchangeLoader() = default; |
| |
| void SignedExchangeLoader::OnReceiveEarlyHints( |
| network::mojom::EarlyHintsPtr early_hints) { |
| NOTREACHED(); |
| } |
| |
| void SignedExchangeLoader::OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr response_head, |
| mojo::ScopedDataPipeConsumerHandle body, |
| absl::optional<mojo_base::BigBuffer> cached_metadata) { |
| // Must not be called because this SignedExchangeLoader and the client |
| // endpoints were bound after OnReceiveResponse() is called. |
| NOTREACHED(); |
| } |
| |
| void SignedExchangeLoader::OnReceiveRedirect( |
| const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr response_head) { |
| // Must not be called because this SignedExchangeLoader and the client |
| // endpoints were bound after OnReceiveResponse() is called. |
| NOTREACHED(); |
| } |
| |
| void SignedExchangeLoader::OnUploadProgress( |
| int64_t current_position, |
| int64_t total_size, |
| OnUploadProgressCallback ack_callback) { |
| // Must not be called because this SignedExchangeLoader and the client |
| // endpoints were bound after OnReceiveResponse() is called. |
| NOTREACHED(); |
| } |
| |
| void SignedExchangeLoader::OnTransferSizeUpdated(int32_t transfer_size_diff) { |
| // TODO(https://crbug.com/803774): Implement this to progressively update the |
| // encoded data length in DevTools. |
| } |
| |
| void SignedExchangeLoader::OnStartLoadingResponseBody( |
| mojo::ScopedDataPipeConsumerHandle response_body) { |
| auto cert_fetcher_factory = SignedExchangeCertFetcherFactory::Create( |
| url_loader_factory_, url_loader_throttles_getter_, |
| outer_request_.throttling_profile_id, |
| (outer_request_.trusted_params && |
| !outer_request_.trusted_params->isolation_info.IsEmpty()) |
| ? net::IsolationInfo::IsFrameSiteEnabled() |
| ? net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kOther, |
| *outer_request_.trusted_params->isolation_info |
| .top_frame_origin(), |
| *outer_request_.trusted_params->isolation_info |
| .frame_origin(), |
| net::SiteForCookies()) |
| : net::IsolationInfo::CreateDoubleKey( |
| net::IsolationInfo::RequestType::kOther, |
| *outer_request_.trusted_params->isolation_info |
| .top_frame_origin(), |
| net::SiteForCookies()) |
| : net::IsolationInfo()); |
| |
| if (g_signed_exchange_factory_for_testing_) { |
| signed_exchange_handler_ = g_signed_exchange_factory_for_testing_->Create( |
| outer_request_.url, |
| std::make_unique<network::DataPipeToSourceStream>( |
| std::move(response_body)), |
| base::BindOnce(&SignedExchangeLoader::OnHTTPExchangeFound, |
| weak_factory_.GetWeakPtr()), |
| std::move(cert_fetcher_factory)); |
| return; |
| } |
| |
| signed_exchange_handler_ = std::make_unique<SignedExchangeHandler>( |
| network::IsUrlPotentiallyTrustworthy(outer_request_.url), |
| web_package::HasNoSniffHeader(*outer_response_head_), content_type_, |
| std::make_unique<network::DataPipeToSourceStream>( |
| std::move(response_body)), |
| base::BindOnce(&SignedExchangeLoader::OnHTTPExchangeFound, |
| weak_factory_.GetWeakPtr()), |
| std::move(cert_fetcher_factory), network_isolation_key_, |
| outer_request_.trusted_params |
| ? absl::make_optional(outer_request_.trusted_params->isolation_info) |
| : absl::nullopt, |
| outer_request_.load_flags, outer_response_head_->remote_endpoint, |
| std::make_unique<blink::WebPackageRequestMatcher>(outer_request_.headers, |
| accept_langs_), |
| std::move(devtools_proxy_), reporter_.get(), frame_tree_node_id_); |
| } |
| |
| void SignedExchangeLoader::OnComplete( |
| const network::URLLoaderCompletionStatus& status) { |
| DCHECK(!outer_response_length_info_); |
| outer_response_length_info_ = OuterResponseLengthInfo(); |
| outer_response_length_info_->encoded_data_length = status.encoded_data_length; |
| outer_response_length_info_->decoded_body_length = status.decoded_body_length; |
| NotifyClientOnCompleteIfReady(); |
| } |
| |
| void SignedExchangeLoader::FollowRedirect( |
| const std::vector<std::string>& removed_headers, |
| const net::HttpRequestHeaders& modified_headers, |
| const net::HttpRequestHeaders& modified_cors_exempt_headers, |
| const absl::optional<GURL>& new_url) { |
| NOTREACHED(); |
| } |
| |
| void SignedExchangeLoader::SetPriority(net::RequestPriority priority, |
| int intra_priority_value) { |
| url_loader_->SetPriority(priority, intra_priority_value); |
| } |
| |
| void SignedExchangeLoader::PauseReadingBodyFromNet() { |
| url_loader_->PauseReadingBodyFromNet(); |
| } |
| |
| void SignedExchangeLoader::ResumeReadingBodyFromNet() { |
| url_loader_->ResumeReadingBodyFromNet(); |
| } |
| |
| void SignedExchangeLoader::ConnectToClient( |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) { |
| DCHECK(pending_client_receiver_.is_valid()); |
| mojo::FusePipes(std::move(pending_client_receiver_), std::move(client)); |
| } |
| |
| std::unique_ptr<PrefetchedSignedExchangeCacheEntry> |
| SignedExchangeLoader::TakePrefetchedSignedExchangeCacheEntry() { |
| DCHECK(cache_entry_); |
| return std::move(cache_entry_); |
| } |
| |
| void SignedExchangeLoader::OnHTTPExchangeFound( |
| SignedExchangeLoadResult result, |
| net::Error error, |
| const GURL& request_url, |
| network::mojom::URLResponseHeadPtr resource_response, |
| std::unique_ptr<net::SourceStream> payload_stream) { |
| if (error) { |
| DCHECK_NE(result, SignedExchangeLoadResult::kSuccess); |
| ReportLoadResult(result); |
| |
| if (error != net::ERR_INVALID_SIGNED_EXCHANGE || |
| !should_redirect_on_failure_ || !request_url.is_valid()) { |
| // Let the request fail. |
| // This will eventually delete |this|. |
| forwarding_client_->OnComplete(network::URLLoaderCompletionStatus(error)); |
| return; |
| } |
| |
| // Make a fallback redirect to |request_url|. |
| DCHECK(!fallback_url_); |
| fallback_url_ = request_url; |
| forwarding_client_->OnReceiveRedirect( |
| signed_exchange_utils::CreateRedirectInfo( |
| request_url, outer_request_, *outer_response_head_, |
| true /* is_fallback_redirect */), |
| signed_exchange_utils::CreateRedirectResponseHead( |
| *outer_response_head_, true /* is_fallback_redirect */)); |
| forwarding_client_.reset(); |
| return; |
| } |
| DCHECK_EQ(result, SignedExchangeLoadResult::kSuccess); |
| inner_request_url_ = request_url; |
| |
| if (cache_entry_) { |
| cache_entry_->SetInnerUrl(*inner_request_url_); |
| auto inner_response_for_cache = resource_response->Clone(); |
| inner_response_for_cache->was_fetched_via_cache = true; |
| inner_response_for_cache->was_in_prefetch_cache = true; |
| cache_entry_->SetInnerResponse(std::move(inner_response_for_cache)); |
| const bool get_info_result = |
| signed_exchange_handler_->GetSignedExchangeInfoForPrefetchCache( |
| *cache_entry_); |
| DCHECK(get_info_result); |
| } |
| |
| forwarding_client_->OnReceiveRedirect( |
| signed_exchange_utils::CreateRedirectInfo( |
| request_url, outer_request_, *outer_response_head_, |
| false /* is_fallback_redirect */), |
| signed_exchange_utils::CreateRedirectResponseHead( |
| *outer_response_head_, false /* is_fallback_redirect */)); |
| forwarding_client_.reset(); |
| |
| const absl::optional<net::SSLInfo>& ssl_info = resource_response->ssl_info; |
| if (ssl_info.has_value() && |
| (url_loader_options_ & |
| network::mojom::kURLLoadOptionSendSSLInfoForCertificateError) && |
| net::IsCertStatusError(ssl_info->cert_status)) { |
| ssl_info_ = ssl_info; |
| } |
| |
| network::mojom::URLResponseHeadPtr inner_response_head_shown_to_client = |
| std::move(resource_response); |
| if (ssl_info.has_value() && |
| !(url_loader_options_ & |
| network::mojom::kURLLoadOptionSendSSLInfoWithResponse)) { |
| inner_response_head_shown_to_client->ssl_info = absl::nullopt; |
| } |
| inner_response_head_shown_to_client->was_fetched_via_cache = |
| outer_response_head_->was_fetched_via_cache; |
| |
| // Currently we always assume that we have body. |
| // TODO(https://crbug.com/80374): Add error handling and bail out |
| // earlier if there's an error. |
| mojo::ScopedDataPipeProducerHandle producer_handle; |
| mojo::ScopedDataPipeConsumerHandle consumer_handle; |
| MojoCreateDataPipeOptions options; |
| options.struct_size = sizeof(MojoCreateDataPipeOptions); |
| options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE; |
| options.element_num_bytes = 1; |
| options.capacity_num_bytes = |
| network::features::GetDataPipeDefaultAllocationSize(); |
| if (mojo::CreateDataPipe(&options, producer_handle, consumer_handle) != |
| MOJO_RESULT_OK) { |
| forwarding_client_->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES)); |
| return; |
| } |
| |
| client_->OnReceiveResponse(std::move(inner_response_head_shown_to_client), |
| std::move(consumer_handle), absl::nullopt); |
| |
| body_data_pipe_adapter_ = std::make_unique<network::SourceStreamToDataPipe>( |
| std::move(payload_stream), std::move(producer_handle)); |
| |
| // Start reading. |
| body_data_pipe_adapter_->Start(base::BindOnce( |
| &SignedExchangeLoader::FinishReadingBody, base::Unretained(this))); |
| } |
| |
| void SignedExchangeLoader::FinishReadingBody(int result) { |
| DCHECK(!decoded_body_read_result_); |
| decoded_body_read_result_ = result; |
| NotifyClientOnCompleteIfReady(); |
| } |
| |
| void SignedExchangeLoader::NotifyClientOnCompleteIfReady() { |
| // If |outer_response_length_info_| or |decoded_body_read_result_| is |
| // unavailable, do nothing and rely on the subsequent call to notify client. |
| if (!outer_response_length_info_ || !decoded_body_read_result_) |
| return; |
| |
| ReportLoadResult(*decoded_body_read_result_ == net::OK |
| ? SignedExchangeLoadResult::kSuccess |
| : SignedExchangeLoadResult::kMerkleIntegrityError); |
| |
| network::URLLoaderCompletionStatus status; |
| status.error_code = *decoded_body_read_result_; |
| status.completion_time = base::TimeTicks::Now(); |
| status.encoded_data_length = outer_response_length_info_->encoded_data_length; |
| status.encoded_body_length = |
| outer_response_length_info_->decoded_body_length - |
| signed_exchange_handler_->GetExchangeHeaderLength(); |
| status.decoded_body_length = body_data_pipe_adapter_->TransferredBytes(); |
| |
| if (ssl_info_) { |
| DCHECK((url_loader_options_ & |
| network::mojom::kURLLoadOptionSendSSLInfoForCertificateError) && |
| net::IsCertStatusError(ssl_info_->cert_status)); |
| status.ssl_info = *ssl_info_; |
| } |
| |
| // This will eventually delete |this|. |
| client_->OnComplete(status); |
| } |
| |
| void SignedExchangeLoader::ReportLoadResult(SignedExchangeLoadResult result) { |
| signed_exchange_utils::RecordLoadResultHistogram(result); |
| // |metric_recorder_| could be null in some tests. |
| if ((outer_request_.load_flags & net::LOAD_PREFETCH) && metric_recorder_) { |
| UMA_HISTOGRAM_ENUMERATION(kPrefetchLoadResultHistogram, result); |
| metric_recorder_->OnSignedExchangePrefetchFinished( |
| outer_request_.url, outer_response_head_->response_time); |
| } |
| |
| if (reporter_) |
| reporter_->ReportLoadResultAndFinish(result); |
| } |
| |
| void SignedExchangeLoader::SetSignedExchangeHandlerFactoryForTest( |
| SignedExchangeHandlerFactory* factory) { |
| g_signed_exchange_factory_for_testing_ = factory; |
| } |
| |
| } // namespace content |