blob: 243a58e4cfba3f7f9baed0b31784f415378a7063 [file] [log] [blame]
// 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/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/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;
int64_t 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(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
const net::HttpRequestHeaders& modified_cors_exempt_headers,
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;
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) {
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