chromium/chrome/browser/policy/messaging_layer/upload/file_upload_impl.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/policy/messaging_layer/upload/file_upload_impl.h"

#include <string>
#include <string_view>
#include <vector>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/task/bind_post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/thread_annotations.h"
#include "base/types/expected.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/device_identity/device_oauth2_token_service.h"
#include "chrome/browser/device_identity/device_oauth2_token_service_factory.h"
#include "chrome/browser/policy/chrome_browser_policy_connector.h"
#include "chrome/browser/policy/messaging_layer/upload/file_upload_job.h"
#include "components/reporting/resources/resource_manager.h"
#include "components/reporting/util/reporting_errors.h"
#include "components/reporting/util/status.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "google_apis/gaia/core_account_id.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_oauth_client.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "google_apis/gaia/oauth2_access_token_manager.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "url/gurl.h"

namespace reporting {

constexpr char kAuthorizationPrefix[] = "Bearer ";

constexpr char kUploadStatusHeader[] = "X-Goog-Upload-Status";
constexpr char kUploadCommandHeader[] = "X-Goog-Upload-Command";
constexpr char kUploadHeaderContentLengthHeader[] =
    "X-Goog-Upload-Header-Content-Length";
constexpr char kUploadHeaderContentTypeHeader[] =
    "X-Goog-Upload-Header-Content-Type";
constexpr char kUploadChunkGranularityHeader[] =
    "X-Goog-Upload-Chunk-Granularity";
constexpr char kUploadUrlHeader[] = "X-Goog-Upload-Url";
constexpr char kUploadSizeReceivedHeader[] = "X-Goog-Upload-Size-Received";
constexpr char kUploadOffsetHeader[] = "X-Goog-Upload-Offset";
constexpr char kUploadProtocolHeader[] = "X-Goog-Upload-Protocol";
constexpr char kUploadIdHeader[] = "X-GUploader-UploadID";

// Helper for network response, headers analysis and status retrieval.
StatusOr<std::string> CheckResponseAndGetStatus(
    const std::unique_ptr<::network::SimpleURLLoader> url_loader,
    const scoped_refptr<::net::HttpResponseHeaders> headers) {
  if (!headers) {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::NO_HEADERS_FOUND,
                                  DataLossErrorReason::MAX_VALUE);
    return base::unexpected(
        Status(error::DATA_LOSS,
               base::StrCat({"Network error=",
                             ::net::ErrorToString(url_loader->NetError())})));
  }

  if (headers->response_code() == net::HTTP_OK) {
    // Successful upload, retrieve and return upload status.
    std::string upload_status;
    if (!headers->GetNormalizedHeader(kUploadStatusHeader, &upload_status)) {
      base::UmaHistogramEnumeration(
          reporting::kUmaDataLossErrorReason,
          DataLossErrorReason::UNEXPECTED_UPLOAD_STATUS,
          DataLossErrorReason::MAX_VALUE);
      return base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"Unexpected upload status=", upload_status})));
    }
    return upload_status;
  } else if (headers->response_code() == net::HTTP_UNAUTHORIZED) {
    return base::unexpected(
        Status(error::UNAUTHENTICATED, "Authentication error"));
  } else {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::POST_REQUEST_FAILED,
                                  DataLossErrorReason::MAX_VALUE);
    return base::unexpected(
        Status(error::DATA_LOSS,
               base::StrCat({"POST request failed with HTTP status code ",
                             base::NumberToString(headers->response_code())})));
  }
}

// Helper to learn upload chunk granularity.
StatusOr<int64_t> GetChunkGranularity(
    const scoped_refptr<::net::HttpResponseHeaders> headers) {
  int64_t upload_granularity = -1;
  std::string upload_granularity_string;
  if (!headers->GetNormalizedHeader(kUploadChunkGranularityHeader,
                                    &upload_granularity_string)) {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::NO_GRANULARITTY_RETURNED,
                                  DataLossErrorReason::MAX_VALUE);
    return base::unexpected(
        Status(error::DATA_LOSS, "No granularity returned"));
  }
  if (!base::StringToInt64(upload_granularity_string, &upload_granularity) ||
      upload_granularity <= 0L) {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::UNEXPECTED_GRANULARITY,
                                  DataLossErrorReason::MAX_VALUE);
    return base::unexpected(Status(
        error::DATA_LOSS,
        base::StrCat({"Unexpected granularity=", upload_granularity_string})));
  }
  return upload_granularity;
}

// Generic context that returns result and self-destructs.
template <typename R>
class ActionContext {
 public:
  ActionContext(const ActionContext& other) = delete;
  ActionContext& operator=(const ActionContext& other) = delete;
  virtual ~ActionContext() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    CHECK(!result_cb_) << "Destruct before callback";
  }

 protected:
  // Constructor is only available to derived classes.
  ActionContext(base::WeakPtr<FileUploadDelegate> delegate,
                base::OnceCallback<void(R)> result_cb)
      : delegate_(std::move(delegate)), result_cb_(std::move(result_cb)) {}

  // Completes work returning result or status, and then self-destructs.
  // This is the only way `ActionContext` ceases to exist, so any asynchronous
  // callback in its subclasses is safe to use `base::Unretained(this)` and
  // does not need weak pointers.
  void Complete(R result) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    CHECK(result_cb_) << "Already completed";
    std::move(result_cb_).Run(std::move(result));
    delete this;
  }

  // Accessor.
  base::WeakPtr<FileUploadDelegate> delegate() const { return delegate_; }

  SEQUENCE_CHECKER(sequence_checker_);

 private:
  const base::WeakPtr<FileUploadDelegate> delegate_;
  base::OnceCallback<void(R)> result_cb_ GUARDED_BY_CONTEXT(sequence_checker_);
};

// Self-destructing context for Authentication.
class FileUploadDelegate::AccessTokenRetriever
    : public ActionContext<StatusOr<std::string>>,
      public OAuth2AccessTokenManager::Consumer {
 public:
  AccessTokenRetriever(
      base::WeakPtr<FileUploadDelegate> delegate,
      base::OnceCallback<void(StatusOr<std::string>)> result_cb)
      : ActionContext(std::move(delegate), std::move(result_cb)),
        OAuth2AccessTokenManager::Consumer("cros_upload_job") {}

  void RequestAccessToken() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!delegate()) {
      Complete(base::unexpected(
          Status(error::UNAVAILABLE, "Delegate is unavailable")));
      base::UmaHistogramEnumeration(
          reporting::kUmaUnavailableErrorReason,
          UnavailableErrorReason::FILE_UPLOAD_DELEGATE_IS_NULL,
          UnavailableErrorReason::MAX_VALUE);
      return;
    }

    CHECK(!access_token_request_);
    DVLOG(1) << "Requesting access token.";

    access_token_request_ = delegate()->StartOAuth2Request(this);
  }

 private:
  // OAuth2AccessTokenManager::Consumer:
  void OnGetTokenSuccess(
      const OAuth2AccessTokenManager::Request* request,
      const OAuth2AccessTokenConsumer::TokenResponse& token_response) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    CHECK_EQ(access_token_request_.get(), request);
    access_token_request_.reset();
    DVLOG(1) << "Token successfully acquired.";
    Complete(token_response.access_token);
  }

  void OnGetTokenFailure(const OAuth2AccessTokenManager::Request* request,
                         const GoogleServiceAuthError& error) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    CHECK_EQ(access_token_request_.get(), request);
    access_token_request_.reset();
    LOG(ERROR) << "Token request failed: " << error.ToString();
    Complete(
        base::unexpected(Status(error::UNAUTHENTICATED, error.ToString())));
  }

  // The OAuth request to receive the access token.
  std::unique_ptr<OAuth2AccessTokenManager::Request> access_token_request_
      GUARDED_BY_CONTEXT(sequence_checker_);
};

// Self-destructing context for FileUploadJob initiation.
class FileUploadDelegate::InitContext
    : public ActionContext<StatusOr<
          std::pair<int64_t /*total*/, std::string /*session_token*/>>> {
 public:
  InitContext(
      std::string_view origin_path,
      std::string_view upload_parameters,
      std::string_view access_token,
      base::WeakPtr<FileUploadDelegate> delegate,
      base::OnceCallback<
          void(StatusOr<std::pair<int64_t /*total*/,
                                  std::string /*session_token*/>>)> result_cb)
      : ActionContext(std::move(delegate), std::move(result_cb)),
        origin_path_(origin_path),
        upload_parameters_(upload_parameters),
        access_token_(access_token) {}

  void Run() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!delegate()) {
      Complete(base::unexpected(
          Status(error::UNAVAILABLE, "Delegate is unavailable")));
      base::UmaHistogramEnumeration(
          reporting::kUmaUnavailableErrorReason,
          UnavailableErrorReason::FILE_UPLOAD_DELEGATE_IS_NULL,
          UnavailableErrorReason::MAX_VALUE);
      return;
    }

    // Perform file operation on a thread pool, then resume on the current task
    // runner.
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
        base::BindOnce(&InitContext::InitFile, origin_path_),
        base::BindOnce(&InitContext::FileOpened, base::Unretained(this)));
  }

  void FileOpened(StatusOr<int64_t> total_result) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!delegate()) {
      Complete(base::unexpected(
          Status(error::UNAVAILABLE, "Delegate is unavailable")));
      base::UmaHistogramEnumeration(
          reporting::kUmaUnavailableErrorReason,
          UnavailableErrorReason::FILE_UPLOAD_DELEGATE_IS_NULL,
          UnavailableErrorReason::MAX_VALUE);
      return;
    }

    if (!total_result.has_value()) {
      Complete(base::unexpected(total_result.error()));
      return;
    }

    // Record total size of the file.
    total_ = total_result.value();

    // Initiate upload.
    DVLOG(1) << "Starting URL fetcher.";

    auto resource_request = std::make_unique<::network::ResourceRequest>();
    resource_request->headers.SetHeader(
        ::net::HttpRequestHeaders::kAuthorization,
        base::StrCat({kAuthorizationPrefix, access_token_.c_str()}));
    resource_request->headers.SetHeader(kUploadCommandHeader, "start");
    resource_request->headers.SetHeader(kUploadHeaderContentLengthHeader,
                                        base::NumberToString(total_));
    resource_request->headers.SetHeader(kUploadHeaderContentTypeHeader,
                                        "application/octet-stream");

    url_loader_ = delegate()->CreatePostLoader(std::move(resource_request));

    // Construct and attach medatata - see
    // go/scotty-http-protocols#unified-resumable-protocol
    // Here we expect `upload_parameters` to be in the form like:
    //
    // "<File-Type>\r\n"
    // "  support_file\r\n"
    // "</File-Type>\r\n"
    // "<Command-ID>\r\n"
    // "  ID12345\r\n"
    // "</Command-ID>\r\n"
    // "<Filename>\r\n"
    // "  resulting_file_name\r\n"
    // "</Filename>\r\n"
    // "text/xml"
    //
    // with the last line indicating content type.
    const auto pos = upload_parameters_.find_last_of("\n");
    if (pos == std::string::npos || pos + 1u >= upload_parameters_.size()) {
      Complete(base::unexpected(
          Status(error::INVALID_ARGUMENT,
                 base::StrCat({"Cannot parse upload_parameters=`",
                               upload_parameters_, "`"}))));
      return;
    }
    const std::string metadata_contents_type =
        upload_parameters_.substr(pos + 1u);
    const std::string metadata =
        upload_parameters_.substr(0, pos + 1u);  // With \n included!
    url_loader_->AttachStringForUpload(metadata, metadata_contents_type);

    // Make a call and get response headers.
    delegate()->SendAndGetResponse(
        url_loader_.get(), base::BindOnce(&InitContext::OnInitURLLoadComplete,
                                          base::Unretained(this)));
  }

  void OnInitURLLoadComplete(
      scoped_refptr<::net::HttpResponseHeaders> headers) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    auto status_result =
        CheckResponseAndGetStatus(std::move(url_loader_), headers);
    if (!status_result.has_value()) {
      Complete(base::unexpected(std::move(status_result).error()));
      return;
    }

    const std::string upload_status = status_result.value();
    if (!base::EqualsCaseInsensitiveASCII(upload_status, "active")) {
      base::UmaHistogramEnumeration(
          reporting::kUmaDataLossErrorReason,
          DataLossErrorReason::UNEXPECTED_UPLOAD_STATUS,
          DataLossErrorReason::MAX_VALUE);
      Complete(base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"Unexpected upload status=", upload_status}))));
      return;
    }

    // Just make sure granulatiy is returned, do not use it here.
    auto upload_granularity_result = GetChunkGranularity(headers);
    if (!upload_granularity_result.has_value()) {
      Complete(base::unexpected(std::move(upload_granularity_result).error()));
      return;
    }

    std::string upload_url;
    if (!headers->GetNormalizedHeader(kUploadUrlHeader, &upload_url)) {
      base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                    DataLossErrorReason::NO_UPLOAD_URL_RETURNED,
                                    DataLossErrorReason::MAX_VALUE);
      Complete(
          base::unexpected(Status(error::DATA_LOSS, "No upload URL returned")));
      return;
    }

    Complete(
        std::make_pair(total_, base::StrCat({origin_path_, "\n", upload_url})));
  }

  static StatusOr<int64_t> InitFile(const std::string origin_path) {
    auto handle = std::make_unique<base::File>(
        base::FilePath(origin_path),
        base::File::FLAG_OPEN | base::File::FLAG_READ);
    if (!handle->IsValid()) {
      base::UmaHistogramEnumeration(
          reporting::kUmaDataLossErrorReason,
          DataLossErrorReason::FAILED_TO_OPEN_UPLOAD_FILE,
          DataLossErrorReason::MAX_VALUE);
      return base::unexpected(Status(
          error::DATA_LOSS,
          base::StrCat({"Cannot open file=", origin_path, " error=",
                        base::File::ErrorToString(handle->error_details())})));
    }

    // Calculate total size of the file.
    return handle->GetLength();
  }

 private:
  const std::string origin_path_;
  const std::string upload_parameters_;
  const std::string access_token_;

  // Helper to upload the data.
  std::unique_ptr<::network::SimpleURLLoader> url_loader_
      GUARDED_BY_CONTEXT(sequence_checker_);

  // Total size.
  int64_t total_ GUARDED_BY_CONTEXT(sequence_checker_) = 0L;
};

// Self-destructing context for FileUploadJob next step.
class FileUploadDelegate::NextStepContext
    : public ActionContext<StatusOr<
          std::pair<int64_t /*uploaded*/, std::string /*session_token*/>>> {
 public:
  NextStepContext(
      int64_t total,
      int64_t uploaded,
      std::string_view session_token,
      ScopedReservation scoped_reservation,
      base::WeakPtr<FileUploadDelegate> delegate,
      base::OnceCallback<
          void(StatusOr<std::pair<int64_t /*uploaded*/,
                                  std::string /*session_token*/>>)> result_cb)
      : ActionContext(std::move(delegate), std::move(result_cb)),
        total_(total),
        uploaded_(uploaded),
        session_token_(session_token),
        scoped_reservation_(std::move(scoped_reservation)) {}

  void Run() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!delegate()) {
      Complete(base::unexpected(
          Status(error::UNAVAILABLE, "Delegate is unavailable")));
      base::UmaHistogramEnumeration(
          reporting::kUmaUnavailableErrorReason,
          UnavailableErrorReason::FILE_UPLOAD_DELEGATE_IS_NULL,
          UnavailableErrorReason::MAX_VALUE);
      return;
    }

    // Parse session token.
    const auto tokens = base::SplitStringPiece(
        session_token_, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
    if (tokens.size() != 2 || tokens[0].empty() || tokens[1].empty()) {
      Complete(base::unexpected(Status(
          error::DATA_LOSS,
          base::StrCat({"Corrupt session token `", session_token_, "`"}))));
      return;
    }
    origin_path_ = tokens[0];
    resumable_upload_url_ = GURL(tokens[1]);
    if (!resumable_upload_url_.is_valid()) {
      Complete(base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"Corrupt resumable upload URL=", tokens[1]}))));
      return;
    }

    // Query upload.
    DVLOG(1) << "Starting Query URL fetcher.";
    auto resource_request = std::make_unique<::network::ResourceRequest>();
    resource_request->url = resumable_upload_url_;
    resource_request->headers.SetHeader(kUploadCommandHeader, "query");

    url_loader_ = delegate()->CreatePostLoader(std::move(resource_request));

    // Make a call and get response headers.
    delegate()->SendAndGetResponse(
        url_loader_.get(),
        base::BindOnce(&NextStepContext::OnQueryURLLoadComplete,
                       base::Unretained(this)));
  }

  void OnQueryURLLoadComplete(
      scoped_refptr<::net::HttpResponseHeaders> headers) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!delegate()) {
      Complete(base::unexpected(
          Status(error::UNAVAILABLE, "Delegate is unavailable")));
      base::UmaHistogramEnumeration(
          reporting::kUmaUnavailableErrorReason,
          UnavailableErrorReason::FILE_UPLOAD_DELEGATE_IS_NULL,
          UnavailableErrorReason::MAX_VALUE);
      return;
    }

    auto status_result =
        CheckResponseAndGetStatus(std::move(url_loader_), headers);
    if (!status_result.has_value()) {
      Complete(base::unexpected(std::move(status_result).error()));
      return;
    }

    const std::string upload_status = status_result.value();
    if (base::EqualsCaseInsensitiveASCII(upload_status, "final")) {
      // Already done.
      Complete(std::make_pair(total_, session_token_));
      return;
    }
    if (!base::EqualsCaseInsensitiveASCII(upload_status, "active")) {
      Complete(base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"Unexpected upload status=", upload_status}))));
      return;
    }

    int64_t upload_received = -1;
    {
      std::string upload_received_string;
      if (!headers->GetNormalizedHeader(kUploadSizeReceivedHeader,
                                        &upload_received_string)) {
        Complete(base::unexpected(
            Status(error::DATA_LOSS, "No upload size returned")));
        return;
      }
      if (!base::StringToInt64(upload_received_string, &upload_received) ||
          upload_received < 0 || uploaded_ > upload_received) {
        Complete(base::unexpected(Status(
            error::DATA_LOSS,
            base::StrCat({"Unexpected received=", upload_received_string,
                          ", expected=", base::NumberToString(uploaded_)}))));
        return;
      }
    }
    if (upload_received >= total_) {
      // Already done.
      Complete(std::make_pair(total_, session_token_));
      return;
    }

    auto upload_granularity_result = GetChunkGranularity(headers);
    if (!upload_granularity_result.has_value()) {
      Complete(base::unexpected(std::move(upload_granularity_result).error()));
      return;
    }
    auto upload_granularity = upload_granularity_result.value();

    // Determine maximum buffer size, rounded down to upload_granularity.
    DCHECK_CALLED_ON_VALID_SEQUENCE(delegate()->sequence_checker_);
    int64_t max_size =
        (delegate()->max_upload_buffer_size_ / upload_granularity) *
        upload_granularity;

    // Upload next or last chunk.
    DVLOG(1) << "Starting Upload URL fetcher.";
    auto resource_request = std::make_unique<::network::ResourceRequest>();
    resource_request->url = resumable_upload_url_;
    int64_t size = total_ - upload_received;
    if (size < max_size) {
      resource_request->headers.SetHeader(kUploadCommandHeader,
                                          "upload, finalize");
    } else {
      size = max_size;
      resource_request->headers.SetHeader(kUploadCommandHeader, "upload");
    }
    resource_request->headers.SetHeader(kUploadOffsetHeader,
                                        base::NumberToString(upload_received));

    // See whether we have enough memory for the buffer.
    ScopedReservation buffer_reservation(size, scoped_reservation_);
    if (!buffer_reservation.reserved()) {
      // Do not post error status - it would stop the whole job.
      // Post success without making any progress!
      Complete(std::make_pair(upload_received, session_token_));
      return;
    }
    // Attach the new reservation.
    scoped_reservation_.HandOver(buffer_reservation);

    // Retrieve data from the file to be attached on a thread pool, then resume
    // on the current task runner. Note: it could be done with
    // `AttachFileForUpload` instead, but loading into memory allows to check
    // integrity of the file (TBD; for now we only verify file access and
    // size).
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
        base::BindOnce(&NextStepContext::LoadFileData,
                       std::string(origin_path_), total_, upload_received,
                       size),
        base::BindOnce(&NextStepContext::PerformUpload, base::Unretained(this),
                       upload_received, size, std::move(resource_request)));
  }

  void PerformUpload(
      int64_t upload_received,
      int64_t size,
      std::unique_ptr<::network::ResourceRequest> resource_request,
      StatusOr<std::string> buffer_result) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!delegate()) {
      Complete(base::unexpected(
          Status(error::UNAVAILABLE, "Delegate is unavailable")));
      base::UmaHistogramEnumeration(
          reporting::kUmaUnavailableErrorReason,
          UnavailableErrorReason::FILE_UPLOAD_DELEGATE_IS_NULL,
          UnavailableErrorReason::MAX_VALUE);
      return;
    }

    if (!buffer_result.has_value()) {
      Complete(base::unexpected(std::move(buffer_result).error()));
      return;
    }

    url_loader_ = delegate()->CreatePostLoader(std::move(resource_request));
    url_loader_->AttachStringForUpload(
        buffer_result.value(),  // owned by caller!
        "application/octet-stream");

    // Make a call and get response headers.
    delegate()->SendAndGetResponse(
        url_loader_.get(),
        base::BindOnce(&NextStepContext::OnUploadURLLoadComplete,
                       base::Unretained(this), upload_received, size));
  }

  void OnUploadURLLoadComplete(
      int64_t uploaded,
      int64_t size,
      scoped_refptr<::net::HttpResponseHeaders> headers) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    // Buffer no longer used.
    scoped_reservation_.Reduce(size);

    auto status_result =
        CheckResponseAndGetStatus(std::move(url_loader_), headers);
    if (!status_result.has_value()) {
      Complete(base::unexpected(status_result.error()));
      return;
    }

    const std::string upload_status = status_result.value();
    if (base::EqualsCaseInsensitiveASCII(upload_status, "final")) {
      // Already done.
      Complete(std::make_pair(total_, session_token_));
      return;
    }
    if (!base::EqualsCaseInsensitiveASCII(upload_status, "active")) {
      Complete(base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"Unexpected upload status=", upload_status}))));
      return;
    }

    Complete(std::make_pair(uploaded + size, session_token_));
  }

  static StatusOr<std::string> LoadFileData(const std::string origin_path,
                                            int64_t total,
                                            int64_t offset,
                                            int64_t size) {
    // Retrieve data from the file to be attached. Note: it could be done with
    // `AttachFileForUpload` instead, but loading into memory allows to check
    // integrity of the file (TBD; for now we only verify file access and
    // size).
    std::string buffer;

    auto handle = std::make_unique<base::File>(
        base::FilePath(origin_path),
        base::File::FLAG_OPEN | base::File::FLAG_READ);
    if (!handle->IsValid()) {
      base::UmaHistogramEnumeration(
          reporting::kUmaDataLossErrorReason,
          DataLossErrorReason::FAILED_TO_OPEN_UPLOAD_FILE,
          DataLossErrorReason::MAX_VALUE);
      return base::unexpected(Status(
          error::DATA_LOSS,
          base::StrCat({"Cannot open file=", origin_path, " error=",
                        base::File::ErrorToString(handle->error_details())})));
    }

    // Verify total size of the file.
    if (total != handle->GetLength()) {
      base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                    DataLossErrorReason::FILE_SIZE_MISMATCH,
                                    DataLossErrorReason::MAX_VALUE);
      return base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"File=", origin_path, " changed size ", " from ",
                               base::NumberToString(total), " to ",
                               base::NumberToString(handle->GetLength())})));
    }

    // Load into buffer.
    buffer.resize(
        size);  // Initialization is redundant, but std::string mandates it.
    const int read_size = handle->Read(offset, buffer.data(), size);
    if (read_size < 0) {
      base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                    DataLossErrorReason::CANNOT_READ_FILE,
                                    DataLossErrorReason::MAX_VALUE);
      return base::unexpected(Status(
          error::DATA_LOSS,
          base::StrCat({"Cannot read file=", origin_path, " error=",
                        base::File::ErrorToString(handle->error_details())})));
    }
    if (read_size != size) {
      base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                    DataLossErrorReason::CANNOT_READ_FILE,
                                    DataLossErrorReason::MAX_VALUE);
      return base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"Failed to read file=", origin_path,
                               " offset=", base::NumberToString(offset),
                               " size=", base::NumberToString(size),
                               " read=", base::NumberToString(read_size)})));
    }
    return buffer;
  }

 private:
  const int64_t total_;
  const int64_t uploaded_;
  const std::string session_token_;

  // Session token components.
  std::string_view origin_path_ GUARDED_BY_CONTEXT(sequence_checker_);
  GURL resumable_upload_url_ GUARDED_BY_CONTEXT(sequence_checker_);

  // Helper to upload the data.
  std::unique_ptr<network::SimpleURLLoader> url_loader_
      GUARDED_BY_CONTEXT(sequence_checker_);

  // Memory usage by upload.
  ScopedReservation scoped_reservation_ GUARDED_BY_CONTEXT(sequence_checker_);
};

// Self-destructing context for FileUploadJob finalization.
class FileUploadDelegate::FinalContext
    : public ActionContext<StatusOr<std::string /*access_parameters*/>> {
 public:
  FinalContext(
      std::string_view session_token,
      base::WeakPtr<FileUploadDelegate> delegate,
      base::OnceCallback<void(StatusOr<std::string /*access_parameters*/>)>
          result_cb)
      : ActionContext(std::move(delegate), std::move(result_cb)),
        session_token_(session_token) {}

  void Run() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!delegate()) {
      Complete(base::unexpected(
          Status(error::UNAVAILABLE, "Delegate is unavailable")));
      base::UmaHistogramEnumeration(
          reporting::kUmaUnavailableErrorReason,
          UnavailableErrorReason::FILE_UPLOAD_DELEGATE_IS_NULL,
          UnavailableErrorReason::MAX_VALUE);
      return;
    }

    // Parse session token.
    const auto tokens = base::SplitStringPiece(
        session_token_, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
    if (tokens.size() != 2 || tokens[0].empty() || tokens[1].empty()) {
      base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                    DataLossErrorReason::CORRUPT_SESSION_TOKEN,
                                    DataLossErrorReason::MAX_VALUE);
      Complete(base::unexpected(Status(
          error::DATA_LOSS,
          base::StrCat({"Corrupt session token `", session_token_, "`"}))));
      return;
    }
    origin_path_ = tokens[0];
    resumable_upload_url_ = GURL(tokens[1]);
    if (!resumable_upload_url_.is_valid()) {
      base::UmaHistogramEnumeration(
          reporting::kUmaDataLossErrorReason,
          DataLossErrorReason::CORRUPT_RESUMABLE_UPLOAD_URL,
          DataLossErrorReason::MAX_VALUE);
      Complete(base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"Corrupt resumable upload URL=", tokens[1]}))));
      return;
    }

    // Query upload.
    DVLOG(1) << "Starting Query URL fetcher.";
    auto resource_request = std::make_unique<::network::ResourceRequest>();
    resource_request->url = resumable_upload_url_;
    resource_request->headers.SetHeader(kUploadCommandHeader, "query");

    url_loader_ = delegate()->CreatePostLoader(std::move(resource_request));

    // Make a call and get response headers.
    delegate()->SendAndGetResponse(
        url_loader_.get(), base::BindOnce(&FinalContext::OnQueryURLLoadComplete,
                                          base::Unretained(this)));
  }

  void OnQueryURLLoadComplete(
      scoped_refptr<::net::HttpResponseHeaders> headers) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!delegate()) {
      Complete(base::unexpected(
          Status(error::UNAVAILABLE, "Delegate is unavailable")));
      base::UmaHistogramEnumeration(
          reporting::kUmaUnavailableErrorReason,
          UnavailableErrorReason::FILE_UPLOAD_DELEGATE_IS_NULL,
          UnavailableErrorReason::MAX_VALUE);
      return;
    }

    auto status_result =
        CheckResponseAndGetStatus(std::move(url_loader_), headers);
    if (!status_result.has_value()) {
      Complete(base::unexpected(std::move(status_result).error()));
      return;
    }

    const std::string& upload_status = status_result.value();
    if (base::EqualsCaseInsensitiveASCII(upload_status, "final")) {
      // All done.
      RespondOnFinal(headers);
      return;
    }
    if (!base::EqualsCaseInsensitiveASCII(upload_status, "active")) {
      Complete(base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"Unexpected upload status=", upload_status}))));
      base::UmaHistogramEnumeration(
          reporting::kUmaDataLossErrorReason,
          DataLossErrorReason::UNEXPECTED_UPLOAD_STATUS,
          DataLossErrorReason::MAX_VALUE);
      return;
    }

    int64_t upload_received = -1;
    {
      std::string upload_received_string;
      if (!headers->GetNormalizedHeader(kUploadSizeReceivedHeader,
                                        &upload_received_string)) {
        Complete(base::unexpected(
            Status(error::DATA_LOSS, "No upload size returned")));
        base::UmaHistogramEnumeration(
            reporting::kUmaDataLossErrorReason,
            DataLossErrorReason::NO_UPLOAD_SIZE_RETURNED,
            DataLossErrorReason::MAX_VALUE);
        return;
      }
      if (!base::StringToInt64(upload_received_string, &upload_received) ||
          upload_received < 0) {
        Complete(base::unexpected(Status(
            error::DATA_LOSS,
            base::StrCat({"Unexpected received=", upload_received_string}))));
        base::UmaHistogramEnumeration(
            reporting::kUmaDataLossErrorReason,
            DataLossErrorReason::UNEXPECTED_UPLOAD_RECEIVED_CODE,
            DataLossErrorReason::MAX_VALUE);
        return;
      }
    }

    // Finalize upload.
    DVLOG(1) << "Starting Upload URL fetcher.";
    auto resource_request = std::make_unique<::network::ResourceRequest>();
    resource_request->url = resumable_upload_url_;
    resource_request->headers.SetHeader(kUploadCommandHeader, "finalize");

    url_loader_ = delegate()->CreatePostLoader(std::move(resource_request));

    // Make a call and get response headers.
    delegate()->SendAndGetResponse(
        url_loader_.get(),
        base::BindOnce(&FinalContext::OnFinalizeURLLoadComplete,
                       base::Unretained(this)));
  }

  void OnFinalizeURLLoadComplete(
      scoped_refptr<::net::HttpResponseHeaders> headers) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    auto status_result =
        CheckResponseAndGetStatus(std::move(url_loader_), headers);
    if (!status_result.has_value()) {
      Complete(base::unexpected(std::move(status_result).error()));
      return;
    }

    const std::string upload_status = status_result.value();
    if (!base::EqualsCaseInsensitiveASCII(upload_status, "final")) {
      Complete(base::unexpected(
          Status(error::DATA_LOSS,
                 base::StrCat({"Unexpected upload status=", upload_status}))));
      base::UmaHistogramEnumeration(
          reporting::kUmaDataLossErrorReason,
          DataLossErrorReason::UNEXPECTED_UPLOAD_STATUS,
          DataLossErrorReason::MAX_VALUE);
      return;
    }

    RespondOnFinal(headers);
  }

 private:
  void RespondOnFinal(scoped_refptr<::net::HttpResponseHeaders> headers) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    std::string upload_id;
    if (!headers->GetNormalizedHeader(kUploadIdHeader, &upload_id) ||
        upload_id.empty()) {
      Complete(
          base::unexpected(Status(error::DATA_LOSS, "No upload ID returned")));
      base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                    DataLossErrorReason::NO_UPLOAD_ID_RETURNED,
                                    DataLossErrorReason::MAX_VALUE);
      return;
    }

    Complete(base::StrCat({"Upload_id=", upload_id}));
  }

  const std::string session_token_;

  // Session token components.
  std::string_view origin_path_ GUARDED_BY_CONTEXT(sequence_checker_);
  GURL resumable_upload_url_ GUARDED_BY_CONTEXT(sequence_checker_);

  // Helper to upload the data.
  std::unique_ptr<network::SimpleURLLoader> url_loader_
      GUARDED_BY_CONTEXT(sequence_checker_);
};

FileUploadDelegate::FileUploadDelegate() {
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

FileUploadDelegate::~FileUploadDelegate() {
  DCHECK_CURRENTLY_ON(::content::BrowserThread::UI);
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void FileUploadDelegate::InitializeOnce() {
  DCHECK_CURRENTLY_ON(::content::BrowserThread::UI);
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (url_loader_factory_) {
    return;  // Already initialized.
  }

  upload_url_ = GURL(g_browser_process->browser_policy_connector()
                         ->GetFileStorageServerUploadUrl());
  CHECK(upload_url_.is_valid());

  account_id_ = DeviceOAuth2TokenServiceFactory::Get()->GetRobotAccountId();
  access_token_manager_ =
      DeviceOAuth2TokenServiceFactory::Get()->GetAccessTokenManager();
  CHECK(access_token_manager_);
  url_loader_factory_ = g_browser_process->shared_url_loader_factory();
  CHECK(url_loader_factory_);
  traffic_annotation_ = std::make_unique<::net::NetworkTrafficAnnotationTag>(
      ::net::DefineNetworkTrafficAnnotation("chrome_support_tool_file_upload",
                                            R"(
        semantics {
          sender: "ChromeOS Support Tool"
          description:
              "ChromeOS Support Tool can request log files upload on a managed "
              "device on behalf of the admin. The log files are bundled "
              "together in a single zip archive that is uploaded using "
              "a multi-chunk resumable protocol. They are stored on Google "
              "servers, so the admin can view the logs in the Admin Console."
          trigger: "When UPLOAD_LOG event is posted by Support Tool "
                   "on the admin's request."
          data: "Zipped archive of log files created by Support Tool and "
                "placed in /var/spool/."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
              email: "[email protected]"
            }
            contacts {
              email: "[email protected]"
            }
          }
          user_data {
            type: USER_CONTENT
          }
          last_reviewed: "2023-03-16"
        }
        policy {
          cookies_allowed: NO
          setting: "This feature cannot be disabled in settings."
          chrome_device_policy {
            # LogUploadEnabled
            device_log_upload_settings {
              system_log_upload_enabled: false
            }
          }
        }
      )"));

  max_upload_buffer_size_ = kMaxUploadBufferSize;
}

std::unique_ptr<OAuth2AccessTokenManager::Request>
FileUploadDelegate::StartOAuth2Request(
    OAuth2AccessTokenManager::Consumer* consumer) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  OAuth2AccessTokenManager::ScopeSet scope_set;
  scope_set.insert(GaiaConstants::kDeviceManagementServiceOAuth);
  return access_token_manager_->StartRequest(account_id_, scope_set, consumer);
}

std::unique_ptr<::network::SimpleURLLoader>
FileUploadDelegate::CreatePostLoader(
    std::unique_ptr<::network::ResourceRequest> resource_request) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  resource_request->method = "POST";
  if (resource_request->url.is_empty()) {
    resource_request->url = upload_url_;
  }
  resource_request->headers.SetHeader(kUploadProtocolHeader, "resumable");
  return ::network::SimpleURLLoader::Create(std::move(resource_request),
                                            *traffic_annotation_);
}

void FileUploadDelegate::SendAndGetResponse(
    ::network::SimpleURLLoader* url_loader,
    base::OnceCallback<void(scoped_refptr<::net::HttpResponseHeaders> headers)>
        response_cb) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  url_loader->DownloadHeadersOnly(
      url_loader_factory_.get(),
      base::BindPostTaskToCurrentDefault(std::move(response_cb)));
}

// static
void FileUploadDelegate::DoInitiate(
    std::string_view origin_path,
    std::string_view upload_parameters,
    base::OnceCallback<void(
        StatusOr<std::pair<int64_t /*total*/, std::string /*session_token*/>>)>
        result_cb) {
  if (!::content::BrowserThread::CurrentlyOn(::content::BrowserThread::UI)) {
    ::content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            &FileUploadDelegate::DoInitiate, GetWeakPtr(),
            std::string(origin_path), std::string(upload_parameters),
            base::BindPostTaskToCurrentDefault(std::move(result_cb))));
    return;
  }

  InitializeOnce();

  DVLOG(1) << "Creating file upload job for support tool use";

  (new AccessTokenRetriever(
       GetWeakPtr(),
       base::BindPostTaskToCurrentDefault(base::BindOnce(
           &FileUploadDelegate::OnAccessTokenResult, GetWeakPtr(),
           std::string(origin_path), std::string(upload_parameters),
           std::move(result_cb)))))
      ->RequestAccessToken();
}

void FileUploadDelegate::OnAccessTokenResult(
    std::string_view origin_path,
    std::string_view upload_parameters,
    base::OnceCallback<void(
        StatusOr<std::pair<int64_t /*total*/, std::string /*session_token*/>>)>
        result_cb,
    StatusOr<std::string> access_token_result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!access_token_result.has_value()) {
    std::move(result_cb).Run(
        base::unexpected(std::move(access_token_result).error()));
    return;
  }

  // Measure file size and store it in total.
  (new InitContext(origin_path, upload_parameters, access_token_result.value(),
                   GetWeakPtr(), std::move(result_cb)))
      ->Run();
}

void FileUploadDelegate::DoNextStep(
    int64_t total,
    int64_t uploaded,
    std::string_view session_token,
    ScopedReservation scoped_reservation,
    base::OnceCallback<void(StatusOr<std::pair<int64_t /*uploaded*/,
                                               std::string /*session_token*/>>)>
        result_cb) {
  if (!::content::BrowserThread::CurrentlyOn(::content::BrowserThread::UI)) {
    ::content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            &FileUploadDelegate::DoNextStep, GetWeakPtr(), total, uploaded,
            std::string(session_token), std::move(scoped_reservation),
            base::BindPostTaskToCurrentDefault(std::move(result_cb))));
    return;
  }

  InitializeOnce();

  (new NextStepContext(total, uploaded, session_token,
                       std::move(scoped_reservation), GetWeakPtr(),
                       std::move(result_cb)))
      ->Run();
}

void FileUploadDelegate::DoFinalize(
    std::string_view session_token,
    base::OnceCallback<void(StatusOr<std::string /*access_parameters*/>)>
        result_cb) {
  if (!::content::BrowserThread::CurrentlyOn(::content::BrowserThread::UI)) {
    ::content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&FileUploadDelegate::DoFinalize, GetWeakPtr(),
                                  std::string(session_token),
                                  base::BindPostTaskToCurrentDefault(
                                      std::move(result_cb))));
    return;
  }

  InitializeOnce();

  (new FinalContext(session_token, GetWeakPtr(), std::move(result_cb)))->Run();
}

void FileUploadDelegate::DoDeleteFile(std::string_view origin_path) {
  const auto delete_result = base::DeleteFile(base::FilePath(origin_path));
  if (!delete_result) {
    LOG(WARNING) << "Failed to delete file=" << origin_path;
  }
}

base::WeakPtr<FileUploadDelegate> FileUploadDelegate::GetWeakPtr() {
  CHECK(weak_ptr_factory_) << "Factory already invalidated.";
  return weak_ptr_factory_->GetWeakPtr();
}
}  // namespace reporting