chromium/chromeos/ash/components/drivefs/drivefs_http_client.cc

// 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 "chromeos/ash/components/drivefs/drivefs_http_client.h"

#include <cstdint>
#include <memory>
#include <type_traits>
#include <utility>

#include "base/containers/enum_set.h"
#include "base/functional/bind.h"
#include "base/unguessable_token.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/data_pipe_getter.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace drivefs {
namespace {

constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("drivefs_http_client", R"(
      semantics {
          sender: "Files App - Google Drive"
          description: "Files App integrates with Google Drive to provide a "
          "local view of what is available on the Google Drive Web interface. "
          "This allows users to navigate Google Drive as if it was a local "
          "file system."
          trigger: "User navigates through the Google Drive directory in the "
          "Files App. User opens a file in the Google Drive directory in the "
          "Files App. Additionally, the Files App will sync Google Drive data "
          "in the background as changes are made on the web or on other "
          "devices."
          data: "All metadata related to files stored in Google Drive as well "
          "as content of files stored in Google Drive."
          destination: GOOGLE_OWNED_SERVICE
          }
        policy {
          cookies_allowed: NO
          chrome_policy {
            DriveDisabled: {
                DriveDisabled: true
            }
          }
        }
        comments: "There are two policies that control this integration. "
        "DriveDisabled will disable all communications while "
        "DriveDisabledOverCellular will disable communication over cellular "
        "networks"
    )");

class DriveFsURLLoaderClient : public network::mojom::URLLoaderClient,
                               public network::mojom::DataPipeGetter {
 public:
  DriveFsURLLoaderClient(
      mojo::PendingRemote<mojom::HttpDelegate> http_delegate_remote,
      const mojom::HttpRequestPtr& request,
      mojo::PendingReceiver<network::mojom::DataPipeGetter> data_pipe_receiver,
      mojo::PendingRemote<network::mojom::URLLoader> loader_remote)
      : request_body_bytes_(request->request_body_bytes),
        loader_remote_(std::move(loader_remote)),
        http_delegate_remote_(std::move(http_delegate_remote)) {
    Clone(std::move(data_pipe_receiver));
    http_delegate_remote_.set_disconnect_handler(
        base::BindOnce(&DriveFsURLLoaderClient::OnHttpDelegateDisconnect,
                       weak_ptr_factory_.GetWeakPtr()));
  }

 private:
  enum class CallbackState : size_t {
    kBodyRequested,
    kResponseReceived,
    kRequestComplete,
    // Add new states above.
    kMin = kBodyRequested,
    kMax = kRequestComplete,
  };

  bool IsFirstCall(CallbackState state) {
    if (callback_state_.Has(state)) {
      return false;
    }
    callback_state_.Put(state);
    return true;
  }

  void OnHttpDelegateDisconnect() {
    // Cancel the request: The DriveFS side disconnected.
    loader_remote_.reset();
  }

  // URLLoaderClient Impl
  void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override {
  }

  void OnReceiveResponse(
      network::mojom::URLResponseHeadPtr response_head,
      mojo::ScopedDataPipeConsumerHandle body,
      std::optional<mojo_base::BigBuffer> cached_metadata) override {
    DCHECK(IsFirstCall(CallbackState::kResponseReceived));
    std::vector<mojom::HttpHeaderPtr> headers;
    size_t iter = 0;
    std::string name;
    std::string value;
    while (response_head->headers->EnumerateHeaderLines(&iter, &name, &value)) {
      headers.push_back(mojom::HttpHeader::New(name, value));
    }
    http_delegate_remote_->OnReceiveResponse(mojom::HttpResponse::New(
        response_head->headers->response_code(), std::move(headers)));
    if (body) {
      http_delegate_remote_->OnReceiveBody(std::move(body));
    }
  }

  void OnReceiveRedirect(
      const net::RedirectInfo& redirect_info,
      network::mojom::URLResponseHeadPtr response_head) override {
    // Cancel the request: Redirects are not permitted for security reasons.
    loader_remote_.reset();
  }

  void OnUploadProgress(int64_t current_position,
                        int64_t total_size,
                        OnUploadProgressCallback ack_callback) override {
    std::move(ack_callback).Run();
  }

  void OnTransferSizeUpdated(int32_t transfer_size_diff) override {
    network::RecordOnTransferSizeUpdatedUMA(
        network::OnTransferSizeUpdatedFrom::kDriveFsURLLoaderClient);
  }

  void OnComplete(const network::URLLoaderCompletionStatus& status) override {
    DCHECK(IsFirstCall(CallbackState::kRequestComplete));
    http_delegate_remote_->OnRequestComplete(mojom::HttpCompletionStatus::New(
        static_cast<mojom::NetError>(status.error_code),
        status.decoded_body_length));
  }

  // DataPipeGetter Impl
  void Read(mojo::ScopedDataPipeProducerHandle pipe,
            ReadCallback callback) override {
    DCHECK(request_body_bytes_);
    DCHECK(IsFirstCall(CallbackState::kBodyRequested));
    std::move(callback).Run(net::OK, request_body_bytes_);
    http_delegate_remote_->GetRequestBody(std::move(pipe));
  }

  void Clone(
      mojo::PendingReceiver<network::mojom::DataPipeGetter> receiver) override {
    data_pipe_receivers_.Add(this, std::move(receiver));
  }

  const int64_t request_body_bytes_;
  base::EnumSet<CallbackState, CallbackState::kMin, CallbackState::kMax>
      callback_state_;
  mojo::ReceiverSet<network::mojom::DataPipeGetter> data_pipe_receivers_;
  mojo::Remote<network::mojom::URLLoader> loader_remote_;
  mojo::Remote<mojom::HttpDelegate> http_delegate_remote_;
  base::WeakPtrFactory<DriveFsURLLoaderClient> weak_ptr_factory_{this};
};

}  // namespace

DriveFsHttpClient::DriveFsHttpClient(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
    : throttling_profile_id_(base::UnguessableToken::Create()),
      url_loader_factory_(std::move(url_loader_factory)) {}

DriveFsHttpClient::~DriveFsHttpClient() = default;

void DriveFsHttpClient::ExecuteHttpRequest(
    mojom::HttpRequestPtr request,
    mojo::PendingRemote<mojom::HttpDelegate> delegate) {
  // Build a `URLLoaderClient` for the request. This client will bridge
  // communication between DriveFS and Chrome OS.
  mojo::PendingRemote<network::mojom::URLLoaderClient> url_loader_client;
  mojo::PendingRemote<network::mojom::DataPipeGetter> data_pipe_getter;
  mojo::PendingRemote<network::mojom::URLLoader> url_loader;
  mojo::PendingReceiver<network::mojom::URLLoader> url_loader_reciever =
      url_loader.InitWithNewPipeAndPassReceiver();
  mojo::ReceiverId client_id =
      clients_.Add(std::make_unique<DriveFsURLLoaderClient>(
                       std::move(delegate), request,
                       data_pipe_getter.InitWithNewPipeAndPassReceiver(),
                       std::move(url_loader)),
                   url_loader_client.InitWithNewPipeAndPassReceiver());
  // Translate the `HttpRequest` from DriveFS into a `network::ResourceRequest`.
  network::ResourceRequest resource_request;
  resource_request.url = GURL(request->url);
  resource_request.method = request->method;
  for (const auto& header : request->headers) {
    resource_request.headers.SetHeader(header->key, header->value);
  }
  // TODO(b/284789869): The Chrome network service currently automatically
  // appends a `If-None-Match` header to requests, this causes a 503 error on
  // the Drive API. For now, don't cache anything until that 503 has been fixed.
  resource_request.headers.SetHeader("Cache-Control", "no-cache");
  if (request->request_body_bytes > 0) {
    resource_request.request_body = new network::ResourceRequestBody();
    resource_request.request_body->AppendDataPipe(std::move(data_pipe_getter));
  }
  resource_request.throttling_profile_id = throttling_profile_id_;
  // Start execution, the `DriveFsURLLoaderClient` will remove itself from the
  // `clients_` map on completion.
  url_loader_factory_->CreateLoaderAndStart(
      std::move(url_loader_reciever), /*request_id=*/client_id,
      /*options=*/network::mojom::kURLLoadOptionBlockAllCookies,
      std::move(resource_request), std::move(url_loader_client),
      net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation));
}

}  // namespace drivefs