| // Copyright 2022 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/browsing_topics/browsing_topics_url_loader.h" |
| |
| #include "base/functional/bind.h" |
| #include "content/browser/browsing_topics/header_util.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/page.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.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/mojom/early_hints.mojom.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/frame/frame_policy.h" |
| #include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| bool GetTopicsHeaderValueForSubresourceRequest( |
| WeakDocumentPtr request_initiator_document, |
| const GURL& url, |
| const network::ResourceRequest& request, |
| std::string& header_value) { |
| DCHECK(header_value.empty()); |
| |
| // Due to the race between the subresource requests and navigations, this |
| // request may arrive before the commit confirmation is received (i.e. |
| // NavigationRequest::DidCommitNavigation()), or after the document is |
| // destroyed. We consider those cases to be ineligible for topics. |
| // |
| // TODO(yaoxia): measure how often this happens. |
| RenderFrameHost* request_initiator_frame = |
| request_initiator_document.AsRenderFrameHostIfValid(); |
| if (!request_initiator_frame) { |
| return false; |
| } |
| |
| // Fenced frames disallow most permissions policies which would let this |
| // function return false regardless, but adding this check to be more |
| // explicit. |
| if (request_initiator_frame->IsNestedWithinFencedFrame()) { |
| return false; |
| } |
| |
| if (!request_initiator_frame->GetPage().IsPrimary()) { |
| return false; |
| } |
| |
| // TODO(crbug.com/1244137): IsPrimary() doesn't actually detect portals yet. |
| // Remove this when it does. |
| if (!static_cast<RenderFrameHostImpl*>( |
| request_initiator_frame->GetMainFrame()) |
| ->IsOutermostMainFrame()) { |
| return false; |
| } |
| |
| url::Origin origin = url::Origin::Create(url); |
| if (origin.opaque()) { |
| return false; |
| } |
| |
| // TODO(yaoxia): should this be `ReportBadMessage`? On the renderer side, the |
| // fetch initiator context must be secure. Does it imply that the requested |
| // `origin` is always potentially trustworthy? |
| if (!network::IsOriginPotentiallyTrustworthy(origin)) { |
| return false; |
| } |
| |
| const blink::PermissionsPolicy* permissions_policy = |
| static_cast<RenderFrameHostImpl*>(request_initiator_frame) |
| ->permissions_policy(); |
| |
| if (!permissions_policy->IsFeatureEnabledForSubresourceRequest( |
| blink::mojom::PermissionsPolicyFeature::kBrowsingTopics, origin, |
| request) || |
| !permissions_policy->IsFeatureEnabledForSubresourceRequest( |
| blink::mojom::PermissionsPolicyFeature:: |
| kBrowsingTopicsBackwardCompatible, |
| origin, request)) { |
| return false; |
| } |
| |
| std::vector<blink::mojom::EpochTopicPtr> topics; |
| bool topics_eligible = GetContentClient()->browser()->HandleTopicsWebApi( |
| origin, request_initiator_frame->GetMainFrame(), |
| browsing_topics::ApiCallerSource::kFetch, |
| /*get_topics=*/true, |
| /*observe=*/false, topics); |
| |
| if (topics_eligible) { |
| header_value = DeriveTopicsHeaderValue(topics); |
| } |
| |
| return topics_eligible; |
| } |
| |
| void ProcessTopicsEligibleResponse( |
| const network::mojom::URLResponseHeadPtr& head, |
| WeakDocumentPtr document, |
| const GURL& url) { |
| RenderFrameHost* rfh = document.AsRenderFrameHostIfValid(); |
| if (!rfh) { |
| return; |
| } |
| |
| HandleTopicsEligibleResponse(head->parsed_headers, url::Origin::Create(url), |
| *rfh, browsing_topics::ApiCallerSource::kFetch); |
| } |
| |
| } // namespace |
| |
| BrowsingTopicsURLLoader::BrowsingTopicsURLLoader( |
| WeakDocumentPtr document, |
| int32_t request_id, |
| uint32_t options, |
| const network::ResourceRequest& resource_request, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client, |
| const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, |
| scoped_refptr<network::SharedURLLoaderFactory> network_loader_factory) |
| : document_(std::move(document)), |
| url_(resource_request.url), |
| request_(resource_request), |
| forwarding_client_(std::move(client)) { |
| DCHECK(network_loader_factory); |
| |
| std::string header_value; |
| topics_eligible_ = GetTopicsHeaderValueForSubresourceRequest( |
| document_, request_.url, request_, header_value); |
| |
| if (topics_eligible_) { |
| request_.headers.SetHeader(kBrowsingTopicsRequestHeaderKey, header_value); |
| } |
| |
| network_loader_factory->CreateLoaderAndStart( |
| loader_.BindNewPipeAndPassReceiver(), request_id, options, request_, |
| client_receiver_.BindNewPipeAndPassRemote(), traffic_annotation); |
| |
| client_receiver_.set_disconnect_handler( |
| base::BindOnce(&BrowsingTopicsURLLoader::OnNetworkConnectionError, |
| base::Unretained(this))); |
| } |
| |
| BrowsingTopicsURLLoader::~BrowsingTopicsURLLoader() = default; |
| |
| void BrowsingTopicsURLLoader::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) { |
| if (new_url) { |
| url_ = new_url.value(); |
| } |
| |
| std::vector<std::string> new_removed_headers = removed_headers; |
| net::HttpRequestHeaders new_modified_headers = modified_headers; |
| |
| new_removed_headers.push_back(kBrowsingTopicsRequestHeaderKey); |
| |
| std::string header_value; |
| topics_eligible_ = GetTopicsHeaderValueForSubresourceRequest( |
| document_, url_, request_, header_value); |
| |
| if (topics_eligible_) { |
| new_modified_headers.SetHeader(kBrowsingTopicsRequestHeaderKey, |
| header_value); |
| } |
| |
| loader_->FollowRedirect(new_removed_headers, new_modified_headers, |
| modified_cors_exempt_headers, new_url); |
| } |
| |
| void BrowsingTopicsURLLoader::SetPriority(net::RequestPriority priority, |
| int intra_priority_value) { |
| loader_->SetPriority(priority, intra_priority_value); |
| } |
| |
| void BrowsingTopicsURLLoader::PauseReadingBodyFromNet() { |
| loader_->PauseReadingBodyFromNet(); |
| } |
| |
| void BrowsingTopicsURLLoader::ResumeReadingBodyFromNet() { |
| loader_->ResumeReadingBodyFromNet(); |
| } |
| |
| void BrowsingTopicsURLLoader::OnReceiveEarlyHints( |
| network::mojom::EarlyHintsPtr early_hints) { |
| forwarding_client_->OnReceiveEarlyHints(std::move(early_hints)); |
| } |
| |
| void BrowsingTopicsURLLoader::OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr head, |
| mojo::ScopedDataPipeConsumerHandle body, |
| absl::optional<mojo_base::BigBuffer> cached_metadata) { |
| if (topics_eligible_) { |
| ProcessTopicsEligibleResponse(head, document_, url_); |
| topics_eligible_ = false; |
| } |
| |
| forwarding_client_->OnReceiveResponse(std::move(head), std::move(body), |
| std::move(cached_metadata)); |
| } |
| |
| void BrowsingTopicsURLLoader::OnReceiveRedirect( |
| const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr head) { |
| if (topics_eligible_) { |
| ProcessTopicsEligibleResponse(head, document_, url_); |
| topics_eligible_ = false; |
| } |
| |
| url_ = redirect_info.new_url; |
| |
| forwarding_client_->OnReceiveRedirect(redirect_info, std::move(head)); |
| } |
| |
| void BrowsingTopicsURLLoader::OnUploadProgress( |
| int64_t current_position, |
| int64_t total_size, |
| base::OnceCallback<void()> callback) { |
| forwarding_client_->OnUploadProgress(current_position, total_size, |
| std::move(callback)); |
| } |
| |
| void BrowsingTopicsURLLoader::OnTransferSizeUpdated( |
| int32_t transfer_size_diff) { |
| forwarding_client_->OnTransferSizeUpdated(transfer_size_diff); |
| } |
| |
| void BrowsingTopicsURLLoader::OnComplete( |
| const network::URLLoaderCompletionStatus& status) { |
| forwarding_client_->OnComplete(status); |
| } |
| |
| void BrowsingTopicsURLLoader::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 |