// 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/devtools/devtools_url_loader_interceptor.h"

#include <memory>
#include <optional>
#include <string_view>

#include "base/barrier_closure.h"
#include "base/base64.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/no_destructor.h"
#include "base/strings/pattern.h"
#include "base/strings/string_util.h"
#include "base/strings/string_view_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/unguessable_token.h"
#include "content/browser/devtools/protocol/network.h"
#include "content/browser/devtools/protocol/network_handler.h"
#include "content/browser/devtools/request_body_collector.h"
#include "content/browser/loader/download_utils_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_client.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/system/data_pipe_drainer.h"
#include "net/base/load_flags.h"
#include "net/base/mime_sniffer.h"
#include "net/cookies/cookie_access_result.h"
#include "net/cookies/cookie_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "net/url_request/redirect_info.h"
#include "net/url_request/redirect_util.h"
#include "net/url_request/referrer_policy.h"
#include "net/url_request/url_request_job.h"  // For static util methods.
#include "services/network/public/cpp/content_decoding_interceptor.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/header_util.h"
#include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/encoded_body_length.mojom-forward.h"
#include "services/network/public/mojom/encoded_body_length.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "third_party/blink/public/platform/resource_request_blocked_reason.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"

namespace content {

InterceptedRequestInfo::InterceptedRequestInfo() = default;

InterceptedRequestInfo::~InterceptedRequestInfo() = default;

DevToolsURLLoaderInterceptor::AuthChallengeResponse::AuthChallengeResponse(
    ResponseType response_type)
    : response_type(response_type) {
  DCHECK_NE(kProvideCredentials, response_type);
}

DevToolsURLLoaderInterceptor::AuthChallengeResponse::AuthChallengeResponse(
    const std::u16string& username,
    const std::u16string& password)
    : response_type(kProvideCredentials), credentials(username, password) {}

DevToolsURLLoaderInterceptor::FilterEntry::FilterEntry(
    const base::UnguessableToken& target_id,
    std::vector<Pattern> patterns,
    RequestInterceptedCallback callback)
    : target_id(target_id),
      patterns(std::move(patterns)),
      callback(std::move(callback)) {}

DevToolsURLLoaderInterceptor::FilterEntry::FilterEntry(FilterEntry&&) = default;
DevToolsURLLoaderInterceptor::FilterEntry::~FilterEntry() = default;

DevToolsURLLoaderInterceptor::Modifications::Modifications() = default;

DevToolsURLLoaderInterceptor::Modifications::Modifications(
    net::Error error_reason)
    : error_reason(error_reason) {}

DevToolsURLLoaderInterceptor::Modifications::Modifications(
    scoped_refptr<net::HttpResponseHeaders> response_headers,
    scoped_refptr<base::RefCountedMemory> response_body)
    : response_headers(std::move(response_headers)),
      response_body(std::move(response_body)) {}

DevToolsURLLoaderInterceptor::Modifications::Modifications(
    std::unique_ptr<AuthChallengeResponse> auth_challenge_response)
    : auth_challenge_response(std::move(auth_challenge_response)) {}

DevToolsURLLoaderInterceptor::Modifications::Modifications(
    std::optional<std::string> modified_url,
    std::optional<std::string> modified_method,
    std::optional<protocol::Binary> modified_post_data,
    std::unique_ptr<HeadersVector> modified_headers,
    std::optional<bool> intercept_response)
    : modified_url(std::move(modified_url)),
      modified_method(std::move(modified_method)),
      modified_post_data(std::move(modified_post_data)),
      modified_headers(std::move(modified_headers)),
      intercept_response(std::move(intercept_response)) {}

DevToolsURLLoaderInterceptor::Modifications::Modifications(
    std::optional<net::Error> error_reason,
    scoped_refptr<net::HttpResponseHeaders> response_headers,
    scoped_refptr<base::RefCountedMemory> response_body,
    size_t body_offset,
    std::optional<std::string> modified_url,
    std::optional<std::string> modified_method,
    std::optional<protocol::Binary> modified_post_data,
    std::unique_ptr<HeadersVector> modified_headers,
    std::unique_ptr<AuthChallengeResponse> auth_challenge_response)
    : error_reason(std::move(error_reason)),
      response_headers(std::move(response_headers)),
      response_body(std::move(response_body)),
      body_offset(body_offset),
      modified_url(std::move(modified_url)),
      modified_method(std::move(modified_method)),
      modified_post_data(std::move(modified_post_data)),
      modified_headers(std::move(modified_headers)),
      auth_challenge_response(std::move(auth_challenge_response)) {}

DevToolsURLLoaderInterceptor::Modifications::~Modifications() = default;

DevToolsURLLoaderInterceptor::Pattern::~Pattern() = default;

DevToolsURLLoaderInterceptor::Pattern::Pattern(const Pattern& other) = default;

DevToolsURLLoaderInterceptor::Pattern::Pattern(
    const std::string& url_pattern,
    base::flat_set<blink::mojom::ResourceType> resource_types,
    InterceptionStage interception_stage)
    : url_pattern(url_pattern),
      resource_types(std::move(resource_types)),
      interception_stage(interception_stage) {}

bool DevToolsURLLoaderInterceptor::Pattern::Matches(
    const std::string& url,
    blink::mojom::ResourceType resource_type) const {
  if (!resource_types.empty() &&
      !base::Contains(resource_types, resource_type)) {
    return false;
  }
  return base::MatchPattern(url, url_pattern);
}

struct CreateLoaderParameters {
  CreateLoaderParameters(
      int32_t request_id,
      uint32_t options,
      network::ResourceRequest request,
      net::MutableNetworkTrafficAnnotationTag traffic_annotation)
      : request_id(request_id),
        options(options),
        request(request),
        traffic_annotation(traffic_annotation) {}

  const int32_t request_id;
  const uint32_t options;
  network::ResourceRequest request;
  const net::MutableNetworkTrafficAnnotationTag traffic_annotation;
};

namespace {

using RequestInterceptedCallback =
    DevToolsURLLoaderInterceptor::RequestInterceptedCallback;
using ContinueInterceptedRequestCallback =
    DevToolsURLLoaderInterceptor::ContinueInterceptedRequestCallback;
using GetResponseBodyCallback =
    DevToolsURLLoaderInterceptor::GetResponseBodyForInterceptionCallback;
using TakeResponseBodyPipeCallback =
    DevToolsURLLoaderInterceptor::TakeResponseBodyPipeCallback;
using Modifications = DevToolsURLLoaderInterceptor::Modifications;
using InterceptionStage = DevToolsURLLoaderInterceptor::InterceptionStage;
using protocol::Response;
using network::mojom::CredentialsMode;
using network::mojom::FetchResponseType;

class BodyReader : public mojo::DataPipeDrainer::Client {
 public:
  explicit BodyReader(base::OnceClosure download_complete_callback)
      : download_complete_callback_(std::move(download_complete_callback)),
        body_(base::MakeRefCounted<base::RefCountedString>()) {}

  void StartReading(mojo::ScopedDataPipeConsumerHandle body);

  void AddCallback(std::unique_ptr<GetResponseBodyCallback> callback) {
    if (data_complete_) {
      DCHECK(callbacks_.empty());
      callback->sendSuccess(encoded_body_, true);
      return;
    }
    callbacks_.push_back(std::move(callback));
  }

  bool data_complete() const { return data_complete_; }

  scoped_refptr<base::RefCountedMemory> body() const {
    DCHECK(data_complete_);
    return body_;
  }

  void CancelWithError(std::string error) {
    for (auto& cb : callbacks_)
      cb->sendFailure(Response::ServerError(error));
    callbacks_.clear();
  }

 private:
  void OnDataAvailable(base::span<const uint8_t> data) override {
    DCHECK(!data_complete_);
    body_->as_string().append(base::as_string_view(data));
  }

  void OnDataComplete() override;

  std::unique_ptr<mojo::DataPipeDrainer> body_pipe_drainer_;
  std::vector<std::unique_ptr<GetResponseBodyCallback>> callbacks_;
  base::OnceClosure download_complete_callback_;
  scoped_refptr<base::RefCountedString> body_;
  std::string encoded_body_;
  bool data_complete_ = false;
};

void BodyReader::StartReading(mojo::ScopedDataPipeConsumerHandle body) {
  DCHECK(!callbacks_.empty());
  DCHECK(!body_pipe_drainer_);
  DCHECK(!data_complete_);

  body_pipe_drainer_ =
      std::make_unique<mojo::DataPipeDrainer>(this, std::move(body));
}

void BodyReader::OnDataComplete() {
  DCHECK(!data_complete_);
  data_complete_ = true;
  body_pipe_drainer_.reset();
  // TODO(caseq): only encode if necessary.
  encoded_body_ = base::Base64Encode(*body_);
  for (auto& cb : callbacks_)
    cb->sendSuccess(encoded_body_, true);
  callbacks_.clear();
  std::move(download_complete_callback_).Run();
}

struct ResponseMetadata {
  ResponseMetadata() = default;
  explicit ResponseMetadata(network::mojom::URLResponseHeadPtr head)
      : head(std::move(head)) {}

  network::mojom::URLResponseHeadPtr head =
      network::mojom::URLResponseHead::New();
  std::unique_ptr<net::RedirectInfo> redirect_info;
  std::optional<mojo_base::BigBuffer> cached_metadata;
  size_t encoded_length = 0;
  size_t transfer_size = 0;
  network::URLLoaderCompletionStatus status;
};

void RemoveUnsafeRequestHeadersOnRedirect(net::HttpRequestHeaders& headers) {
  // Mimic the behavior of URLLoader::OnReceivedRedirect. It has already
  // been called for the request and we just need to reflect the changes
  // on our side. It is ok to remove more headers than the network stack
  // did as RequestReceivedExtraInfo event will contain all the actual
  // headers.
  headers.RemoveHeader(net::HttpRequestHeaders::kCookie);
  const net::HttpRequestHeaders::HeaderVector request_headers =
      headers.GetHeaderVector();
  for (const auto& header : request_headers) {
    if (StartsWith(header.key, "sec-ch-",
                   base::CompareCase::INSENSITIVE_ASCII) ||
        StartsWith(header.key, "sec-fetch-",
                   base::CompareCase::INSENSITIVE_ASCII)) {
      headers.RemoveHeader(header.key);
    }
  }
}

class HeadersOverride {
 public:
  static std::unique_ptr<HeadersOverride> SaveAndOverride(
      network::ResourceRequest& request,
      DevToolsURLLoaderInterceptor::Modifications::HeadersVector
          modified_headers) {
    std::unique_ptr<HeadersOverride> instance(new HeadersOverride(request));
    DCHECK(request.headers.IsEmpty());

    for (const auto& entry : modified_headers) {
      if (base::EqualsCaseInsensitiveASCII(entry.first,
                                           net::HttpRequestHeaders::kReferer)) {
        request.referrer = GURL(entry.second);
        request.referrer_policy = net::ReferrerPolicy::NEVER_CLEAR;
      } else {
        request.headers.SetHeader(entry.first, entry.second);
      }
    }
    return instance;
  }

  static void Revert(std::unique_ptr<HeadersOverride> instance) {
    instance->request_->headers = std::move(instance->original_headers_);
    instance->request_->referrer = instance->original_referrer_;
    instance->request_->referrer_policy = instance->original_referrer_policy_;
  }

  void RemoveUnsafeOriginalHeadersOnRedirect() {
    RemoveUnsafeRequestHeadersOnRedirect(original_headers_);
  }

  // Compute `remove_headers` and `modified_headers` that are needed
  // to turn `a` into `b`.
  static void ComputeModifications(const net::HttpRequestHeaders& a,
                                   const net::HttpRequestHeaders& b,
                                   std::vector<std::string>& removed_headers,
                                   net::HttpRequestHeaders& modified_headers) {
    DCHECK(removed_headers.empty());
    DCHECK(modified_headers.IsEmpty());

    std::map<std::string, std::string> old_headers;
    for (const auto& entry : a.GetHeaderVector())
      old_headers.insert({entry.key, entry.value});

    for (const auto& entry : b.GetHeaderVector()) {
      auto it = old_headers.find(entry.key);
      if (it == old_headers.end() || it->second != entry.value)
        modified_headers.SetHeader(entry.key, entry.value);
      if (it != old_headers.end())
        old_headers.erase(it);
    }
    for (const auto& entry : old_headers)
      removed_headers.push_back(entry.first);
  }

 private:
  explicit HeadersOverride(network::ResourceRequest& request)
      : request_(request),
        original_headers_(std::move(request.headers)),
        original_referrer_(request.referrer),
        original_referrer_policy_(request.referrer_policy) {}

  const raw_ref<network::ResourceRequest> request_;
  net::HttpRequestHeaders original_headers_;
  GURL original_referrer_;
  net::ReferrerPolicy original_referrer_policy_;
};

}  // namespace

class InterceptionJob : public network::mojom::URLLoaderClient,
                        public network::mojom::URLLoader {
 public:
  static InterceptionJob* FindByRequestId(
      const GlobalRequestID& global_req_id) {
    const auto& map = GetInterceptionJobMap();
    auto it = map.find(global_req_id);
    return it == map.end() ? nullptr : it->second;
  }

  InterceptionJob(
      DevToolsURLLoaderInterceptor* interceptor,
      const std::string& id,
      const base::UnguessableToken& frame_token,
      int32_t process_id,
      const std::optional<std::string>& renderer_request_id,
      std::unique_ptr<CreateLoaderParameters> create_loader_params,
      bool is_download,
      mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
      mojo::PendingRemote<network::mojom::URLLoaderClient> client,
      mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
      mojo::PendingRemote<network::mojom::CookieManager> cookie_manager);

  InterceptionJob(const InterceptionJob&) = delete;
  InterceptionJob& operator=(const InterceptionJob&) = delete;

  void GetResponseBody(std::unique_ptr<GetResponseBodyCallback> callback);
  void TakeResponseBodyPipe(TakeResponseBodyPipeCallback callback);
  void ContinueInterceptedRequest(
      std::unique_ptr<Modifications> modifications,
      std::unique_ptr<ContinueInterceptedRequestCallback> callback);
  void Detach();

  void OnAuthRequest(
      const net::AuthChallengeInfo& auth_info,
      DevToolsURLLoaderInterceptor::HandleAuthRequestCallback callback);

 private:
  static std::map<GlobalRequestID, InterceptionJob*>& GetInterceptionJobMap() {
    static base::NoDestructor<std::map<GlobalRequestID, InterceptionJob*>> inst;
    return *inst;
  }

  ~InterceptionJob() override {
    if (registered_in_global_request_map_) {
      size_t erased = GetInterceptionJobMap().erase(global_req_id_);
      DCHECK_EQ(1lu, erased);
    }
  }

  Response InnerContinueRequest(std::unique_ptr<Modifications> modifications);
  void ProcessFollowRedirect(
      const net::HttpRequestHeaders& modified_cors_exempt_headers);
  void ProcessAuthResponse(
      const DevToolsURLLoaderInterceptor::AuthChallengeResponse&
          auth_challenge_response);
  Response ProcessResponseOverride(
      scoped_refptr<net::HttpResponseHeaders> headers,
      scoped_refptr<base::RefCountedMemory> body,
      size_t response_body_offset);
  void ProcessRedirectByClient(const GURL& redirect_url);
  void ProcessSetCookies(const net::HttpResponseHeaders& response_headers,
                         base::OnceClosure callback);
  void SendResponse(scoped_refptr<base::RefCountedMemory> body, size_t offset);
  void ApplyModificationsToRequest(
      std::unique_ptr<Modifications> modifications);

  void StartRequest();
  void CancelRequest();
  void CompleteRequest(const network::URLLoaderCompletionStatus& status);
  void Shutdown();

  std::unique_ptr<InterceptedRequestInfo> BuildRequestInfo(
      const network::mojom::URLResponseHeadPtr& head);
  void NotifyClient(std::unique_ptr<InterceptedRequestInfo> request_info);
  void FetchCookies(base::OnceClosure callback);
  void OnGotCookies(
      base::OnceClosure callback,
      const net::CookieAccessResultList& cookies_with_access_result,
      const net::CookieAccessResultList& excluded_cookies);
  void OnGotRequestBodies(base::OnceClosure callback,
                          std::vector<RequestBodyCollector::BodyEntry> bodies);
  void CompleteNotifyingClient(
      std::unique_ptr<InterceptedRequestInfo> request_info);

  void ResponseBodyComplete();

  bool ShouldBypassForResponse() const {
    if (state_ == State::kResponseTaken)
      return false;
    DCHECK_EQ(!!response_metadata_, !!body_reader_);
    DCHECK_EQ(state_, State::kResponseReceived);
    return !response_metadata_;
  }

  // network::mojom::URLLoader methods
  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;
  void SetPriority(net::RequestPriority priority,
                   int32_t intra_priority_value) override;

#if BUILDFLAG(ARKWEB_RESOURCE_INTERCEPTION)
  void OnTransferDataWithSharedMemory(base::ReadOnlySharedMemoryRegion region, uint64_t buffer_size) override {}
#endif

  // network::mojom::URLLoaderClient methods
  void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override;
  void OnReceiveResponse(
      network::mojom::URLResponseHeadPtr head,
      mojo::ScopedDataPipeConsumerHandle body,
      std::optional<mojo_base::BigBuffer> cached_metadata) override;
  void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
                         network::mojom::URLResponseHeadPtr head) override;
  void OnUploadProgress(int64_t current_position,
                        int64_t total_size,
                        OnUploadProgressCallback callback) override;
  void OnTransferSizeUpdated(int32_t transfer_size_diff) override;
  void OnComplete(const network::URLLoaderCompletionStatus& status) override;

  void StartLoadingResponseBody(mojo::ScopedDataPipeConsumerHandle body);

  bool CanGetResponseBody(std::string* error_reason);
  bool StartJobAndMaybeNotify();

  void UpdateCORSFlag();
  network::mojom::FetchResponseType CalculateResponseTainting();
  network::ResourceRequest GetResourceRequestForCookies();

  const std::string id_prefix_;
  const GlobalRequestID global_req_id_;
  const base::UnguessableToken frame_token_;
  const bool report_upload_;

  raw_ptr<DevToolsURLLoaderInterceptor> interceptor_;
  DevToolsURLLoaderInterceptor::InterceptionStages stages_;

  std::unique_ptr<CreateLoaderParameters> create_loader_params_;
  const bool is_download_;

  mojo::Receiver<network::mojom::URLLoaderClient> client_receiver_{this};
  mojo::Receiver<network::mojom::URLLoader> loader_receiver_{this};

  mojo::Remote<network::mojom::URLLoaderClient> client_;
  mojo::Remote<network::mojom::URLLoader> loader_;
  mojo::Remote<network::mojom::URLLoaderFactory> target_factory_;
  mojo::Remote<network::mojom::CookieManager> cookie_manager_;

  enum State {
    kNotStarted,
    kRequestSent,
    kRedirectReceived,
    kFollowRedirect,
    kAuthRequired,
    kResponseReceived,
    kResponseTaken,
  };

  State state_;
  base::TimeTicks start_ticks_;
  base::Time start_time_;

  bool waiting_for_resolution_;
  int redirect_count_;
  bool tainted_origin_ = false;
  bool fetch_cors_flag_ = false;
  std::string current_id_;
  std::string redirected_request_id_;

  std::unique_ptr<BodyReader> body_reader_;
  std::unique_ptr<ResponseMetadata> response_metadata_;
  std::vector<net::SourceStreamType> client_side_content_decoding_types_;
  mojo::ScopedDataPipeConsumerHandle body_;
  bool registered_in_global_request_map_;

  std::optional<std::pair<net::RequestPriority, int32_t>> priority_;
  DevToolsURLLoaderInterceptor::HandleAuthRequestCallback
      pending_auth_callback_;
  TakeResponseBodyPipeCallback pending_response_body_pipe_callback_;

  const std::optional<std::string> renderer_request_id_;

  // List of URLs that have been redirected through. The last member is the
  // current request URL. Tracked for the purpose of computing the proper
  // SameSite cookies to return, which depends on the redirect chain.
  std::vector<GURL> url_chain_;
  // In case headers are overridden, keep the original and restore them
  // upon a redirect, so that overrides don't stick across redirects.
  std::unique_ptr<HeadersOverride> headers_override_;
  // Header overrides are reverted before the redirect, so that
  // request paused event contains original headers. Previous headers
  // are used on resume to compute the difference for the network stack.
  std::unique_ptr<net::HttpRequestHeaders> headers_before_redirect_;

  // These two are needed to build a Request and are prepared as needed when
  // sending Request for the first time. Both need to be cleared upon redirect.
  std::vector<RequestBodyCollector::BodyEntry> request_bodies_;
  std::optional<std::string> request_cookies_;

  // This is only for retaining the body collector for the duration of its
  // work (and properly cancelling it if the job gets prematurely destroyed).
  std::unique_ptr<RequestBodyCollector> request_body_collector_;

  SEQUENCE_CHECKER(sequence_checker_);
};

void DevToolsURLLoaderInterceptor::CreateJob(
    const base::UnguessableToken& frame_token,
    int32_t process_id,
    bool is_download,
    const std::optional<std::string>& renderer_request_id,
    std::unique_ptr<CreateLoaderParameters> create_params,
    mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client,
    mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
    mojo::PendingRemote<network::mojom::CookieManager> cookie_manager) {
  DCHECK(!frame_token.is_empty());

  static int last_id = 0;

  std::string id = base::StringPrintf("interception-job-%d", ++last_id);
  // This class will manage its own life time to match the loader client.
  new InterceptionJob(
      this, std::move(id), frame_token, process_id, renderer_request_id,
      std::move(create_params), is_download, std::move(loader_receiver),
      std::move(client), std::move(target_factory), std::move(cookie_manager));
}

DevToolsURLLoaderInterceptor::InterceptionStages
DevToolsURLLoaderInterceptor::GetInterceptionStages(
    const GURL& url,
    blink::mojom::ResourceType resource_type) const {
  DevToolsURLLoaderInterceptor::InterceptionStages stages;
  std::string unused;
  std::string url_str = protocol::NetworkHandler::ExtractFragment(url, &unused);
  for (const auto& pattern : patterns_) {
    if (pattern.Matches(url_str, resource_type))
      stages.Put(pattern.interception_stage);
  }
  return stages;
}

class DevToolsURLLoaderFactoryProxy : public network::mojom::URLLoaderFactory {
 public:
  DevToolsURLLoaderFactoryProxy(
      const base::UnguessableToken& frame_token,
      int32_t process_id,
      bool is_download,
      mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver,
      mojo::PendingRemote<network::mojom::URLLoaderFactory>
          target_factory_remote,
      mojo::PendingRemote<network::mojom::CookieManager> cookie_manager,
      base::WeakPtr<DevToolsURLLoaderInterceptor> interceptor);
  ~DevToolsURLLoaderFactoryProxy() override;

 private:
  // network::mojom::URLLoaderFactory implementation
  void CreateLoaderAndStart(
      mojo::PendingReceiver<network::mojom::URLLoader> loader,
      int32_t request_id,
      uint32_t options,
      const network::ResourceRequest& request,
      mojo::PendingRemote<network::mojom::URLLoaderClient> client,
      const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
      override;
  void Clone(mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver)
      override;

  void OnProxyBindingError();
  void OnTargetFactoryError();

  const base::UnguessableToken frame_token_;
  const int32_t process_id_;
  const bool is_download_;

  mojo::Remote<network::mojom::URLLoaderFactory> target_factory_;
  mojo::Remote<network::mojom::CookieManager> cookie_manager_;
  base::WeakPtr<DevToolsURLLoaderInterceptor> interceptor_;
  mojo::ReceiverSet<network::mojom::URLLoaderFactory> receivers_;

  SEQUENCE_CHECKER(sequence_checker_);
};

// This class owns itself and will delete self when any mojo
// connection is broken.
DevToolsURLLoaderFactoryProxy::DevToolsURLLoaderFactoryProxy(
    const base::UnguessableToken& frame_token,
    int32_t process_id,
    bool is_download,
    mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver,
    mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory_remote,
    mojo::PendingRemote<network::mojom::CookieManager> cookie_manager,
    base::WeakPtr<DevToolsURLLoaderInterceptor> interceptor)
    : frame_token_(frame_token),
      process_id_(process_id),
      is_download_(is_download),
      target_factory_(std::move(target_factory_remote)),
      interceptor_(std::move(interceptor)) {
  target_factory_.set_disconnect_handler(
      base::BindOnce(&DevToolsURLLoaderFactoryProxy::OnTargetFactoryError,
                     base::Unretained(this)));

  receivers_.Add(this, std::move(loader_receiver));
  receivers_.set_disconnect_handler(
      base::BindRepeating(&DevToolsURLLoaderFactoryProxy::OnProxyBindingError,
                          base::Unretained(this)));

  cookie_manager_.Bind(std::move(cookie_manager));
  cookie_manager_.set_disconnect_handler(
      base::BindOnce(&DevToolsURLLoaderFactoryProxy::OnTargetFactoryError,
                     base::Unretained(this)));
}

DevToolsURLLoaderFactoryProxy::~DevToolsURLLoaderFactoryProxy() = default;

void DevToolsURLLoaderFactoryProxy::CreateLoaderAndStart(
    mojo::PendingReceiver<network::mojom::URLLoader> loader,
    int32_t request_id,
    uint32_t options,
    const network::ResourceRequest& request,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client,
    const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  DevToolsURLLoaderInterceptor* interceptor = interceptor_.get();
  if (!interceptor_ || request.url.SchemeIs(url::kDataScheme)) {
    target_factory_->CreateLoaderAndStart(std::move(loader), request_id,
                                          options, request, std::move(client),
                                          traffic_annotation);
    return;
  }
  auto creation_params = std::make_unique<CreateLoaderParameters>(
      request_id, options, request, traffic_annotation);
  mojo::PendingRemote<network::mojom::URLLoaderFactory> factory_clone;
  target_factory_->Clone(factory_clone.InitWithNewPipeAndPassReceiver());
  mojo::PendingRemote<network::mojom::CookieManager> cookie_manager_clone;
  cookie_manager_->CloneInterface(
      cookie_manager_clone.InitWithNewPipeAndPassReceiver());
  interceptor->CreateJob(
      frame_token_, process_id_, is_download_, request.devtools_request_id,
      std::move(creation_params), std::move(loader), std::move(client),
      std::move(factory_clone), std::move(cookie_manager_clone));
}

void DevToolsURLLoaderFactoryProxy::Clone(
    mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  receivers_.Add(this, std::move(receiver));
}

void DevToolsURLLoaderFactoryProxy::OnTargetFactoryError() {
  delete this;
}

void DevToolsURLLoaderFactoryProxy::OnProxyBindingError() {
  if (receivers_.empty())
    delete this;
}

// static
void DevToolsURLLoaderInterceptor::HandleAuthRequest(
    GlobalRequestID req_id,
    const net::AuthChallengeInfo& auth_info,
    HandleAuthRequestCallback callback) {
  if (auto* job = InterceptionJob::FindByRequestId(req_id))
    job->OnAuthRequest(auth_info, std::move(callback));
  else
    std::move(callback).Run(true, std::nullopt);
}

DevToolsURLLoaderInterceptor::DevToolsURLLoaderInterceptor(
    RequestInterceptedCallback callback)
    : request_intercepted_callback_(std::move(callback)), weak_factory_(this) {}

DevToolsURLLoaderInterceptor::~DevToolsURLLoaderInterceptor() {
  for (auto const& entry : jobs_)
    entry.second->Detach();
}

void DevToolsURLLoaderInterceptor::SetPatterns(
    std::vector<DevToolsURLLoaderInterceptor::Pattern> patterns,
    bool handle_auth) {
  patterns_ = std::move(patterns);
  handle_auth_ = handle_auth;
  DCHECK(patterns_.size() || !handle_auth);
}

void DevToolsURLLoaderInterceptor::GetResponseBody(
    const std::string& interception_id,
    std::unique_ptr<GetResponseBodyCallback> callback) {
  if (InterceptionJob* job = FindJob(interception_id, &callback))
    job->GetResponseBody(std::move(callback));
}

void DevToolsURLLoaderInterceptor::TakeResponseBodyPipe(
    const std::string& interception_id,
    DevToolsURLLoaderInterceptor::TakeResponseBodyPipeCallback callback) {
  auto it = jobs_.find(interception_id);
  if (it == jobs_.end()) {
    std::move(callback).Run(
        protocol::Response::InvalidParams("Invalid InterceptionId."),
        mojo::ScopedDataPipeConsumerHandle(), std::string());
    return;
  }
  it->second->TakeResponseBodyPipe(std::move(callback));
}

void DevToolsURLLoaderInterceptor::ContinueInterceptedRequest(
    const std::string& interception_id,
    std::unique_ptr<Modifications> modifications,
    std::unique_ptr<ContinueInterceptedRequestCallback> callback) {
  if (InterceptionJob* job = FindJob(interception_id, &callback)) {
    job->ContinueInterceptedRequest(std::move(modifications),
                                    std::move(callback));
  }
}

bool DevToolsURLLoaderInterceptor::CreateProxyForInterception(
    int process_id,
    StoragePartition* storage_partition,
    const base::UnguessableToken& frame_token,
    bool is_navigation,
    bool is_download,
    network::mojom::URLLoaderFactoryOverride* intercepting_factory) {
  DCHECK(storage_partition);

  if (patterns_.empty())
    return false;

  // If we're the first interceptor to install an override, make a
  // remote/receiver pair, then handle this similarly to appending
  // a proxy to existing override.
  if (!intercepting_factory->overriding_factory) {
    DCHECK(!intercepting_factory->overridden_factory_receiver);
    intercepting_factory->overridden_factory_receiver =
        intercepting_factory->overriding_factory
            .InitWithNewPipeAndPassReceiver();
  }
  mojo::PendingRemote<network::mojom::URLLoaderFactory> target_remote;
  auto overridden_factory_receiver =
      target_remote.InitWithNewPipeAndPassReceiver();
  mojo::PendingRemote<network::mojom::CookieManager> cookie_manager;

  // TODO(crbug.com/40276949): Using 0 as the process id for navigations
  // can lead to collisions between multiple navigations/service workers main
  // script fetch. It should be replaced by the more robust
  // GlobalRequestID::MakeBrowserInitiated().
  int process_id_override = process_id;
  if (is_navigation)
    process_id_override = 0;

  storage_partition->GetNetworkContext()->GetCookieManager(
      cookie_manager.InitWithNewPipeAndPassReceiver());
  new DevToolsURLLoaderFactoryProxy(
      frame_token, process_id_override, is_download,
      std::move(intercepting_factory->overridden_factory_receiver),
      std::move(target_remote), std::move(cookie_manager),
      weak_factory_.GetWeakPtr());
  intercepting_factory->overridden_factory_receiver =
      std::move(overridden_factory_receiver);
  return true;
}

InterceptionJob::InterceptionJob(
    DevToolsURLLoaderInterceptor* interceptor,
    const std::string& id,
    const base::UnguessableToken& frame_token,
    int process_id,
    const std::optional<std::string>& renderer_request_id,
    std::unique_ptr<CreateLoaderParameters> create_loader_params,
    bool is_download,
    mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client,
    mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
    mojo::PendingRemote<network::mojom::CookieManager> cookie_manager)
    : id_prefix_(id),
      global_req_id_(process_id, create_loader_params->request_id),
      frame_token_(frame_token),
      report_upload_(!!create_loader_params->request.request_body),
      interceptor_(interceptor),
      create_loader_params_(std::move(create_loader_params)),
      is_download_(is_download),
      client_(std::move(client)),
      target_factory_(std::move(target_factory)),
      cookie_manager_(std::move(cookie_manager)),
      state_(kNotStarted),
      waiting_for_resolution_(false),
      redirect_count_(0),
      renderer_request_id_(renderer_request_id) {
  loader_receiver_.Bind(std::move(loader_receiver));
  loader_receiver_.set_disconnect_handler(
      base::BindOnce(&InterceptionJob::Shutdown, base::Unretained(this)));

  auto& job_map = GetInterceptionJobMap();
  // TODO(caseq): for now, all auth requests will go to the top-level job.
  // Figure out if we need anything smarter here.
  registered_in_global_request_map_ =
      job_map.emplace(global_req_id_, this).second;

  url_chain_.push_back(create_loader_params_->request.url);

  if (StartJobAndMaybeNotify())
    return;

  StartRequest();
}

bool InterceptionJob::StartJobAndMaybeNotify() {
  UpdateCORSFlag();
  start_ticks_ = base::TimeTicks::Now();
  start_time_ = base::Time::Now();

  current_id_ = id_prefix_ + base::StringPrintf(".%d", redirect_count_);
  interceptor_->AddJob(current_id_, this);

  const network::ResourceRequest& request = create_loader_params_->request;
  stages_ = interceptor_->GetInterceptionStages(
      request.url,
      static_cast<blink::mojom::ResourceType>(request.resource_type));

  if (!stages_.Has(InterceptionStage::kRequest)) {
    return false;
  }

  if (state_ == State::kRedirectReceived)
    state_ = State::kFollowRedirect;
  else
    DCHECK_EQ(State::kNotStarted, state_);
  NotifyClient(BuildRequestInfo(nullptr));
  return true;
}

// FIXME(caseq): The logic in the three methods below is borrowed from
// CorsURLLoader as a matter of a quick and mergeable fix for crbug.com/1022173.
// This logic should be unified with CorsURLLoader.
network::mojom::FetchResponseType InterceptionJob::CalculateResponseTainting() {
  if (fetch_cors_flag_)
    return FetchResponseType::kCors;
  if (create_loader_params_->request.mode ==
          network::mojom::RequestMode::kNoCors &&
      tainted_origin_) {
    return FetchResponseType::kOpaque;
  }
  return FetchResponseType::kBasic;
}

network::ResourceRequest InterceptionJob::GetResourceRequestForCookies() {
  FetchResponseType response_tainting =
      fetch_cors_flag_ ? FetchResponseType::kCors : FetchResponseType::kBasic;

  network::ResourceRequest result = create_loader_params_->request;
  result.credentials_mode =
      network::cors::CalculateCredentialsFlag(
          create_loader_params_->request.credentials_mode, response_tainting)
          ? CredentialsMode::kInclude
          : CredentialsMode::kOmit;
  return result;
}

void InterceptionJob::UpdateCORSFlag() {
  if (fetch_cors_flag_)
    return;

  const network::ResourceRequest& request = create_loader_params_->request;
  fetch_cors_flag_ = network::cors::ShouldCheckCors(
      request.url, request.request_initiator, request.mode);
}

bool InterceptionJob::CanGetResponseBody(std::string* error_reason) {
  if (!stages_.Has(InterceptionStage::kResponse)) {
    *error_reason =
        "Can only get response body on HeadersReceived pattern matched "
        "requests.";
    return false;
  }
  if (state_ != State::kResponseReceived || !waiting_for_resolution_) {
    *error_reason =
        "Can only get response body on requests captured after headers "
        "received.";
    return false;
  }
  return true;
}

void InterceptionJob::GetResponseBody(
    std::unique_ptr<GetResponseBodyCallback> callback) {
  std::string error_reason;
  if (!CanGetResponseBody(&error_reason)) {
    callback->sendFailure(Response::ServerError(std::move(error_reason)));
    return;
  }
  bool body_reader_created = !body_reader_;
  if (!body_reader_) {
    body_reader_ = std::make_unique<BodyReader>(base::BindOnce(
        &InterceptionJob::ResponseBodyComplete, base::Unretained(this)));
    client_receiver_.Resume();
  }
  body_reader_->AddCallback(std::move(callback));
  // Needs to happen after |AddCallback| to avoid a DCHECK.
  if (body_reader_created && body_)
    StartLoadingResponseBody(std::move(body_));
}

void InterceptionJob::TakeResponseBodyPipe(
    TakeResponseBodyPipeCallback callback) {
  std::string error_reason;
  if (!CanGetResponseBody(&error_reason)) {
    std::move(callback).Run(Response::ServerError(std::move(error_reason)),
                            mojo::ScopedDataPipeConsumerHandle(),
                            std::string());
    return;
  }
  DCHECK_EQ(state_, State::kResponseReceived);
  DCHECK(!!response_metadata_);
  state_ = State::kResponseTaken;
  pending_response_body_pipe_callback_ = std::move(callback);
  client_receiver_.Resume();
  if (body_)
    StartLoadingResponseBody(std::move(body_));
}

void InterceptionJob::ContinueInterceptedRequest(
    std::unique_ptr<Modifications> modifications,
    std::unique_ptr<ContinueInterceptedRequestCallback> callback) {
  Response response = InnerContinueRequest(std::move(modifications));
  // |this| may be destroyed at this point.
  if (response.IsSuccess())
    callback->sendSuccess();
  else
    callback->sendFailure(std::move(response));
}

void InterceptionJob::Detach() {
  stages_.Clear();
  interceptor_ = nullptr;
  if (!waiting_for_resolution_)
    return;
  if (state_ == State::kAuthRequired) {
    state_ = State::kRequestSent;
    waiting_for_resolution_ = false;
    // Corresponds to the TRACE_EVENT_BEGIN in CompleteNotifyingClient.
    TRACE_EVENT_END("devtools", perfetto::Track::FromPointer(this));
    std::move(pending_auth_callback_).Run(true, std::nullopt);
    return;
  }
  InnerContinueRequest(std::make_unique<Modifications>());
}

Response InterceptionJob::InnerContinueRequest(
    std::unique_ptr<Modifications> modifications) {
  if (!waiting_for_resolution_) {
    return Response::ServerError(
        "Invalid state for continueInterceptedRequest");
  }
  waiting_for_resolution_ = false;
  // Corresponds to the TRACE_EVENT_BEGIN in CompleteNotifyingClient.
  TRACE_EVENT_END("devtools", perfetto::Track::FromPointer(this));
  if (modifications->intercept_response.has_value()) {
    stages_.PutOrRemove(InterceptionStage::kResponse,
                        modifications->intercept_response.value());
  }

  if (state_ == State::kAuthRequired) {
    if (!modifications->auth_challenge_response)
      return Response::InvalidParams("authChallengeResponse required.");
    ProcessAuthResponse(*modifications->auth_challenge_response);
    return Response::Success();
  }
  if (modifications->auth_challenge_response)
    return Response::InvalidParams("authChallengeResponse not expected.");

  if (modifications->error_reason) {
    network::URLLoaderCompletionStatus status(
        modifications->error_reason.value());
    status.completion_time = base::TimeTicks::Now();
    if (modifications->error_reason == net::ERR_BLOCKED_BY_CLIENT) {
      // So we know that these modifications originated from devtools
      // (also known as inspector), and can therefore annotate the
      // request. We only do this for one specific error code thus
      // far, to minimize risk of breaking other usages.
      status.extended_error_code =
          static_cast<int>(blink::ResourceRequestBlockedReason::kInspector);
    }
    CompleteRequest(status);
    return Response::Success();
  }

  if (modifications->response_headers || modifications->response_body) {
    // If only intercepted response headers are overridden continue with
    // normal load of the original response body.
    if (response_metadata_ && !modifications->response_body) {
      network::mojom::URLResponseHeadPtr& head = response_metadata_->head;
      head->headers = std::move(modifications->response_headers);
      // TODO(caseq): we're cheating here a bit, raw_headers() have \0's
      // where real headers would have \r\n, but the sizes here
      // probably don't have to be exact.
      size_t headers_size = head->headers->raw_headers().size();
      head->encoded_data_length = headers_size;
    } else {
      return ProcessResponseOverride(std::move(modifications->response_headers),
                                     std::move(modifications->response_body),
                                     modifications->body_offset);
    }
  }

  if (state_ == State::kFollowRedirect) {
    if (!modifications->modified_url.has_value()) {
      // TODO(caseq): report error if other modifications are present.

      // At this point we already reverted headers to the original state.
      if (modifications->modified_headers) {
        headers_override_ = HeadersOverride::SaveAndOverride(
            create_loader_params_->request,
            std::move(*modifications->modified_headers));
      }

      ProcessFollowRedirect({});
      return Response::Success();
    }
    CancelRequest();
    // Fall through to the generic logic of re-starting the request
    // at the bottom of the method.
  }
  if (state_ == State::kRedirectReceived) {
    // TODO(caseq): report error if other modifications are present.
    if (modifications->modified_url.has_value()) {
      std::string location = modifications->modified_url.value();
      CancelRequest();
      response_metadata_->head->headers->SetHeader("location", location);
      GURL redirect_url = create_loader_params_->request.url.Resolve(location);
      if (!redirect_url.is_valid())
        return Response::ServerError("Invalid modified URL");
      ProcessRedirectByClient(redirect_url);
      return Response::Success();
    }
    client_->OnReceiveRedirect(*response_metadata_->redirect_info,
                               std::move(response_metadata_->head));
    return Response::Success();
  }

  if (body_reader_) {
    if (body_reader_->data_complete())
      SendResponse(body_reader_->body(), 0);

    // There are read callbacks pending, so let the reader do its job and come
    // back when it's done.
    return Response::Success();
  }

  if (response_metadata_) {
    if (state_ == State::kResponseTaken) {
      return Response::InvalidParams(
          "Unable to continue request as is after body is taken");
    }
    // TODO(caseq): report error if other modifications are present.
    if (response_metadata_->status.error_code) {
      CompleteRequest(response_metadata_->status);
      return Response::Success();
    }
    DCHECK_EQ(State::kResponseReceived, state_);
    DCHECK(!body_reader_);
    client_->OnReceiveResponse(std::move(response_metadata_->head),
                               std::move(body_),
                               std::move(response_metadata_->cached_metadata));
    response_metadata_.reset();
    client_receiver_.Resume();
    return Response::Success();
  }

  DCHECK_EQ(State::kNotStarted, state_);
  ApplyModificationsToRequest(std::move(modifications));
  headers_before_redirect_.reset();
  StartRequest();
  return Response::Success();
}

void InterceptionJob::ProcessFollowRedirect(
    const net::HttpRequestHeaders& modified_cors_exempt_headers) {
  std::vector<std::string> removed_headers;
  net::HttpRequestHeaders modified_headers;
  CHECK(headers_before_redirect_);
  HeadersOverride::ComputeModifications(*headers_before_redirect_,
                                        create_loader_params_->request.headers,
                                        removed_headers, modified_headers);
  headers_before_redirect_.reset();
  loader_->FollowRedirect(removed_headers, modified_headers,
                          modified_cors_exempt_headers, std::nullopt);
  state_ = State::kRequestSent;
}

void InterceptionJob::ApplyModificationsToRequest(
    std::unique_ptr<Modifications> modifications) {
  network::ResourceRequest* request = &create_loader_params_->request;

  // Note this redirect is not visible to the page by design. If they want a
  // visible redirect they can mock a response with a 302.
  if (modifications->modified_url.has_value()) {
    DCHECK_EQ(url_chain_.back(), request->url);
    const GURL new_url(modifications->modified_url.value());
    const url::Origin original_origin = url::Origin::Create(request->url);
    const url::Origin new_origin = url::Origin::Create(new_url);
    request->url = new_url;
    url_chain_.back() = new_url;

    // A direct origin comparison is used instead of
    // `net::SchemefulSite::IsSameSite`. `IsSameSite` considers different ports
    // on localhost as same-site, which conflicts with the stricter,
    // origin-based Referrer-Policy check performed later in
    // `URLRequest::StartJob`. This mismatch would cause the `NetworkDelegate`
    // to block the request via
    // `CancelURLRequestWithPolicyViolatingReferrerHeader`, leading to an
    // `ERR_BLOCKED_BY_CLIENT` failure. Using an origin-based check here aligns
    // the logic and prevents this error.
    if (original_origin != new_origin) {
      GURL new_referrer = net::URLRequestJob::ComputeReferrerForPolicy(
          request->referrer_policy, request->referrer, new_url,
          /* same_origin_out_for_metrics*/ nullptr);
      // net/ has a similar check but would block a request with wrong referrer,
      // so help clients a bit.
      if (new_referrer != request->referrer) {
        request->referrer = {};
      }
      request->site_for_cookies = net::SiteForCookies::FromUrl(new_url);
      if (request->trusted_params) {
        request->trusted_params->isolation_info = {};
      }
    }
  }

  if (modifications->modified_method.has_value()) {
    request->method = modifications->modified_method.value();
  }

  if (modifications->modified_post_data.has_value()) {
    const auto& post_data = modifications->modified_post_data.value();
    request->request_body =
        network::ResourceRequestBody::CreateFromCopyOfBytes(post_data);
    request_bodies_.clear();
  }

  if (modifications->modified_headers) {
    DCHECK(!headers_override_);
    headers_override_ = HeadersOverride::SaveAndOverride(
        *request, std::move(*modifications->modified_headers));
  }
}

void InterceptionJob::ProcessAuthResponse(
    const DevToolsURLLoaderInterceptor::AuthChallengeResponse& response) {
  DCHECK_EQ(kAuthRequired, state_);
  switch (response.response_type) {
    case DevToolsURLLoaderInterceptor::AuthChallengeResponse::kDefault:
      std::move(pending_auth_callback_).Run(true, std::nullopt);
      break;
    case DevToolsURLLoaderInterceptor::AuthChallengeResponse::kCancelAuth:
      std::move(pending_auth_callback_).Run(false, std::nullopt);
      break;
    case DevToolsURLLoaderInterceptor::AuthChallengeResponse::
        kProvideCredentials:
      std::move(pending_auth_callback_).Run(false, response.credentials);
      break;
  }
  state_ = kRequestSent;
}

Response InterceptionJob::ProcessResponseOverride(
    scoped_refptr<net::HttpResponseHeaders> headers,
    scoped_refptr<base::RefCountedMemory> body,
    size_t response_body_offset) {
  CancelRequest();

  DCHECK_LE(response_body_offset, body ? body->size() : 0);
  size_t body_size = body ? body->size() - response_body_offset : 0;
  response_metadata_ = std::make_unique<ResponseMetadata>();
  network::mojom::URLResponseHeadPtr& head = response_metadata_->head;

  head->request_time = start_time_;
  head->response_time = base::Time::Now();

  // TODO(caseq): we're only doing this because some clients expect load timing
  // to be present with mocked responses. Consider removing this.
  const base::TimeTicks now_ticks = base::TimeTicks::Now();
  head->load_timing.request_start_time = start_time_;
  head->load_timing.request_start = start_ticks_;
  head->load_timing.receive_headers_end = now_ticks;

  static const char kDummyHeaders[] = "HTTP/1.1 200 OK\0\0";
  head->headers = std::move(headers);
  if (!head->headers) {
    head->headers =
        base::MakeRefCounted<net::HttpResponseHeaders>(kDummyHeaders);
  }
  head->headers->GetMimeTypeAndCharset(&head->mime_type, &head->charset);
  const GURL& url = create_loader_params_->request.url;
  if (create_loader_params_->options &
      network::mojom::kURLLoadOptionSniffMimeType) {
    if (body_size && network::ShouldSniffContent(url, *head)) {
      size_t bytes_to_sniff =
          std::min(body_size, static_cast<size_t>(net::kMaxBytesToSniff));
      const std::string hint = head->mime_type;
      net::SniffMimeType(base::as_string_view(*body).substr(
                             response_body_offset, bytes_to_sniff),
                         url, hint, net::ForceSniffFileUrlsForHtml::kDisabled,
                         &head->mime_type);
      head->did_mime_sniff = true;
    } else if (head->mime_type.empty()) {
      head->mime_type.assign("text/plain");
    }
  }
  // TODO(caseq): we're cheating here a bit, raw_headers() have \0's
  // where real headers would have \r\n, but the sizes here
  // probably don't have to be exact.
  size_t headers_size = head->headers->raw_headers().size();
  head->content_length = body_size;
  head->encoded_data_length = headers_size;
  head->encoded_body_length = network::mojom::EncodedBodyLength::New(0u);
  head->request_start = start_ticks_;
  head->response_start = now_ticks;

  response_metadata_->transfer_size = body_size;

  response_metadata_->status.completion_time = base::TimeTicks::Now();
  response_metadata_->status.encoded_data_length = headers_size + body_size;
  response_metadata_->status.encoded_body_length = body_size;
  response_metadata_->status.decoded_body_length = body_size;

  base::OnceClosure continue_after_cookies_set;
  std::string location;
  if (head->headers->IsRedirect(&location)) {
    GURL redirect_url = url.Resolve(location);
    if (redirect_url.is_valid()) {
      continue_after_cookies_set =
          base::BindOnce(&InterceptionJob::ProcessRedirectByClient,
                         base::Unretained(this), std::move(redirect_url));
    }
  }
  if (!continue_after_cookies_set) {
    continue_after_cookies_set =
        base::BindOnce(&InterceptionJob::SendResponse, base::Unretained(this),
                       std::move(body), response_body_offset);
  }
  ProcessSetCookies(*head->headers, std::move(continue_after_cookies_set));

  return Response::Success();
}

void InterceptionJob::ProcessSetCookies(const net::HttpResponseHeaders& headers,
                                        base::OnceClosure callback) {
  if (!GetResourceRequestForCookies().SavesCookies()) {
    std::move(callback).Run();
    return;
  }

  std::vector<std::unique_ptr<net::CanonicalCookie>> cookies;
  std::optional<base::Time> server_time = headers.GetDateValue();
  base::Time now = base::Time::Now();

  const std::string_view name("Set-Cookie");
  std::string cookie_line;
  size_t iter = 0;
  while (headers.EnumerateHeader(&iter, name, &cookie_line)) {
    std::unique_ptr<net::CanonicalCookie> cookie = net::CanonicalCookie::Create(
        create_loader_params_->request.url, cookie_line, now, server_time,
        std::nullopt, net::CookieSourceType::kOther,
        /*status=*/nullptr);
    if (cookie)
      cookies.emplace_back(std::move(cookie));
  }

  net::CookieOptions options;
  options.set_include_httponly();
  bool should_treat_as_first_party =
      GetContentClient()
          ->browser()
          ->ShouldIgnoreSameSiteCookieRestrictionsWhenTopLevel(
              create_loader_params_->request.site_for_cookies.scheme(),
              create_loader_params_->request.url.SchemeIsCryptographic());
  DCHECK_EQ(create_loader_params_->request.url, url_chain_.back());
  bool is_main_frame_navigation =
      create_loader_params_->request.trusted_params.has_value() &&
      create_loader_params_->request.trusted_params->isolation_info
              .request_type() == net::IsolationInfo::RequestType::kMainFrame;
  options.set_same_site_cookie_context(
      net::cookie_util::ComputeSameSiteContextForResponse(
          url_chain_, create_loader_params_->request.site_for_cookies,
          create_loader_params_->request.request_initiator,
          is_main_frame_navigation, should_treat_as_first_party));

  // |this| might be deleted here if |cookies| is empty!
  auto on_cookie_set = base::BindRepeating(
      [](base::RepeatingClosure closure, net::CookieAccessResult) {
        closure.Run();
      },
      base::BarrierClosure(cookies.size(), std::move(callback)));
  for (auto& cookie : cookies) {
    cookie_manager_->SetCanonicalCookie(
        *cookie, create_loader_params_->request.url, options, on_cookie_set);
  }
}

void InterceptionJob::ProcessRedirectByClient(const GURL& redirect_url) {
  DCHECK(redirect_url.is_valid());

  const net::HttpResponseHeaders& headers = *response_metadata_->head->headers;
  const network::ResourceRequest& request = create_loader_params_->request;

  auto first_party_url_policy =
      request.update_first_party_url_on_redirect
          ? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT
          : net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL;

  response_metadata_->redirect_info = std::make_unique<net::RedirectInfo>(
      net::RedirectInfo::ComputeRedirectInfo(
          request.method, request.url, request.site_for_cookies,
          first_party_url_policy, request.referrer_policy,
          request.referrer.spec(), request.request_initiator,
          headers.response_code(), redirect_url,
          net::RedirectUtil::GetReferrerPolicyHeader(&headers),
          false /* insecure_scheme_was_upgraded */, true /* copy_fragment */));

  client_->OnReceiveRedirect(*response_metadata_->redirect_info,
                             std::move(response_metadata_->head));
}

void InterceptionJob::SendResponse(scoped_refptr<base::RefCountedMemory> body,
                                   size_t offset) {
  base::span<const uint8_t> bytes_to_write;
  if (body) {
    bytes_to_write = base::as_byte_span(*body).subspan(offset);
  }
  // We shouldn't be able to transfer a string that big over the protocol,
  // but just in case...
  CHECK_LE(bytes_to_write.size(), UINT32_MAX)
      << "Response bodies larger than " << UINT32_MAX << " are not supported";
  mojo::ScopedDataPipeConsumerHandle consumer_handle;
  mojo::ScopedDataPipeProducerHandle producer_handle;
  CHECK_EQ(mojo::CreateDataPipe(bytes_to_write.size(), producer_handle,
                                consumer_handle),
           MOJO_RESULT_OK);

  if (body) {
    size_t actually_written_bytes = 0;
    MojoResult res = producer_handle->WriteData(
        bytes_to_write, MOJO_WRITE_DATA_FLAG_NONE, actually_written_bytes);
    DCHECK_EQ(0u, res);
    DCHECK_EQ(actually_written_bytes, bytes_to_write.size());
  }
  response_metadata_->head->client_side_content_decoding_types.clear();
  client_->OnReceiveResponse(std::move(response_metadata_->head),
                             std::move(consumer_handle),
                             std::move(response_metadata_->cached_metadata));

  if (response_metadata_->transfer_size)
    client_->OnTransferSizeUpdated(response_metadata_->transfer_size);
  CompleteRequest(response_metadata_->status);
}

void InterceptionJob::ResponseBodyComplete() {
  if (waiting_for_resolution_)
    return;
  // We're here only if client has already told us to proceed with unmodified
  // response.
  SendResponse(body_reader_->body(), 0);
}

void InterceptionJob::StartRequest() {
  DCHECK_EQ(State::kNotStarted, state_);
  DCHECK(!response_metadata_);

  state_ = State::kRequestSent;

  target_factory_->CreateLoaderAndStart(
      loader_.BindNewPipeAndPassReceiver(), create_loader_params_->request_id,
      create_loader_params_->options, create_loader_params_->request,
      client_receiver_.BindNewPipeAndPassRemote(),
      create_loader_params_->traffic_annotation);
  client_receiver_.set_disconnect_handler(
      base::BindOnce(&InterceptionJob::Shutdown, base::Unretained(this)));

  if (priority_)
    loader_->SetPriority(priority_->first, priority_->second);
}

void InterceptionJob::CancelRequest() {
  if (state_ == State::kNotStarted)
    return;
  client_receiver_.reset();
  body_.reset();
  loader_.reset();
  if (body_reader_) {
    body_reader_->CancelWithError(
        "Another command has cancelled the fetch request");
    body_reader_.reset();
  }
  state_ = State::kNotStarted;
}

std::unique_ptr<InterceptedRequestInfo> InterceptionJob::BuildRequestInfo(
    const network::mojom::URLResponseHeadPtr& head) {
  auto result = std::make_unique<InterceptedRequestInfo>();
  result->interception_id = current_id_;
  if (renderer_request_id_.has_value())
    result->renderer_request_id = renderer_request_id_.value();
  result->frame_id = frame_token_;
  blink::mojom::ResourceType resource_type =
      static_cast<blink::mojom::ResourceType>(
          create_loader_params_->request.resource_type);
  result->resource_type = resource_type;
  result->is_navigation =
      resource_type == blink::mojom::ResourceType::kMainFrame ||
      resource_type == blink::mojom::ResourceType::kSubFrame;

  if (head && head->headers)
    result->response_headers = head->headers;
  if (!redirected_request_id_.empty())
    result->redirected_request_id = redirected_request_id_;
  return result;
}

void InterceptionJob::FetchCookies(base::OnceClosure callback) {
  net::CookieOptions options;
  options.set_include_httponly();
  options.set_do_not_update_access_time();

  const network::ResourceRequest& request = create_loader_params_->request;
  DCHECK_EQ(request.url, url_chain_.back());

  bool should_treat_as_first_party =
      GetContentClient()
          ->browser()
          ->ShouldIgnoreSameSiteCookieRestrictionsWhenTopLevel(
              request.site_for_cookies.scheme(),
              request.url.SchemeIsCryptographic());
  bool is_main_frame_navigation =
      request.trusted_params.has_value() &&
      request.trusted_params->isolation_info.request_type() ==
          net::IsolationInfo::RequestType::kMainFrame;
  options.set_same_site_cookie_context(
      net::cookie_util::ComputeSameSiteContextForRequest(
          request.method, url_chain_, request.site_for_cookies,
          request.request_initiator, is_main_frame_navigation,
          should_treat_as_first_party,
          request.destination ==
              network::mojom::RequestDestination::kWebIdentity));

  cookie_manager_->GetCookieList(
      request.url, options, net::CookiePartitionKeyCollection::Todo(),
      base::BindOnce(&InterceptionJob::OnGotCookies, base::Unretained(this),
                     std::move(callback)));
}

void InterceptionJob::NotifyClient(
    std::unique_ptr<InterceptedRequestInfo> request_info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!waiting_for_resolution_);

  const network::ResourceRequest request = GetResourceRequestForCookies();

  const bool have_cookies = !!request_cookies_;
  const bool want_cookies = request.SendsCookies();
  CHECK(!have_cookies || want_cookies);

  const bool have_request_bodies = !request_bodies_.empty();
  const bool want_request_bodies = !!request.request_body;
  CHECK(!have_request_bodies || want_request_bodies);

  const int pending_callback_count =
      (have_cookies == want_cookies ? 0 : 1) +
      (have_request_bodies == want_request_bodies ? 0 : 1);

  base::RepeatingClosure closure = BarrierClosure(
      pending_callback_count,
      base::BindOnce(&InterceptionJob::CompleteNotifyingClient,
                     base::Unretained(this), std::move(request_info)));
  if (have_cookies != want_cookies) {
    FetchCookies(closure);
  }
  if (have_request_bodies != want_request_bodies) {
    CHECK(!request_body_collector_);
    request_body_collector_ = RequestBodyCollector::Collect(
        *request.request_body,
        base::BindOnce(&InterceptionJob::OnGotRequestBodies,
                       base::Unretained(this), closure));
  }
}

void InterceptionJob::OnGotCookies(
    base::OnceClosure callback,
    const net::CookieAccessResultList& cookies_with_access_result,
    const net::CookieAccessResultList& excluded_cookies) {
  request_cookies_.emplace(
      cookies_with_access_result.empty()
          ? std::string()
          : net::CanonicalCookie::BuildCookieLine(cookies_with_access_result));
  std::move(callback).Run();
}

void InterceptionJob::OnGotRequestBodies(
    base::OnceClosure callback,
    std::vector<RequestBodyCollector::BodyEntry> bodies) {
  request_bodies_ = std::move(bodies);
  request_body_collector_.reset();
  std::move(callback).Run();
}

void InterceptionJob::CompleteNotifyingClient(
    std::unique_ptr<InterceptedRequestInfo> request_info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!interceptor_)
    return;
  request_info->network_request =
      protocol::NetworkHandler::CreateRequestFromResourceRequest(
          create_loader_params_->request,
          request_cookies_.value_or(std::string()), request_bodies_);

  TRACE_EVENT_BEGIN("devtools", "Fetch.requestPaused",
                    perfetto::Track::FromPointer(this));
  waiting_for_resolution_ = true;
  interceptor_->request_intercepted_callback_.Run(std::move(request_info));
}

void InterceptionJob::CompleteRequest(
    const network::URLLoaderCompletionStatus& status) {
  client_->OnComplete(status);
  Shutdown();
}

void InterceptionJob::Shutdown() {
  if (interceptor_)
    interceptor_->RemoveJob(current_id_);
  delete this;
}

// URLLoader methods
void InterceptionJob::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) {
  DCHECK(!new_url.has_value()) << "Redirect with modified url was not "
                                  "supported yet. crbug.com/845683";
  DCHECK(!waiting_for_resolution_);

  network::ResourceRequest* request = &create_loader_params_->request;
  const net::RedirectInfo& info = *response_metadata_->redirect_info;
  if (request->request_initiator &&
      (!url::IsSameOriginWith(info.new_url, request->url) &&
       !request->request_initiator->IsSameOriginWith(request->url))) {
    tainted_origin_ = true;
  }

  // Save previous headers and revert to the original ones before applying
  // any client changes.
  headers_before_redirect_ = std::make_unique<net::HttpRequestHeaders>(
      create_loader_params_->request.headers);
  if (headers_override_) {
    // Always revert to the first request in the chain.
    HeadersOverride::Revert(std::move(headers_override_));
  }

  bool clear_body = false;
  // Reflect changes to the request that the network service will make on
  // FollowRedirect.
  net::RedirectUtil::UpdateHttpRequest(request->url, request->method, info,
                                       removed_headers, modified_headers,
                                       &request->headers, &clear_body);
  request->cors_exempt_headers.MergeFrom(modified_cors_exempt_headers);
  for (const std::string& name : removed_headers)
    request->cors_exempt_headers.RemoveHeader(name);

  if (clear_body) {
    request->request_body = nullptr;
    request_bodies_.clear();
  }
  request_cookies_.reset();

  request->method = info.new_method;
  request->url = info.new_url;
  request->site_for_cookies = info.new_site_for_cookies;
  request->referrer_policy = info.new_referrer_policy;
  request->referrer = GURL(info.new_referrer);
  if (request->trusted_params) {
    const auto new_origin = url::Origin::Create(info.new_url);
    request->trusted_params->isolation_info =
        request->trusted_params->isolation_info.CreateForRedirect(new_origin);
  }
  response_metadata_.reset();

  UpdateCORSFlag();

  url_chain_.push_back(create_loader_params_->request.url);

  if (interceptor_) {
    redirected_request_id_ = current_id_;
    // Pretend that each redirect hop is a new request -- this is for
    // compatibilty with URLRequestJob-based interception implementation.
    interceptor_->RemoveJob(current_id_);
    redirect_count_++;
    if (StartJobAndMaybeNotify())
      return;
  }
  if (state_ == State::kRedirectReceived) {
    ProcessFollowRedirect(modified_cors_exempt_headers);
    return;
  }

  DCHECK_EQ(State::kNotStarted, state_);
  headers_before_redirect_.reset();
  StartRequest();
}

void InterceptionJob::SetPriority(net::RequestPriority priority,
                                  int32_t intra_priority_value) {
  priority_ = std::make_pair(priority, intra_priority_value);

  if (loader_)
    loader_->SetPriority(priority, intra_priority_value);
}

// URLLoaderClient methods
void InterceptionJob::OnReceiveEarlyHints(
    network::mojom::EarlyHintsPtr early_hints) {
  client_->OnReceiveEarlyHints(std::move(early_hints));
}

void InterceptionJob::OnReceiveResponse(
    network::mojom::URLResponseHeadPtr head,
    mojo::ScopedDataPipeConsumerHandle body,
    std::optional<mojo_base::BigBuffer> cached_metadata) {
  state_ = State::kResponseReceived;
  DCHECK(!response_metadata_);
  if (!stages_.Has(InterceptionStage::kResponse)) {
    client_->OnReceiveResponse(std::move(head), std::move(body),
                               std::move(cached_metadata));
    return;
  }
  client_receiver_.Pause();
  body_ = std::move(body);
  client_side_content_decoding_types_ =
      head->client_side_content_decoding_types;

  auto request_info = BuildRequestInfo(head);
  const network::ResourceRequest& request = create_loader_params_->request;
  request_info->is_download =
      request_info->is_navigation &&
      (is_download_ ||
       download_utils::IsDownload(/*browser_context=*/nullptr, request.url,
                                  head->headers.get(), head->mime_type));

  response_metadata_ = std::make_unique<ResponseMetadata>(std::move(head));
  response_metadata_->cached_metadata = std::move(cached_metadata);

  NotifyClient(std::move(request_info));
}

void InterceptionJob::OnReceiveRedirect(
    const net::RedirectInfo& redirect_info,
    network::mojom::URLResponseHeadPtr head) {
  DCHECK_EQ(State::kRequestSent, state_);
  state_ = State::kRedirectReceived;
  response_metadata_ = std::make_unique<ResponseMetadata>(head.Clone());
  response_metadata_->redirect_info =
      std::make_unique<net::RedirectInfo>(redirect_info);

  // Delete some headers to sync the request with what the network
  // service already did.
  RemoveUnsafeRequestHeadersOnRedirect(create_loader_params_->request.headers);
  if (headers_override_) {
    headers_override_->RemoveUnsafeOriginalHeadersOnRedirect();
  }

  if (!stages_.Has(InterceptionStage::kResponse)) {
    client_->OnReceiveRedirect(redirect_info, std::move(head));
    return;
  }

  std::unique_ptr<InterceptedRequestInfo> request_info = BuildRequestInfo(head);
  request_info->redirect_url = redirect_info.new_url.spec();
  NotifyClient(std::move(request_info));
}

void InterceptionJob::OnUploadProgress(int64_t current_position,
                                       int64_t total_size,
                                       OnUploadProgressCallback callback) {
  if (!report_upload_)
    return;
  client_->OnUploadProgress(current_position, total_size, std::move(callback));
}

void InterceptionJob::OnTransferSizeUpdated(int32_t transfer_size_diff) {
  network::RecordOnTransferSizeUpdatedUMA(
      network::OnTransferSizeUpdatedFrom::kInterceptionJob);
  if (ShouldBypassForResponse())
    client_->OnTransferSizeUpdated(transfer_size_diff);
  else
    response_metadata_->transfer_size += transfer_size_diff;
}

void InterceptionJob::StartLoadingResponseBody(
    mojo::ScopedDataPipeConsumerHandle body) {
  if (!client_side_content_decoding_types_.empty()) {
    network::ContentDecodingInterceptor::DecodeOnNetworkService(
        *GetNetworkService(), client_side_content_decoding_types_, body,
        network::ContentDecodingInterceptor::ClientType::kDevTools,
        base::DoNothing());
  }
  if (pending_response_body_pipe_callback_) {
    DCHECK_EQ(State::kResponseTaken, state_);
    DCHECK(!body_reader_);
    std::move(pending_response_body_pipe_callback_)
        .Run(Response::Success(), std::move(body),
             response_metadata_->head->mime_type);
    return;
  }
  DCHECK_EQ(State::kResponseReceived, state_);
  DCHECK(!ShouldBypassForResponse());
  body_reader_->StartReading(std::move(body));
}

void InterceptionJob::OnComplete(
    const network::URLLoaderCompletionStatus& status) {
  // No need to listen to the channel any more, so just reset it, so if the pipe
  // is closed by the other end, |shutdown| isn't run.
  client_receiver_.reset();
  loader_.reset();

  if (!response_metadata_) {
    // If we haven't seen response and get an error completion,
    // treat it as a response and intercept (provided response are
    // being intercepted).
    if (!stages_.Has(InterceptionStage::kResponse) || !status.error_code) {
      CompleteRequest(status);
      return;
    }
    response_metadata_ = std::make_unique<ResponseMetadata>();
    response_metadata_->status = status;
    auto request_info = BuildRequestInfo(nullptr);
    request_info->response_error_code = status.error_code;
    NotifyClient(std::move(request_info));
    return;
  }
  // Since we're not forwarding OnComplete right now, make sure
  // we're in the proper state. The completion is due upon client response.
  DCHECK(state_ == State::kResponseReceived || state_ == State::kResponseTaken)
      << "Unexpected state " << static_cast<int>(state_);
  DCHECK(waiting_for_resolution_);

  response_metadata_->status = status;
}

void InterceptionJob::OnAuthRequest(
    const net::AuthChallengeInfo& auth_info,
    DevToolsURLLoaderInterceptor::HandleAuthRequestCallback callback) {
  DCHECK_EQ(kRequestSent, state_);
  DCHECK(pending_auth_callback_.is_null());
  DCHECK(!waiting_for_resolution_);

  if (!stages_.Has(InterceptionStage::kRequest) || !interceptor_ ||
      !interceptor_->handle_auth_) {
    std::move(callback).Run(true, std::nullopt);
    return;
  }
  state_ = State::kAuthRequired;
  auto request_info = BuildRequestInfo(nullptr);
  request_info->auth_challenge =
      std::make_unique<net::AuthChallengeInfo>(auth_info);
  pending_auth_callback_ = std::move(callback);
  NotifyClient(std::move(request_info));
}

}  // namespace content