| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/network/accept_ch_frame_interceptor.h" |
| |
| #include <algorithm> |
| |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "net/http/http_request_headers.h" |
| #include "services/network/public/cpp/client_hints.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/mojom/web_client_hints_types.mojom-shared.h" |
| #include "third_party/perfetto/include/perfetto/tracing/track.h" |
| |
| namespace network { |
| |
| namespace { |
| |
| // Parses AcceptCHFrame and removes client hints already in the headers. |
| std::vector<mojom::WebClientHintsType> ComputeAcceptCHFrameHints( |
| const std::string& accept_ch_frame, |
| const net::HttpRequestHeaders& headers) { |
| std::optional<std::vector<mojom::WebClientHintsType>> maybe_hints = |
| ParseClientHintsHeader(accept_ch_frame); |
| |
| if (!maybe_hints) { |
| return {}; |
| } |
| |
| // Only look at/add headers that aren't already present. |
| std::vector<mojom::WebClientHintsType> hints; |
| for (auto hint : maybe_hints.value()) { |
| // ResourceWidth is only for images, which won't trigger a restart. |
| if (hint == mojom::WebClientHintsType::kResourceWidth || |
| hint == mojom::WebClientHintsType::kResourceWidth_DEPRECATED) { |
| continue; |
| } |
| |
| const std::string header = GetClientHintToNameMap().at(hint); |
| if (!headers.HasHeader(header)) { |
| hints.push_back(hint); |
| } |
| } |
| |
| return hints; |
| } |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<AcceptCHFrameInterceptor> AcceptCHFrameInterceptor::MaybeCreate( |
| mojo::PendingRemote<mojom::AcceptCHFrameObserver> accept_ch_frame_observer, |
| std::optional<ResourceRequest::TrustedParams::EnabledClientHints> |
| enabled_client_hints) { |
| if (!accept_ch_frame_observer || |
| !base::FeatureList::IsEnabled(features::kAcceptCHFrame)) { |
| return nullptr; |
| } |
| return base::WrapUnique(new AcceptCHFrameInterceptor( |
| std::move(accept_ch_frame_observer), std::move(enabled_client_hints), |
| base::PassKey<AcceptCHFrameInterceptor>())); |
| } |
| |
| std::unique_ptr<AcceptCHFrameInterceptor> |
| AcceptCHFrameInterceptor::CreateForTesting( |
| mojo::PendingRemote<mojom::AcceptCHFrameObserver> accept_ch_frame_observer, |
| std::optional<ResourceRequest::TrustedParams::EnabledClientHints> |
| enabled_client_hints) { |
| return base::WrapUnique(new AcceptCHFrameInterceptor( |
| std::move(accept_ch_frame_observer), std::move(enabled_client_hints), |
| base::PassKey<AcceptCHFrameInterceptor>())); |
| } |
| |
| AcceptCHFrameInterceptor::AcceptCHFrameInterceptor( |
| mojo::PendingRemote<mojom::AcceptCHFrameObserver> accept_ch_frame_observer, |
| std::optional<ResourceRequest::TrustedParams::EnabledClientHints> |
| enabled_client_hints, |
| base::PassKey<AcceptCHFrameInterceptor>) |
| : accept_ch_frame_observer_(std::move(accept_ch_frame_observer)), |
| enabled_client_hints_(std::move(enabled_client_hints)) {} |
| |
| AcceptCHFrameInterceptor::~AcceptCHFrameInterceptor() = default; |
| |
| net::Error AcceptCHFrameInterceptor::OnConnected( |
| const GURL& url, |
| const std::string& accept_ch_frame, |
| const net::HttpRequestHeaders& headers, |
| net::CompletionOnceCallback callback) { |
| if (accept_ch_frame.empty() || !accept_ch_frame_observer_) { |
| return net::OK; |
| } |
| // Find client hints that are in the ACCEPT_CH frame that were not already |
| // included in the request |
| const auto hints = ComputeAcceptCHFrameHints(accept_ch_frame, headers); |
| base::UmaHistogramBoolean("Net.URLLoader.AcceptCH.RunObserverCall", |
| !hints.empty()); |
| if (hints.empty()) { |
| return net::OK; |
| } |
| |
| const NeedsObserverCheckReason reason = |
| NeedsObserverCheck(url::Origin::Create(url), hints); |
| base::UmaHistogramEnumeration( |
| "Net.AcceptCHFrameInterceptor.NeedsObserverCheckReason", reason); |
| if (reason == NeedsObserverCheckReason::kNotNeeded) { |
| return net::OK; |
| } |
| |
| // If there are hints in the ACCEPT_CH frame that weren't included in the |
| // original request, notify the observer. If those hints can be included, |
| // this URLLoader will be destroyed and another with the correct hints |
| // started. Otherwise, the callback to continue the network transaction will |
| // be called and the URLLoader will continue as normal. |
| auto record = [](net::CompletionOnceCallback callback, |
| base::TimeTicks call_time, perfetto::Track track, |
| int status) { |
| base::UmaHistogramMicrosecondsTimes("Net.URLLoader.AcceptCH.RoundTripTime", |
| base::TimeTicks::Now() - call_time); |
| base::UmaHistogramSparse("Net.URLLoader.AcceptCH.Status", -status); |
| TRACE_EVENT_END("loading", track, "status", status); |
| std::move(callback).Run(status); |
| }; |
| TRACE_EVENT_BEGIN("loading", "AcceptCHObserver::OnAcceptCHFrameReceived call", |
| perfetto::Track::FromPointer(this), "url", url); |
| |
| // Explanation of callback lifetime safety: |
| // The `callback` originates from a net/ layer object (e.g., |
| // HttpNetworkTransaction) and might rely on an unretained pointer to that |
| // object. The URLLoader which owns `this` manages the lifetime of `this` |
| // (and its Mojo remote) and the net/ layer object associated with the |
| // `callback`. So binding the `callback` here is safe. |
| accept_ch_frame_observer_->OnAcceptCHFrameReceived( |
| url::Origin::Create(url), hints, |
| base::BindOnce(record, std::move(callback), base::TimeTicks::Now(), |
| perfetto::Track::FromPointer(this))); |
| return net::ERR_IO_PENDING; |
| } |
| |
| AcceptCHFrameInterceptor::NeedsObserverCheckReason |
| AcceptCHFrameInterceptor::NeedsObserverCheckForTesting( |
| const url::Origin& origin, |
| const std::vector<mojom::WebClientHintsType>& hints) { |
| return NeedsObserverCheck(origin, hints); |
| } |
| |
| AcceptCHFrameInterceptor::NeedsObserverCheckReason |
| AcceptCHFrameInterceptor::NeedsObserverCheck( |
| const url::Origin& origin, |
| const std::vector<mojom::WebClientHintsType>& hints) { |
| if (!enabled_client_hints_.has_value()) { |
| return NeedsObserverCheckReason::kNoEnabledClientHints; |
| } |
| |
| // For main frames, the origin must match to use the cached hints. |
| if (enabled_client_hints_->is_outermost_main_frame && |
| !enabled_client_hints_->origin.IsSameOriginWith(origin)) { |
| return NeedsObserverCheckReason::kMainFrameOriginMismatch; |
| } |
| // For subframes, the optimization is only allowed if the feature is enabled. |
| if (!enabled_client_hints_->is_outermost_main_frame && |
| !features::kAcceptCHOffloadForSubframe.Get()) { |
| return NeedsObserverCheckReason::kSubframeFeatureDisabled; |
| } |
| |
| CHECK(base::FeatureList::IsEnabled(features::kOffloadAcceptCHFrameCheck)); |
| // The Accept-CH frame can be offloaded (i.e., handled in the network |
| // service without an IPC to the browser process) if all hints in the frame |
| // are present in either the `hints` list (enabled and allowed) or the |
| // `not_allowed_hints` list (persisted but currently disallowed). If any hint |
| // is not in either list, we must fall back to the browser process to check. |
| bool needs_observer_check = false; |
| for (const auto& h : hints) { |
| const bool is_in_hints = |
| std::ranges::contains(enabled_client_hints_->hints, h); |
| const bool is_in_not_allowed_hints = |
| features::kAcceptCHFrameOffloadNotAllowedHints.Get() && |
| std::ranges::contains(enabled_client_hints_->not_allowed_hints, h); |
| const bool is_valid_for_offload = is_in_hints || is_in_not_allowed_hints; |
| if (is_in_not_allowed_hints && !is_in_hints) { |
| base::UmaHistogramEnumeration( |
| "Net.AcceptCHFrameInterceptor.OffloadSuccessForNotAllowedHint", h); |
| } |
| if (!is_valid_for_offload) { |
| needs_observer_check = true; |
| base::UmaHistogramEnumeration( |
| "Net.AcceptCHFrameInterceptor.MismatchClientHint2", h); |
| } |
| } |
| |
| if (needs_observer_check) { |
| return NeedsObserverCheckReason::kHintNotEnabled; |
| } |
| |
| return NeedsObserverCheckReason::kNotNeeded; |
| } |
| |
| } // namespace network |