chromium/chrome/browser/offline_pages/offline_page_url_loader.cc

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

#include "chrome/browser/offline_pages/offline_page_url_loader.h"

#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/offline_pages/offline_page_utils.h"
#include "chrome/browser/renderer_host/chrome_navigation_ui_data.h"
#include "components/offline_pages/core/offline_page_feature.h"
#include "components/offline_pages/core/offline_page_item.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/system/simple_watcher.h"
#include "net/base/io_buffer.h"
#include "net/url_request/referrer_policy.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"

namespace offline_pages {

namespace {

constexpr uint32_t kBufferSize = 4096;

content::WebContents* GetWebContents(int frame_tree_node_id) {
  return content::WebContents::FromFrameTreeNodeId(frame_tree_node_id);
}

bool GetTabId(content::WebContents* web_contents, int* tab_id) {
  return OfflinePageUtils::GetTabId(web_contents, tab_id);
}

net::RedirectInfo CreateRedirectInfo(const GURL& redirected_url,
                                     int response_code) {
  net::RedirectInfo redirect_info;
  redirect_info.new_url = redirected_url;
  redirect_info.new_referrer_policy = net::ReferrerPolicy::NO_REFERRER;
  redirect_info.new_method = "GET";
  redirect_info.status_code = response_code;
  redirect_info.new_site_for_cookies =
      net::SiteForCookies::FromUrl(redirect_info.new_url);
  return redirect_info;
}

bool ShouldCreateLoader(const network::ResourceRequest& resource_request) {
  // Ignore the requests not for the main frame.
  if (resource_request.resource_type !=
      static_cast<int>(blink::mojom::ResourceType::kMainFrame))
    return false;

  // Ignore non-http/https requests.
  if (!resource_request.url.SchemeIsHTTPOrHTTPS())
    return false;

  // Ignore requests other than GET.
  if (resource_request.method != "GET")
    return false;

  return true;
}

}  // namespace

// static
std::unique_ptr<OfflinePageURLLoader> OfflinePageURLLoader::Create(
    content::NavigationUIData* navigation_ui_data,
    int frame_tree_node_id,
    const network::ResourceRequest& tentative_resource_request,
    content::URLLoaderRequestInterceptor::LoaderCallback callback) {
  if (ShouldCreateLoader(tentative_resource_request)) {
    return base::WrapUnique(new OfflinePageURLLoader(
        navigation_ui_data, frame_tree_node_id, tentative_resource_request,
        std::move(callback)));
  }

  std::move(callback).Run({});
  return nullptr;
}

OfflinePageURLLoader::OfflinePageURLLoader(
    content::NavigationUIData* navigation_ui_data,
    int frame_tree_node_id,
    const network::ResourceRequest& tentative_resource_request,
    content::URLLoaderRequestInterceptor::LoaderCallback callback)
    : navigation_ui_data_(navigation_ui_data),
      frame_tree_node_id_(frame_tree_node_id),
      transition_type_(tentative_resource_request.transition_type),
      loader_callback_(std::move(callback)) {
  // TODO(crbug.com/40590410): Figure out how offline page interception should
  // interact with URLLoaderThrottles. It might be incorrect to use
  // |tentative_resource_request.headers| here, since throttles can rewrite
  // headers between now and when the request handler passed to
  // |loader_callback_| is invoked.
  request_handler_ = std::make_unique<OfflinePageRequestHandler>(
      tentative_resource_request.url, tentative_resource_request.headers, this);
  request_handler_->Start();
}

OfflinePageURLLoader::~OfflinePageURLLoader() {}

void OfflinePageURLLoader::SetTabIdGetterForTesting(
    OfflinePageRequestHandler::Delegate::TabIdGetter tab_id_getter) {
  tab_id_getter_ = tab_id_getter;
}

void OfflinePageURLLoader::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) {
  NOTREACHED_IN_MIGRATION();
}

void OfflinePageURLLoader::SetPriority(net::RequestPriority priority,
                                       int32_t intra_priority_value) {
  // Ignore: this class doesn't have a concept of priority.
}

void OfflinePageURLLoader::PauseReadingBodyFromNet() {
  // Ignore: this class doesn't read from network.
}

void OfflinePageURLLoader::ResumeReadingBodyFromNet() {
  // Ignore: this class doesn't read from network.
}

void OfflinePageURLLoader::FallbackToDefault() {
  std::move(loader_callback_).Run({});
}

void OfflinePageURLLoader::NotifyStartError(int error) {
  std::move(loader_callback_)
      .Run(base::BindOnce(&OfflinePageURLLoader::OnReceiveError,
                          weak_ptr_factory_.GetWeakPtr(), error));
}

void OfflinePageURLLoader::NotifyHeadersComplete(int64_t file_size) {
  std::move(loader_callback_)
      .Run(base::BindOnce(&OfflinePageURLLoader::OnReceiveResponse,
                          weak_ptr_factory_.GetWeakPtr(), file_size));
}

void OfflinePageURLLoader::NotifyReadRawDataComplete(int bytes_read) {
  if (bytes_read < 0) {
    // Negative |bytes_read| is net error code.
    Finish(bytes_read);
    return;
  }
  if (bytes_read == 0) {
    // Zero |bytes_read| means reaching EOF.
    Finish(net::OK);
    return;
  }

  bytes_of_raw_data_to_transfer_ = base::checked_cast<size_t>(bytes_read);
  write_position_ = 0;

  TransferRawData();
}

void OfflinePageURLLoader::TransferRawData() {
  while (true) {
    base::span<const uint8_t> bytes = base::as_bytes(buffer_->span())
                                          .first(bytes_of_raw_data_to_transfer_)
                                          .subspan(write_position_);
    // If all the read data have been transferred, read more.
    if (bytes.empty()) {
      ReadRawData();
      return;
    }

    size_t bytes_written = 0;
    MojoResult result = producer_handle_->WriteData(
        bytes, MOJO_WRITE_DATA_FLAG_NONE, bytes_written);
    if (result == MOJO_RESULT_SHOULD_WAIT) {
      handle_watcher_->ArmOrNotify();
      return;
    }

    if (result != MOJO_RESULT_OK) {
      Finish(net::ERR_FAILED);
      return;
    }

    write_position_ += bytes_written;
  }
}

void OfflinePageURLLoader::SetOfflinePageNavigationUIData(
    bool is_offline_page) {
  // This method should be called before the response data is received.
  DCHECK(!receiver_.is_bound());

  ChromeNavigationUIData* navigation_data =
      static_cast<ChromeNavigationUIData*>(navigation_ui_data_);
  std::unique_ptr<OfflinePageNavigationUIData> offline_page_data =
      std::make_unique<OfflinePageNavigationUIData>(is_offline_page);
  navigation_data->SetOfflinePageNavigationUIData(std::move(offline_page_data));
}

int OfflinePageURLLoader::GetPageTransition() const {
  return transition_type_;
}

OfflinePageRequestHandler::Delegate::WebContentsGetter
OfflinePageURLLoader::GetWebContentsGetter() const {
  return base::BindRepeating(&GetWebContents, frame_tree_node_id_);
}

OfflinePageRequestHandler::Delegate::TabIdGetter
OfflinePageURLLoader::GetTabIdGetter() const {
  if (!tab_id_getter_.is_null())
    return tab_id_getter_;
  return base::BindRepeating(&GetTabId);
}

void OfflinePageURLLoader::ReadRawData() {
  int result = request_handler_->ReadRawData(buffer_.get(), kBufferSize);
  // If |result| is not ERR_IO_PENDING, the read data is available immediately.
  // Otherwise, the read is asynchronous and NotifyReadRawDataComplete will
  // be invoked when the read finishes.
  if (result != net::ERR_IO_PENDING)
    NotifyReadRawDataComplete(result);
}

void OfflinePageURLLoader::OnReceiveError(
    int error,
    const network::ResourceRequest& /* resource_request */,
    mojo::PendingReceiver<network::mojom::URLLoader> receiver,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client) {
  client_.Bind(std::move(client));
  Finish(error);
}

void OfflinePageURLLoader::OnReceiveResponse(
    int64_t file_size,
    const network::ResourceRequest& /* resource_request */,
    mojo::PendingReceiver<network::mojom::URLLoader> receiver,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client) {
  // TODO(crbug.com/40590410): Figure out how offline page interception should
  // interact with URLLoaderThrottles. It might be incorrect to ignore
  // |resource_request| here, since it's the current request after
  // throttles.
  DCHECK(!receiver_.is_bound());
  receiver_.Bind(std::move(receiver));
  receiver_.set_disconnect_handler(base::BindOnce(
      &OfflinePageURLLoader::OnMojoDisconnect, weak_ptr_factory_.GetWeakPtr()));
  client_.Bind(std::move(client));

  mojo::ScopedDataPipeConsumerHandle consumer_handle;
  if (mojo::CreateDataPipe(kBufferSize, producer_handle_, consumer_handle) !=
      MOJO_RESULT_OK) {
    Finish(net::ERR_FAILED);
    return;
  }

  auto response_head = network::mojom::URLResponseHead::New();
  response_head->request_start = base::TimeTicks::Now();
  response_head->response_start = response_head->request_start;

  scoped_refptr<net::HttpResponseHeaders> redirect_headers =
      request_handler_->GetRedirectHeaders();
  if (redirect_headers.get()) {
    std::string redirected_url;
    bool is_redirect = redirect_headers->IsRedirect(&redirected_url);
    DCHECK(is_redirect);
    response_head->headers = redirect_headers;
    response_head->encoded_data_length = 0;
    client_->OnReceiveRedirect(
        CreateRedirectInfo(GURL(redirected_url),
                           redirect_headers->response_code()),
        std::move(response_head));
    return;
  }

  response_head->mime_type = "multipart/related";
  response_head->content_length = file_size;

  client_->OnReceiveResponse(std::move(response_head),
                             std::move(consumer_handle), std::nullopt);

  handle_watcher_ = std::make_unique<mojo::SimpleWatcher>(
      FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL,
      base::SequencedTaskRunner::GetCurrentDefault());
  handle_watcher_->Watch(
      producer_handle_.get(), MOJO_HANDLE_SIGNAL_WRITABLE,
      MOJO_WATCH_CONDITION_SATISFIED,
      base::BindRepeating(&OfflinePageURLLoader::OnHandleReady,
                          weak_ptr_factory_.GetWeakPtr()));

  buffer_ = base::MakeRefCounted<net::IOBufferWithSize>(kBufferSize);
  ReadRawData();
}

void OfflinePageURLLoader::OnHandleReady(
    MojoResult result,
    const mojo::HandleSignalsState& state) {
  if (result != MOJO_RESULT_OK) {
    Finish(net::ERR_FAILED);
    return;
  }
  TransferRawData();
}

void OfflinePageURLLoader::Finish(int error) {
  client_->OnComplete(network::URLLoaderCompletionStatus(error));
  handle_watcher_.reset();
  producer_handle_.reset();
  client_.reset();
  weak_ptr_factory_.InvalidateWeakPtrs();
  MaybeDeleteSelf();
}

void OfflinePageURLLoader::OnMojoDisconnect() {
  receiver_.reset();
  client_.reset();
  MaybeDeleteSelf();
}

void OfflinePageURLLoader::MaybeDeleteSelf() {
  if (!receiver_.is_bound() && !client_.is_bound())
    delete this;
}

}  // namespace offline_pages