| // 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/public/cpp/content_decoding_interceptor.h" | |
| #include <string_view> | |
| #include "base/byte_size.h" | |
| #include "base/metrics/histogram_functions.h" | |
| #include "base/numerics/safe_conversions.h" | |
| #include "base/process/current_process.h" | |
| #include "base/strings/strcat.h" | |
| #include "base/task/sequenced_task_runner.h" | |
| #include "base/task/thread_pool.h" | |
| #include "base/timer/elapsed_timer.h" | |
| #include "base/types/pass_key.h" | |
| #include "mojo/public/cpp/bindings/callback_helpers.h" | |
| #include "mojo/public/cpp/bindings/receiver.h" | |
| #include "mojo/public/cpp/bindings/remote.h" | |
| #include "mojo/public/cpp/bindings/self_owned_receiver.h" | |
| #include "net/filter/filter_source_stream.h" | |
| #include "services/network/public/cpp/data_pipe_to_source_stream.h" | |
| #include "services/network/public/cpp/features.h" | |
| #include "services/network/public/cpp/loading_params.h" | |
| #include "services/network/public/cpp/source_stream_to_data_pipe.h" | |
| #include "services/network/public/mojom/early_hints.mojom.h" | |
| #include "services/network/public/mojom/network_service.mojom.h" | |
| #include "services/network/public/mojom/url_response_head.mojom.h" | |
| namespace network { | |
| namespace { | |
| // Defines suffixes for UMA histogram names based on the client type that | |
| // initiated the content decoding interception requiring a data pipe. | |
| constexpr auto kClientTypeToMetricsSuffix = | |
| base::MakeFixedFlatMap<ContentDecodingInterceptor::ClientType, | |
| std::string_view>({ | |
| {ContentDecodingInterceptor::ClientType::kTest, "Test"}, | |
| {ContentDecodingInterceptor::ClientType::kURLLoaderThrottle, | |
| "URLLoaderThrottle"}, | |
| {ContentDecodingInterceptor::ClientType::kCommitNavigation, | |
| "CommitNavigation"}, | |
| {ContentDecodingInterceptor::ClientType::kDownload, "Download"}, | |
| {ContentDecodingInterceptor::ClientType::kNavigationPreload, | |
| "NavigationPreload"}, | |
| {ContentDecodingInterceptor::ClientType::kSignedExchange, | |
| "SignedExchange"}, | |
| {ContentDecodingInterceptor::ClientType::kDevTools, "DevTools"}, | |
| }); | |
| static_assert( | |
| kClientTypeToMetricsSuffix.size() == | |
| static_cast<size_t>(ContentDecodingInterceptor::ClientType::kMaxValue) + | |
| 1, | |
| "ClientType entry missing from kClientTypeToMetricsSuffix map."); | |
| uint32_t GetRendererSideContentDecodingPipeSize() { | |
| const int feature_param_value = | |
| features::kRendererSideContentDecodingPipeSize.Get(); | |
| if (feature_param_value != 0) { | |
| return base::checked_cast<uint32_t>(feature_param_value); | |
| } | |
| return network::GetDataPipeDefaultAllocationSize( | |
| network::DataPipeAllocationSize::kLargerSizeIfPossible); | |
| } | |
| // Implements the URLLoaderClient and URLLoader interfaces to intercept a | |
| // request after receiving a response and perform content decoding. This class | |
| // acts as a middleman between the original URLLoader/URLLoaderClient pair and | |
| // the new URLLoader/URLLoaderClient pair that the caller sees after | |
| // interception. | |
| class Interceptor : public network::mojom::URLLoaderClient, | |
| public network::mojom::URLLoader { | |
| public: | |
| // Creates a new `Interceptor` and starts the interception process. The | |
| // created object is owned by `destination_url_loader_receiver`. | |
| // | |
| // The data flow is illustrated below: | |
| // Blink-side =================================================== Network-side | |
| // [Destination] [Source] | |
| // =URLLoader=======> | (remote)| ==URLLoader=======> | |
| // <=URLLoaderClient= |(remote) `this` (receiver)| <=URLLoaderClient== | |
| // <=DataPipe======== |(producer) (consumer)| <=DataPipe========= | |
| static void CreateAndStart( | |
| const std::vector<net::SourceStreamType>& types, | |
| mojo::ScopedDataPipeConsumerHandle source, | |
| mojo::ScopedDataPipeProducerHandle dest, | |
| mojo::PendingRemote<network::mojom::URLLoader> source_url_loader_remote, | |
| mojo::PendingReceiver<network::mojom::URLLoaderClient> | |
| source_url_client_receiver, | |
| mojo::PendingRemote<network::mojom::URLLoaderClient> | |
| destination_url_loader_client, | |
| mojo::PendingReceiver<network::mojom::URLLoader> | |
| destination_url_loader_receiver, | |
| scoped_refptr<base::SequencedTaskRunner> worker_task_runner) { | |
| auto interceptor = | |
| std::make_unique<Interceptor>(base::PassKey<Interceptor>()); | |
| auto* interceptor_ptr = interceptor.get(); | |
| mojo::MakeSelfOwnedReceiver(std::move(interceptor), | |
| std::move(destination_url_loader_receiver)); | |
| interceptor_ptr->Start(types, std::move(source), std::move(dest), | |
| std::move(source_url_loader_remote), | |
| std::move(source_url_client_receiver), | |
| std::move(destination_url_loader_client), | |
| std::move(worker_task_runner)); | |
| } | |
| // Private constructor. Use `CreateAndStart()` to create instances. | |
| explicit Interceptor(base::PassKey<Interceptor>) {} | |
| Interceptor(const Interceptor&) = delete; | |
| Interceptor& operator=(const Interceptor&) = delete; | |
| ~Interceptor() override = default; | |
| private: | |
| // Struct to hold the result of the decoding operation. | |
| struct DecodeResult { | |
| int net_err; | |
| base::ByteSize transferred_bytes; | |
| }; | |
| // Starts the interception and decoding process. | |
| void Start( | |
| const std::vector<net::SourceStreamType>& types, | |
| mojo::ScopedDataPipeConsumerHandle source, | |
| mojo::ScopedDataPipeProducerHandle dest, | |
| mojo::PendingRemote<network::mojom::URLLoader> source_url_loader_remote, | |
| mojo::PendingReceiver<network::mojom::URLLoaderClient> | |
| source_url_client_receiver, | |
| mojo::PendingRemote<network::mojom::URLLoaderClient> | |
| destination_url_loader_client, | |
| scoped_refptr<base::SequencedTaskRunner> worker_task_runner) { | |
| // Create `source_stream_to_data_pipe_` with FilterSourceStream to perform | |
| // content decoding. | |
| source_stream_to_data_pipe_ = std::make_unique<SourceStreamToDataPipe>( | |
| net::FilterSourceStream::CreateDecodingSourceStream( | |
| // Create `DataPipeToSourceStream` to convert from `source` data | |
| // pipe to `net::SourceStream`. | |
| std::make_unique<DataPipeToSourceStream>(std::move(source), | |
| worker_task_runner), | |
| types), | |
| std::move(dest), worker_task_runner); | |
| // Starts reading and decoding the data. The decoded data will be written | |
| // to `dest`. | |
| source_stream_to_data_pipe_->Start( | |
| base::BindOnce(&Interceptor::OnFinishDecode, base::Unretained(this))); | |
| if (source_url_loader_remote) { | |
| // For some requests (eg: NavigationPreloadRequest), we don't bind | |
| // URLLoader. | |
| source_url_loader_.Bind(std::move(source_url_loader_remote)); | |
| } | |
| source_url_client_receiver_.Bind(std::move(source_url_client_receiver)); | |
| destination_url_loader_client_.Bind( | |
| std::move(destination_url_loader_client)); | |
| } | |
| // network::mojom::URLLoaderClient implementation | |
| void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override { | |
| // `this` is created after receiving a response. So OnReceiveEarlyHints() | |
| // must not be called. | |
| NOTREACHED(); | |
| } | |
| void OnReceiveResponse( | |
| network::mojom::URLResponseHeadPtr response_head, | |
| mojo::ScopedDataPipeConsumerHandle body, | |
| std::optional<mojo_base::BigBuffer> cached_metadata) override { | |
| // `this` is created after receiving a response. So OnReceiveResponse() | |
| // must not be called. | |
| NOTREACHED(); | |
| } | |
| void OnReceiveRedirect( | |
| const net::RedirectInfo& redirect_info, | |
| network::mojom::URLResponseHeadPtr response_head) override { | |
| // `this` is created after receiving a response. So OnReceiveRedirect() | |
| // must not be called. | |
| NOTREACHED(); | |
| } | |
| void OnUploadProgress(int64_t current_position, | |
| int64_t total_size, | |
| OnUploadProgressCallback ack_callback) override { | |
| // `this` is created after receiving a response. So OnUploadProgress() must | |
| // not be called. | |
| NOTREACHED(); | |
| } | |
| void OnTransferSizeUpdated(int32_t transfer_size_diff) override { | |
| // Forward transfer size updates to the original client. | |
| destination_url_loader_client_->OnTransferSizeUpdated(transfer_size_diff); | |
| } | |
| void OnComplete(const network::URLLoaderCompletionStatus& status) override { | |
| // Store the completion status and check if decoding is also complete. | |
| completion_status_ = status; | |
| MaybeSendOnComplete(); | |
| } | |
| // network::mojom::URLLoader implementation | |
| void FollowRedirect( | |
| network::HttpRequestHeadersUpdateParams headers_update_params, | |
| const std::optional<GURL>& new_url) override { | |
| // Redirects should be handled before interception. | |
| NOTREACHED(); | |
| } | |
| void SetPriority(net::RequestPriority priority, | |
| int32_t intra_priority_value) override { | |
| // Forward priority changes to the original URLLoader. | |
| CHECK(source_url_loader_); | |
| source_url_loader_->SetPriority(priority, intra_priority_value); | |
| } | |
| // Called when the decoding process finishes. `net_err` has a result of the | |
| // decoding. | |
| void OnFinishDecode(int net_err) { | |
| decode_result_ = DecodeResult( | |
| {net_err, source_stream_to_data_pipe_->TransferredBytes()}); | |
| source_stream_to_data_pipe_.reset(); | |
| MaybeSendOnComplete(); | |
| } | |
| // Sends the OnComplete message to the original client if both the decoding | |
| // and the original request are complete. | |
| void MaybeSendOnComplete() { | |
| if (!decode_result_ || !completion_status_) { | |
| return; | |
| } | |
| // If the original request completed successfully, update the completion | |
| // status with the decoding result. | |
| if (completion_status_->error_code == net::OK) { | |
| if (decode_result_->net_err != net::OK) { | |
| completion_status_ = | |
| network::URLLoaderCompletionStatus(decode_result_->net_err); | |
| } else { | |
| completion_status_->decoded_body_length = | |
| decode_result_->transferred_bytes; | |
| } | |
| } | |
| destination_url_loader_client_->OnComplete(*completion_status_); | |
| } | |
| // Created with a FilterSourceStream which performs the content decoding. | |
| std::unique_ptr<SourceStreamToDataPipe> source_stream_to_data_pipe_; | |
| // The original URLLoader. Used for forwarding priority changes. | |
| mojo::Remote<network::mojom::URLLoader> source_url_loader_; | |
| // Receives messages from the original URLLoaderClient. | |
| mojo::Receiver<network::mojom::URLLoaderClient> source_url_client_receiver_{ | |
| this}; | |
| // Forwards messages to the original URLLoaderClient. | |
| mojo::Remote<network::mojom::URLLoaderClient> destination_url_loader_client_; | |
| // Stores the result of the decoding operation. | |
| std::optional<DecodeResult> decode_result_; | |
| // Stores the completion status received from the original URLLoaderClient. | |
| std::optional<network::URLLoaderCompletionStatus> completion_status_; | |
| }; | |
| } // namespace | |
| // static | |
| bool ContentDecodingInterceptor:: | |
| is_network_serice_runnning_in_the_current_process_ = false; | |
| // static | |
| std::optional<ContentDecodingInterceptor::DataPipePair> | |
| ContentDecodingInterceptor::CreateDataPipePair(ClientType client_type) { | |
| if (features::kRendererSideContentDecodingForceMojoFailureForTesting.Get()) { | |
| LOG(ERROR) << "Simulating Mojo data pipe creation failure."; | |
| return std::nullopt; | |
| } | |
| const MojoCreateDataPipeOptions options{ | |
| .struct_size = sizeof(MojoCreateDataPipeOptions), | |
| .flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE, | |
| .element_num_bytes = 1, | |
| .capacity_num_bytes = GetRendererSideContentDecodingPipeSize()}; | |
| mojo::ScopedDataPipeProducerHandle pipe_producer_handle; | |
| mojo::ScopedDataPipeConsumerHandle pipe_consumer_handle; | |
| base::ElapsedTimer timer; | |
| const auto mojo_result = mojo::CreateDataPipe(&options, pipe_producer_handle, | |
| pipe_consumer_handle); | |
| const bool success = mojo_result == MOJO_RESULT_OK; | |
| // Record success/failure UMA, suffixing the histogram name with the client | |
| // type that requested the pipe. | |
| base::UmaHistogramBoolean( | |
| base::StrCat({"Network.ContentDecodingInterceptor.CreateDataPipeSuccess.", | |
| kClientTypeToMetricsSuffix.at(client_type)}), | |
| success); | |
| if (success) { | |
| base::UmaHistogramMicrosecondsTimes( | |
| base::StrCat( | |
| {"Network.ContentDecodingInterceptor.CreateDataPipeSuccessTime.", | |
| kClientTypeToMetricsSuffix.at(client_type)}), | |
| timer.Elapsed()); | |
| return std::make_pair(std::move(pipe_producer_handle), | |
| std::move(pipe_consumer_handle)); | |
| } | |
| LOG(ERROR) << "Failed to create a Mojo data pipe."; | |
| // The only expected failure reason in practice is resource exhaustion. | |
| CHECK_EQ(mojo_result, MOJO_RESULT_RESOURCE_EXHAUSTED); | |
| return std::nullopt; | |
| } | |
| void ContentDecodingInterceptor::Intercept( | |
| const std::vector<net::SourceStreamType>& types, | |
| network::mojom::URLLoaderClientEndpointsPtr& endpoints, | |
| mojo::ScopedDataPipeConsumerHandle& body, | |
| DataPipePair data_pipe_pair, | |
| scoped_refptr<base::SequencedTaskRunner> worker_task_runner) { | |
| Intercept( | |
| types, std::move(data_pipe_pair), | |
| base::BindOnce( | |
| [](network::mojom::URLLoaderClientEndpointsPtr* original_endpoints, | |
| mojo::ScopedDataPipeConsumerHandle* original_body, | |
| network::mojom::URLLoaderClientEndpointsPtr& endpoints, | |
| mojo::ScopedDataPipeConsumerHandle& body) { | |
| endpoints.Swap(original_endpoints); | |
| body.swap(*original_body); | |
| }, | |
| base::Unretained(&endpoints), base::Unretained(&body)), | |
| std::move(worker_task_runner)); | |
| } | |
| void ContentDecodingInterceptor::Intercept( | |
| const std::vector<net::SourceStreamType>& types, | |
| DataPipePair data_pipe_pair, | |
| base::OnceCallback< | |
| void(network::mojom::URLLoaderClientEndpointsPtr& endpoints, | |
| mojo::ScopedDataPipeConsumerHandle& body)> swap_callback, | |
| scoped_refptr<base::SequencedTaskRunner> worker_task_runner) { | |
| CHECK(!types.empty()); | |
| CHECK(data_pipe_pair.first->is_valid()); | |
| CHECK(data_pipe_pair.second->is_valid()); | |
| mojo::ScopedDataPipeConsumerHandle pipe_consumer_handle = | |
| std::move(data_pipe_pair.second); | |
| // Create new endpoints for the intercepted URLLoader and URLLoaderClient. | |
| mojo::PendingReceiver<network::mojom::URLLoader> url_loader_receiver; | |
| mojo::PendingRemote<network::mojom::URLLoaderClient> url_loader_client; | |
| auto endpoints = network::mojom::URLLoaderClientEndpoints::New( | |
| url_loader_receiver.InitWithNewPipeAndPassRemote(), | |
| url_loader_client.InitWithNewPipeAndPassReceiver()); | |
| // Calls `swap_callback` to connect the new created endpoints to the caller | |
| // side. | |
| std::move(swap_callback).Run(endpoints, pipe_consumer_handle); | |
| Intercept(types, std::move(pipe_consumer_handle), | |
| std::move(data_pipe_pair.first), std::move(endpoints->url_loader), | |
| std::move(endpoints->url_loader_client), | |
| std::move(url_loader_receiver), std::move(url_loader_client), | |
| worker_task_runner); | |
| } | |
| // static | |
| void ContentDecodingInterceptor::Intercept( | |
| const std::vector<net::SourceStreamType>& types, | |
| mojo::ScopedDataPipeConsumerHandle source_body, | |
| mojo::ScopedDataPipeProducerHandle dest_body, | |
| mojo::PendingRemote<network::mojom::URLLoader> source_url_loader, | |
| mojo::PendingReceiver<network::mojom::URLLoaderClient> | |
| source_url_loader_client, | |
| mojo::PendingReceiver<network::mojom::URLLoader> dest_url_loader, | |
| mojo::PendingRemote<network::mojom::URLLoaderClient> dest_url_loader_client, | |
| scoped_refptr<base::SequencedTaskRunner> worker_task_runner) { | |
| CHECK(IsInContentDecodingAllowedProcess()); | |
| // Post a task to create and start the `Interceptor` on the worker thread. | |
| worker_task_runner->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&Interceptor::CreateAndStart, types, | |
| std::move(source_body), std::move(dest_body), | |
| std::move(source_url_loader), | |
| std::move(source_url_loader_client), | |
| std::move(dest_url_loader_client), | |
| std::move(dest_url_loader), worker_task_runner)); | |
| } | |
| // static | |
| void ContentDecodingInterceptor::InterceptOnNetworkService( | |
| mojom::NetworkService& network_service, | |
| const std::vector<net::SourceStreamType>& types, | |
| network::mojom::URLLoaderClientEndpointsPtr& endpoints, | |
| mojo::ScopedDataPipeConsumerHandle& body, | |
| DataPipePair data_pipe_pair) { | |
| mojo::PendingRemote<network::mojom::URLLoader> new_url_loader; | |
| mojo::PendingReceiver<network::mojom::URLLoaderClient> new_url_loader_client; | |
| network_service.InterceptUrlLoaderForBodyDecoding( | |
| types, std::move(body), std::move(data_pipe_pair.first), | |
| std::move(endpoints->url_loader), std::move(endpoints->url_loader_client), | |
| new_url_loader.InitWithNewPipeAndPassReceiver(), | |
| new_url_loader_client.InitWithNewPipeAndPassRemote()); | |
| body = std::move(data_pipe_pair.second); | |
| endpoints = network::mojom::URLLoaderClientEndpoints::New( | |
| std::move(new_url_loader), std::move(new_url_loader_client)); | |
| } | |
| // static | |
| void ContentDecodingInterceptor::DecodeOnNetworkService( | |
| mojom::NetworkService& network_service, | |
| const std::vector<net::SourceStreamType>& types, | |
| mojo::ScopedDataPipeConsumerHandle& body, | |
| ClientType client_type, | |
| base::OnceCallback<void(net::Error)> callback) { | |
| auto data_pipe_pair = CreateDataPipePair(client_type); | |
| if (!data_pipe_pair) { | |
| std::move(callback).Run(net::ERR_INSUFFICIENT_RESOURCES); | |
| return; | |
| } | |
| network_service.DecodeContentEncoding( | |
| types, std::move(body), std::move(data_pipe_pair->first), | |
| mojo::WrapCallbackWithDefaultInvokeIfNotRun( | |
| base::BindOnce([](int32_t error) { | |
| return static_cast<net::Error>(error); | |
| }).Then(std::move(callback)), | |
| net::ERR_FAILED)); | |
| body = std::move(data_pipe_pair->second); | |
| } | |
| // static | |
| void ContentDecodingInterceptor::SetIsNetworkServiceRunningInTheCurrentProcess( | |
| bool value, | |
| SetIsNetworkServiceRunningInTheCurrentProcessKey) { | |
| is_network_serice_runnning_in_the_current_process_ = value; | |
| } | |
| // static | |
| bool ContentDecodingInterceptor::IsInContentDecodingAllowedProcess() { | |
| // Allow if NetworkService runs in the current process. | |
| if (is_network_serice_runnning_in_the_current_process_) { | |
| return true; | |
| } | |
| // Otherwise, disallow only if this is the browser process. | |
| if (base::CurrentProcess::GetInstance().GetType({}) == | |
| base::CurrentProcessType::PROCESS_BROWSER) { | |
| return false; | |
| } | |
| // Allow in other process types (renderer, utility, etc.). | |
| return true; | |
| } | |
| } // namespace network |