// 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 "content/browser/worker_host/worker_script_loader.h"

#include "base/functional/bind.h"
#include "content/browser/loader/navigation_loader_interceptor.h"
#include "content/browser/loader/response_head_update_params.h"
#include "content/browser/service_worker/service_worker_client.h"
#include "content/browser/service_worker/service_worker_main_resource_handle.h"
#include "content/browser/service_worker/service_worker_main_resource_loader_interceptor.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/url_utils.h"
#include "net/base/ip_endpoint.h"
#include "net/base/load_timing_info.h"
#include "net/base/net_errors.h"
#include "net/url_request/redirect_util.h"
#include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/early_hints.mojom.h"

namespace content {

WorkerScriptLoader::WorkerScriptLoader(
    ChildProcessId process_id,
    const DedicatedOrSharedWorkerToken& worker_token,
    int32_t request_id,
    uint32_t options,
    const network::ResourceRequest& resource_request,
    const net::IsolationInfo& isolation_info,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client,
    base::WeakPtr<ServiceWorkerMainResourceHandle> service_worker_handle,
    const BrowserContextGetter& browser_context_getter,
    scoped_refptr<network::SharedURLLoaderFactory> default_loader_factory,
    const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
    : request_id_(request_id),
      options_(options),
      resource_request_(resource_request),
      client_(std::move(client)),
      service_worker_handle_(std::move(service_worker_handle)),
      browser_context_getter_(browser_context_getter),
      default_loader_factory_(std::move(default_loader_factory)),
      traffic_annotation_(traffic_annotation) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!service_worker_handle_) {
    // The DedicatedWorkerHost or SharedWorkerHost is already destroyed.
    Abort();
    return;
  }
  interceptor_ = ServiceWorkerMainResourceLoaderInterceptor::CreateForWorker(
      resource_request_, isolation_info, process_id, worker_token,
      service_worker_handle_);

  Start();
}

WorkerScriptLoader::~WorkerScriptLoader() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

base::WeakPtr<WorkerScriptLoader> WorkerScriptLoader::GetWeakPtr() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return weak_factory_.GetWeakPtr();
}

void WorkerScriptLoader::Abort() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  complete_status_ = network::URLLoaderCompletionStatus(net::ERR_ABORTED);
  CommitCompleted();
}

void WorkerScriptLoader::Start() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  CHECK_EQ(state_, State::kInitial);

  // The DedicatedWorkerHost or SharedWorkerHost is already destroyed.
  if (!service_worker_handle_) {
    Abort();
    return;
  }

  BrowserContext* browser_context = browser_context_getter_.Run();
  if (!browser_context) {
    Abort();
    return;
  }

  if (interceptor_) {
    interceptor_->MaybeCreateLoader(
        resource_request_, browser_context,
        base::BindOnce(&WorkerScriptLoader::MaybeStartLoader,
                       weak_factory_.GetWeakPtr(), interceptor_.get()),
        base::BindOnce(&WorkerScriptLoader::Fallback,
                       weak_factory_.GetWeakPtr()));
    return;
  }

  LoadFromNetwork();
}

network::mojom::URLLoaderFactory* WorkerScriptLoader::Fallback(
    base::WeakPtr<WorkerScriptLoader> self,
    ResponseHeadUpdateParams) {
  if (!self) {
    return nullptr;
  }
  return self->default_loader_factory_.get();
}

void WorkerScriptLoader::MaybeStartLoader(
    ServiceWorkerMainResourceLoaderInterceptor* interceptor,
    std::optional<NavigationLoaderInterceptor::Result> interceptor_result) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  CHECK_EQ(state_, State::kInitial);
  DCHECK(interceptor);

  if (!service_worker_handle_) {
    // The DedicatedWorkerHost or SharedWorkerHost is already destroyed.
    Abort();
    return;
  }

  // `interceptor_result->subresource_loader_params` isn't set by
  // ServiceWorkerMainResourceLoaderInterceptor and thus is ignored here.

  if (interceptor_result && interceptor_result->single_request_factory) {
    // The interceptor elected to handle the request. Use it.
    url_loader_factory_ = std::move(interceptor_result->single_request_factory);
    url_loader_.reset();
    url_loader_factory_->CreateLoaderAndStart(
        url_loader_.BindNewPipeAndPassReceiver(), request_id_, options_,
        resource_request_,
        url_loader_client_receiver_.BindNewPipeAndPassRemote(),
        traffic_annotation_);
    // We continue in URLLoaderClient calls.
    return;
  }

  // The interceptor didn't elect to handle the request. Fallback to network.
  LoadFromNetwork();
}

void WorkerScriptLoader::LoadFromNetwork() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  CHECK_EQ(state_, State::kInitial);

  url_loader_client_receiver_.reset();
  url_loader_factory_ = default_loader_factory_;
  url_loader_.reset();
  url_loader_factory_->CreateLoaderAndStart(
      url_loader_.BindNewPipeAndPassReceiver(), request_id_, options_,
      resource_request_, url_loader_client_receiver_.BindNewPipeAndPassRemote(),
      traffic_annotation_);
  // We continue in URLLoaderClient calls.
}

// URLLoader -------------------------------------------------------------------
// When this class gets a FollowRedirect IPC from the renderer, it restarts with
// the new URL.

void WorkerScriptLoader::FollowRedirect(
    network::HttpRequestHeadersUpdateParams headers_update_params,
    const std::optional<GURL>& new_url) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(!new_url.has_value()) << "Redirect with modified URL was not "
                                  "supported yet. crbug.com/845683";
  DCHECK(redirect_info_);

  // |should_clear_upload| is unused because there is no body anyway.
  DCHECK(!resource_request_.request_body);
  bool should_clear_upload = false;
  net::RedirectUtil::UpdateHttpRequest(
      resource_request_.url, resource_request_.method, *redirect_info_,
      headers_update_params.removed_headers,
      headers_update_params.modified_headers, &resource_request_.headers,
      &should_clear_upload);
  resource_request_.cors_exempt_headers.MergeFrom(
      headers_update_params.modified_cors_exempt_headers);
  for (const std::string& name : headers_update_params.removed_headers) {
    resource_request_.cors_exempt_headers.RemoveHeader(name);
  }

  resource_request_.url = redirect_info_->new_url;
  resource_request_.method = redirect_info_->new_method;
  resource_request_.site_for_cookies = redirect_info_->new_site_for_cookies;
  resource_request_.referrer = GURL(redirect_info_->new_referrer);
  resource_request_.referrer_policy = redirect_info_->new_referrer_policy;

  // Restart the request.
  url_loader_client_receiver_.reset();
  redirect_info_.reset();

  Start();
}

// Below we make a small effort to support the other URLLoader functions by
// forwarding to the current |url_loader_| if any, but don't bother queuing
// state or propagating state to a new URLLoader upon redirect.
void WorkerScriptLoader::SetPriority(net::RequestPriority priority,
                                     int32_t intra_priority_value) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (url_loader_)
    url_loader_->SetPriority(priority, intra_priority_value);
}
// URLLoader end --------------------------------------------------------------

// URLLoaderClient ------------------------------------------------------------
// This class forwards any client messages to the outer client in the renderer.
// Additionally, on redirects it saves the redirect info so if the renderer
// calls FollowRedirect(), it can do so.

void WorkerScriptLoader::OnReceiveEarlyHints(
    network::mojom::EarlyHintsPtr early_hints) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  client_->OnReceiveEarlyHints(std::move(early_hints));
}

void WorkerScriptLoader::OnReceiveResponse(
    network::mojom::URLResponseHeadPtr response_head,
    mojo::ScopedDataPipeConsumerHandle body,
    std::optional<mojo_base::BigBuffer> cached_metadata) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (resource_request_.url.SchemeIsBlob() && response_head) {
    // A blob URL is loaded by a renderer-hosted BlobURLLoader. A blob URL
    // request is never handled by a service worker or a network socket.
    // Sanitize service worker and network address space fields to ensure
    // robustness against invalid input and maintain expected specifications.
    response_head->was_fetched_via_service_worker = false;
    response_head->url_list_via_service_worker.clear();
    response_head->service_worker_response_source =
        network::mojom::FetchResponseSource::kUnspecified;
    response_head->initial_service_worker_status.reset();
    response_head->service_worker_router_info.reset();
    response_head->client_address_space =
        network::mojom::IPAddressSpace::kUnknown;
    response_head->response_address_space =
        network::mojom::IPAddressSpace::kUnknown;
    response_head->remote_endpoint = net::IPEndPoint();
    response_head->was_fetched_via_cache = false;
    response_head->is_validated = false;
  }
  client_->OnReceiveResponse(std::move(response_head), std::move(body),
                             std::move(cached_metadata));
}

void WorkerScriptLoader::OnReceiveRedirect(
    const net::RedirectInfo& redirect_info,
    network::mojom::URLResponseHeadPtr response_head) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (resource_request_.url.SchemeIsBlob()) {
    // Loading a blob URL never produces a redirect.
    complete_status_ =
        network::URLLoaderCompletionStatus(net::ERR_UNSAFE_REDIRECT);
    CommitCompleted();
    return;
  }

  if (!IsSafeRedirectTarget(resource_request_.url, redirect_info.new_url)) {
    complete_status_ =
        network::URLLoaderCompletionStatus(net::ERR_UNSAFE_REDIRECT);
    CommitCompleted();
    return;
  }

  if (--redirect_limit_ == 0) {
    complete_status_ =
        network::URLLoaderCompletionStatus(net::ERR_TOO_MANY_REDIRECTS);
    CommitCompleted();
    return;
  }

  redirect_info_ = redirect_info;
  client_->OnReceiveRedirect(redirect_info, std::move(response_head));
}

void WorkerScriptLoader::OnUploadProgress(
    int64_t current_position,
    int64_t total_size,
    OnUploadProgressCallback ack_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  client_->OnUploadProgress(current_position, total_size,
                            std::move(ack_callback));
}

void WorkerScriptLoader::OnTransferSizeUpdated(int32_t transfer_size_diff) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  network::RecordOnTransferSizeUpdatedUMA(
      network::OnTransferSizeUpdatedFrom::kWorkerScriptLoader);
  client_->OnTransferSizeUpdated(transfer_size_diff);
}

void WorkerScriptLoader::OnComplete(
    const network::URLLoaderCompletionStatus& status) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  complete_status_ = status;
  switch (state_) {
    case State::kInitial:
      // Don't wait for `WorkerScriptFetcher::callback_` on failure.
      if (status.error_code != net::OK) {
        break;
      }
      state_ = State::kOnCompleteCalled;
      return;
    case State::kFetcherCallbackCalled:
      break;
    case State::kOnCompleteCalled:
    case State::kCompleted:
      NOTREACHED();
  }
  CommitCompleted();
}

// URLLoaderClient end ---------------------------------------------------------

void WorkerScriptLoader::OnFetcherCallbackCalled() {
  switch (state_) {
    case State::kInitial:
      state_ = State::kFetcherCallbackCalled;
      break;
    case State::kOnCompleteCalled:
      CHECK(complete_status_);
      CHECK_EQ(complete_status_->error_code, net::OK);
      CommitCompleted();
      break;
    case State::kCompleted:
      // `CommitCompleted()` is already called with a failure and thus safely
      // ignore the fetcher callback notification.
      break;
    case State::kFetcherCallbackCalled:
      NOTREACHED();
  }
}

// `CommitCompleted()` with `net::OK` must not be called before
// `WorkerScriptFetcher::callback_` to ensure the order:
// 1. `ServiceWorkerContainerHost` pipes are passed to the renderer process
//    inside `WorkerScriptFetcher::callback_`
// 2. `ServiceWorkerClient::SetExecutionReady()` is called inside
//    `WorkerScriptLoader::CommitCompleted()`
// 3. `client_->OnComplete()` is called which eventually triggers worker
//    top-level script evaluation on the renderer process.
// (Note that non-OK `CommitCompleted()` can be called without 1 or 3)
void WorkerScriptLoader::CommitCompleted() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  CHECK_NE(state_, State::kCompleted);
  CHECK(complete_status_);
  state_ = State::kCompleted;

  if (complete_status_->error_code == net::OK && service_worker_handle_ &&
      service_worker_handle_->service_worker_client()) {
    service_worker_handle_->service_worker_client()->SetExecutionReady();
  }

  client_->OnComplete(*complete_status_);

  // We're done. Ensure we no longer send messages to our client, and no longer
  // talk to the loader we're a client of.
  client_.reset();
  url_loader_client_receiver_.reset();
  url_loader_.reset();
}

}  // namespace content
