| // 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/object_navigation_fallback_body_loader.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/containers/contains.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/renderer_host/render_frame_host_manager.h" |
| #include "content/browser/renderer_host/render_frame_proxy_host.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "mojo/public/cpp/system/data_pipe_drainer.h" |
| #include "net/http/http_response_info.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "services/network/public/mojom/early_hints.mojom.h" |
| #include "services/network/public/mojom/timing_allow_origin.mojom.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "third_party/blink/public/mojom/navigation/navigation_params.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| #include "url/url_util.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // This logic is duplicated from Performance::PassesTimingAllowCheck. Ensure |
| // that any changes are synced between both copies. |
| bool PassesTimingAllowCheck( |
| const network::mojom::URLResponseHead& response_head, |
| const GURL& url, |
| const GURL& next_url, |
| const url::Origin& parent_origin, |
| bool& response_tainting_not_basic, |
| bool& tainted_origin_flag) { |
| const url::Origin response_origin = url::Origin::Create(url); |
| const bool is_same_origin = response_origin.IsSameOriginWith(parent_origin); |
| // Still same-origin and resource tainting is "basic": just return true. |
| if (!response_tainting_not_basic && is_same_origin) { |
| return true; |
| } |
| |
| // Otherwise, a cross-origin response is currently (or has previously) been |
| // handled, so resource tainting is no longer "basic". |
| response_tainting_not_basic = true; |
| |
| const network::mojom::TimingAllowOriginPtr& tao = |
| response_head.parsed_headers->timing_allow_origin; |
| if (!tao) { |
| return false; |
| } |
| |
| if (tao->which() == network::mojom::TimingAllowOrigin::Tag::kAll) |
| return true; |
| |
| // TODO(https://crbug.com/1128402): For now, this bookkeeping only exists to |
| // stay in sync with the Blink code. |
| bool is_next_resource_same_origin = true; |
| if (url != next_url) { |
| is_next_resource_same_origin = |
| response_origin.IsSameOriginWith(url::Origin::Create(next_url)); |
| } |
| |
| if (!is_same_origin && !is_next_resource_same_origin) { |
| tainted_origin_flag = true; |
| } |
| |
| return base::Contains(tao->get_serialized_origins(), |
| parent_origin.Serialize()); |
| } |
| |
| // This logic is duplicated from Performance::AllowsTimingRedirect(). Ensure |
| // that any changes are synced between both copies. |
| // |
| // TODO(https://crbug.com/1201767): There is a *third* implementation of the TAO |
| // check in CorsURLLoader, but it exactly implements the TAO check as defined in |
| // the Fetch standard. Unfortunately, the definition in the standard always |
| // allows timing details for navigations: the response tainting is always |
| // considered "basic" for navigations, which means that timing details will |
| // always be allowed, even for cross-origin frames. Oops. |
| bool AllowTimingDetailsForParent( |
| const url::Origin& parent_origin, |
| const blink::mojom::CommonNavigationParams& common_params, |
| const blink::mojom::CommitNavigationParams& commit_params, |
| const network::mojom::URLResponseHead& response_head) { |
| bool response_tainting_not_basic = false; |
| bool tainted_origin_flag = false; |
| |
| DCHECK_EQ(commit_params.redirect_infos.size(), |
| commit_params.redirect_response.size()); |
| for (size_t i = 0; i < commit_params.redirect_infos.size(); ++i) { |
| const GURL& next_response_url = |
| i + 1 < commit_params.redirect_infos.size() |
| ? commit_params.redirect_infos[i + 1].new_url |
| : common_params.url; |
| if (!PassesTimingAllowCheck( |
| *commit_params.redirect_response[i], |
| commit_params.redirect_infos[i].new_url, next_response_url, |
| parent_origin, response_tainting_not_basic, tainted_origin_flag)) { |
| return false; |
| } |
| } |
| |
| return PassesTimingAllowCheck( |
| response_head, common_params.url, common_params.url, parent_origin, |
| response_tainting_not_basic, tainted_origin_flag); |
| } |
| |
| // This logic is duplicated from Performance::GenerateResourceTiming(). Ensure |
| // that any changes are synced between both copies. |
| blink::mojom::ResourceTimingInfoPtr GenerateResourceTiming( |
| const url::Origin& parent_origin, |
| const blink::mojom::CommonNavigationParams& common_params, |
| const blink::mojom::CommitNavigationParams& commit_params, |
| const network::mojom::URLResponseHead& response_head) { |
| // TODO(dcheng): There should be a Blink helper for populating the timing info |
| // that's exposed in //third_party/blink/common. This would allow a lot of the |
| // boilerplate to be shared. |
| |
| auto timing_info = blink::mojom::ResourceTimingInfo::New(); |
| const GURL& initial_url = !commit_params.original_url.is_empty() |
| ? commit_params.original_url |
| : common_params.url; |
| timing_info->name = initial_url.spec(); |
| timing_info->start_time = common_params.navigation_start; |
| timing_info->alpn_negotiated_protocol = |
| response_head.alpn_negotiated_protocol; |
| timing_info->connection_info = net::HttpResponseInfo::ConnectionInfoToString( |
| response_head.connection_info); |
| |
| // If there's no received headers end time, don't set load timing. This is the |
| // case for non-HTTP requests, requests that don't go over the wire, and |
| // certain error cases. |
| // TODO(dcheng): Is it actually possible to hit this path if |
| // `response_head.headers` is populated? |
| if (!response_head.load_timing.receive_headers_end.is_null()) { |
| timing_info->timing = response_head.load_timing; |
| } |
| // `response_end` will be populated after loading the body. |
| timing_info->context_type = blink::mojom::RequestContextType::OBJECT; |
| |
| timing_info->allow_timing_details = AllowTimingDetailsForParent( |
| parent_origin, common_params, commit_params, response_head); |
| |
| DCHECK_EQ(commit_params.redirect_infos.size(), |
| commit_params.redirect_response.size()); |
| |
| if (!commit_params.redirect_infos.empty()) { |
| timing_info->allow_redirect_details = timing_info->allow_timing_details; |
| timing_info->last_redirect_end_time = |
| commit_params.redirect_response.back()->load_timing.receive_headers_end; |
| } else { |
| timing_info->allow_redirect_details = false; |
| timing_info->last_redirect_end_time = base::TimeTicks(); |
| } |
| // The final value for `encoded_body_size` and `decoded_body_size` will be |
| // populated after loading the body. |
| timing_info->did_reuse_connection = response_head.load_timing.socket_reused; |
| // Use url::Origin to handle cases like blob:https://. |
| timing_info->is_secure_transport = base::Contains( |
| url::GetSecureSchemes(), url::Origin::Create(common_params.url).scheme()); |
| timing_info->allow_negative_values = false; |
| return timing_info; |
| } |
| |
| std::string ExtractServerTimingValueIfNeeded( |
| const network::mojom::URLResponseHead& response_head) { |
| std::string value; |
| if (!response_head.timing_allow_passed) |
| return value; |
| |
| // Note: the renderer will be responsible for parsing the actual server |
| // timing values. |
| response_head.headers->GetNormalizedHeader("Server-Timing", &value); |
| return value; |
| } |
| |
| } // namespace |
| |
| NAVIGATION_HANDLE_USER_DATA_KEY_IMPL(ObjectNavigationFallbackBodyLoader); |
| |
| // static |
| void ObjectNavigationFallbackBodyLoader::CreateAndStart( |
| NavigationRequest& navigation_request, |
| const blink::mojom::CommonNavigationParams& common_params, |
| const blink::mojom::CommitNavigationParams& commit_params, |
| const network::mojom::URLResponseHead& response_head, |
| mojo::ScopedDataPipeConsumerHandle response_body, |
| network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints, |
| base::OnceClosure completion_closure) { |
| // This should only be called for HTTP errors. |
| DCHECK(response_head.headers); |
| RenderFrameHostImpl* render_frame_host = |
| navigation_request.frame_tree_node()->current_frame_host(); |
| // A frame owned by <object> should always have a parent. |
| DCHECK(render_frame_host->GetParent()); |
| // It's safe to snapshot the parent origin in the calculation here; if the |
| // parent frame navigates, `render_frame_host_` will be deleted, which |
| // triggers deletion of `this`, cancelling all remaining work. |
| blink::mojom::ResourceTimingInfoPtr timing_info = GenerateResourceTiming( |
| render_frame_host->GetParent()->GetLastCommittedOrigin(), common_params, |
| commit_params, response_head); |
| std::string server_timing_value = |
| ExtractServerTimingValueIfNeeded(response_head); |
| |
| CreateForNavigationHandle( |
| navigation_request, std::move(timing_info), |
| std::move(server_timing_value), std::move(response_body), |
| std::move(url_loader_client_endpoints), std::move(completion_closure)); |
| } |
| |
| ObjectNavigationFallbackBodyLoader::~ObjectNavigationFallbackBodyLoader() {} |
| |
| ObjectNavigationFallbackBodyLoader::ObjectNavigationFallbackBodyLoader( |
| NavigationHandle& navigation_handle, |
| blink::mojom::ResourceTimingInfoPtr timing_info, |
| std::string server_timing_value, |
| mojo::ScopedDataPipeConsumerHandle response_body, |
| network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints, |
| base::OnceClosure completion_closure) |
| : navigation_request_(static_cast<NavigationRequest&>(navigation_handle)), |
| url_loader_(std::move(url_loader_client_endpoints->url_loader)), |
| url_loader_client_receiver_( |
| this, |
| std::move(url_loader_client_endpoints->url_loader_client)), |
| response_body_drainer_( |
| std::make_unique<mojo::DataPipeDrainer>(this, |
| std::move(response_body))), |
| timing_info_(std::move(timing_info)), |
| server_timing_value_(std::move(server_timing_value)), |
| completion_closure_(std::move(completion_closure)) { |
| // Unretained is safe; `url_loader_` is owned by `this` and will not dispatch |
| // callbacks after it is destroyed. |
| url_loader_client_receiver_.set_disconnect_handler( |
| base::BindOnce(&ObjectNavigationFallbackBodyLoader::BodyLoadFailed, |
| base::Unretained(this))); |
| } |
| |
| void ObjectNavigationFallbackBodyLoader::MaybeComplete() { |
| // Completion requires receiving the completion status from the `URLLoader`, |
| // as well as the response body being completely drained. |
| if (!status_ || response_body_drainer_) |
| return; |
| |
| // At this point, `this` is done and the associated NavigationRequest and |
| // `this` must be cleaned up, no matter what else happens. Running |
| // `completion_closure_` will delete the NavigationRequest, which will delete |
| // `this`. |
| base::ScopedClosureRunner cleanup(std::move(completion_closure_)); |
| |
| timing_info_->response_end = status_->completion_time; |
| timing_info_->encoded_body_size = status_->encoded_body_length; |
| timing_info_->decoded_body_size = status_->decoded_body_length; |
| |
| RenderFrameHostManager* render_manager = |
| navigation_request_.frame_tree_node()->render_manager(); |
| if (RenderFrameProxyHost* proxy = render_manager->GetProxyToParent()) { |
| if (proxy->is_render_frame_proxy_live()) { |
| proxy->GetAssociatedRemoteFrame() |
| ->RenderFallbackContentWithResourceTiming(std::move(timing_info_), |
| server_timing_value_); |
| } |
| } else { |
| render_manager->current_frame_host() |
| ->GetAssociatedLocalFrame() |
| ->RenderFallbackContentWithResourceTiming(std::move(timing_info_), |
| server_timing_value_); |
| } |
| } |
| |
| void ObjectNavigationFallbackBodyLoader::BodyLoadFailed() { |
| // At this point, `this` is done and the associated NavigationRequest and |
| // `this` must be cleaned up, no matter what else happens. Running |
| // `completion_closure_` will delete the NavigationRequest, which will delete |
| // `this`. |
| base::ScopedClosureRunner cleanup(std::move(completion_closure_)); |
| |
| // The endpoint for the URL loader client was closed before the body load |
| // completed. This is considered failure, so trigger the fallback content, but |
| // without any timing info, since it can't be calculated. |
| navigation_request_.RenderFallbackContentForObjectTag(); |
| } |
| |
| void ObjectNavigationFallbackBodyLoader::OnReceiveEarlyHints( |
| network::mojom::EarlyHintsPtr) { |
| // Should have already happened. |
| NOTREACHED(); |
| } |
| |
| void ObjectNavigationFallbackBodyLoader::OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr, |
| mojo::ScopedDataPipeConsumerHandle body, |
| absl::optional<mojo_base::BigBuffer> cached_metadata) { |
| // Should have already happened. |
| NOTREACHED(); |
| } |
| |
| void ObjectNavigationFallbackBodyLoader::OnReceiveRedirect( |
| const net::RedirectInfo&, |
| network::mojom::URLResponseHeadPtr) { |
| // Should have already happened. |
| NOTREACHED(); |
| } |
| |
| void ObjectNavigationFallbackBodyLoader::OnUploadProgress( |
| int64_t current_position, |
| int64_t total_size, |
| OnUploadProgressCallback) { |
| // Should have already happened. |
| NOTREACHED(); |
| } |
| |
| void ObjectNavigationFallbackBodyLoader::OnTransferSizeUpdated( |
| int32_t transfer_size_diff) { |
| // Not needed so implementation omitted. |
| } |
| |
| void ObjectNavigationFallbackBodyLoader::OnComplete( |
| const network::URLLoaderCompletionStatus& status) { |
| status_ = status; |
| MaybeComplete(); |
| } |
| |
| void ObjectNavigationFallbackBodyLoader::OnDataAvailable(const void* data, |
| size_t num_bytes) {} |
| |
| void ObjectNavigationFallbackBodyLoader::OnDataComplete() { |
| response_body_drainer_.reset(); |
| MaybeComplete(); |
| } |
| |
| } // namespace content |