// 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();
  }
#if BUILDFLAG(ARKWEB_RESOURCE_INTERCEPTION)
    // URLLoaderClient implementation for shared memory data transfer
    void OnTransferDataWithSharedMemory(base::ReadOnlySharedMemoryRegion region,
                                       uint64_t buffer_size) override {
      // Forward the shared memory data to the destination client
      if (destination_url_loader_client_) {
        destination_url_loader_client_->OnTransferDataWithSharedMemory(
            std::move(region), buffer_size);
      }
    }
#endif  // BUILDFLAG(ARKWEB_RESOURCE_INTERCEPTION)
  // 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