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

#include "net/quic/quic_session_pool_proxy_job.h"

#include "base/memory/weak_ptr.h"
#include "net/base/completion_once_callback.h"
#include "net/base/network_change_notifier.h"
#include "net/base/network_handle.h"
#include "net/base/request_priority.h"
#include "net/base/trace_constants.h"
#include "net/base/tracing.h"
#include "net/log/net_log_with_source.h"
#include "net/quic/address_utils.h"
#include "net/quic/quic_crypto_client_config_handle.h"
#include "net/quic/quic_http_stream.h"
#include "net/quic/quic_session_pool.h"
#include "net/third_party/quiche/src/quiche/quic/core/quic_packet_writer.h"
#include "net/third_party/quiche/src/quiche/quic/core/quic_versions.h"

namespace net {

QuicSessionPool::ProxyJob::ProxyJob(
    QuicSessionPool* pool,
    quic::ParsedQuicVersion quic_version,
    const QuicSessionAliasKey& key,
    NetworkTrafficAnnotationTag proxy_annotation_tag,
    const HttpUserAgentSettings* http_user_agent_settings,
    std::unique_ptr<CryptoClientConfigHandle> client_config_handle,
    RequestPriority priority,
    int cert_verify_flags,
    const NetLogWithSource& net_log)
    : QuicSessionPool::Job::Job(
          pool,
          key,
          std::move(client_config_handle),
          priority,
          NetLogWithSource::Make(
              net_log.net_log(),
              NetLogSourceType::QUIC_SESSION_POOL_PROXY_JOB)),
      io_callback_(base::BindRepeating(&QuicSessionPool::ProxyJob::OnIOComplete,
                                       base::Unretained(this))),
      quic_version_(quic_version),
      proxy_annotation_tag_(proxy_annotation_tag),
      cert_verify_flags_(cert_verify_flags),
      http_user_agent_settings_(http_user_agent_settings) {
  DCHECK(!Job::key().session_key().proxy_chain().is_direct());
}

QuicSessionPool::ProxyJob::~ProxyJob() = default;

int QuicSessionPool::ProxyJob::Run(CompletionOnceCallback callback) {
  int rv = DoLoop(OK);
  if (rv == ERR_IO_PENDING) {
    callback_ = std::move(callback);
  }

  return rv > 0 ? OK : rv;
}

void QuicSessionPool::ProxyJob::SetRequestExpectations(
    QuicSessionRequest* request) {
  // This Job does not do host resolution, but can notify when the session
  // creation is finished.
  const bool session_creation_finished =
      session_attempt_ && session_attempt_->session_creation_finished();
  if (!session_creation_finished) {
    request->ExpectQuicSessionCreation();
  }
}

void QuicSessionPool::ProxyJob::PopulateNetErrorDetails(
    NetErrorDetails* details) const {
  // First, prefer any error details reported from creating the session over
  // which this job is carried.
  if (net_error_details_.quic_connection_error != quic::QUIC_NO_ERROR) {
    *details = net_error_details_;
    return;
  }

  // Second, prefer to include error details from the session over which this
  // job is carried, as any error in that session is "closer to" the client.
  if (proxy_session_) {
    proxy_session_->PopulateNetErrorDetails(details);
    if (details->quic_connection_error != quic::QUIC_NO_ERROR) {
      return;
    }
  }

  // Finally, return the error from the session attempt.
  if (!session_attempt_ || !session_attempt_->session()) {
    return;
  }
  details->connection_info = QuicHttpStream::ConnectionInfoFromQuicVersion(
      session_attempt_->session()->connection()->version());
  details->quic_connection_error = session_attempt_->session()->error();
}

int QuicSessionPool::ProxyJob::DoLoop(int rv) {
  do {
    IoState state = io_state_;
    io_state_ = STATE_NONE;
    switch (state) {
      case STATE_CREATE_PROXY_SESSION:
        CHECK_EQ(OK, rv);
        rv = DoCreateProxySession();
        break;
      case STATE_CREATE_PROXY_SESSION_COMPLETE:
        rv = DoCreateProxySessionComplete(rv);
        break;
      case STATE_CREATE_PROXY_STREAM:
        CHECK_EQ(OK, rv);
        rv = DoCreateProxyStream();
        break;
      case STATE_CREATE_PROXY_STREAM_COMPLETE:
        rv = DoCreateProxyStreamComplete(rv);
        break;
      case STATE_ATTEMPT_SESSION:
        rv = DoAttemptSession();
        break;
      default:
        NOTREACHED() << "io_state_: " << io_state_;
        break;
    }
  } while (io_state_ != STATE_NONE && rv != ERR_IO_PENDING);
  return rv;
}

void QuicSessionPool::ProxyJob::OnSessionAttemptComplete(int rv) {
  CHECK_NE(rv, ERR_IO_PENDING);
  if (!callback_.is_null()) {
    std::move(callback_).Run(rv);
  }
}

void QuicSessionPool::ProxyJob::OnIOComplete(int rv) {
  rv = DoLoop(rv);
  if (rv != ERR_IO_PENDING && !callback_.is_null()) {
    std::move(callback_).Run(rv);
  }
}

int QuicSessionPool::ProxyJob::DoCreateProxySession() {
  io_state_ = STATE_CREATE_PROXY_SESSION_COMPLETE;

  net_log().BeginEvent(NetLogEventType::QUIC_SESSION_POOL_PROXY_JOB_CONNECT);

  const QuicSessionKey& session_key = key_.session_key();
  auto [proxy_chain_prefix, last_proxy_server] =
      session_key.proxy_chain().SplitLast();
  auto last_server = last_proxy_server.host_port_pair();
  url::SchemeHostPort destination(url::kHttpsScheme, last_server.host(),
                                  last_server.port());

  net_log_.BeginEventWithStringParams(
      NetLogEventType::QUIC_SESSION_POOL_PROXY_JOB_CREATE_PROXY_SESSION,
      "destination", destination.Serialize());

  proxy_session_request_ = std::make_unique<QuicSessionRequest>(pool_);
  return proxy_session_request_->Request(
      destination, quic_version_, proxy_chain_prefix, proxy_annotation_tag_,
      http_user_agent_settings_.get(), SessionUsage::kProxy,
      session_key.privacy_mode(), priority(), session_key.socket_tag(),
      session_key.network_anonymization_key(), session_key.secure_dns_policy(),
      session_key.require_dns_https_alpn(), cert_verify_flags_,
      GURL("https://" + last_server.ToString()), net_log(), &net_error_details_,
      /*failed_on_default_network_callback=*/CompletionOnceCallback(),
      io_callback_);
}

int QuicSessionPool::ProxyJob::DoCreateProxySessionComplete(int rv) {
  net_log().EndEventWithNetErrorCode(
      NetLogEventType::QUIC_SESSION_POOL_PROXY_JOB_CREATE_PROXY_SESSION, rv);
  if (rv != 0) {
    proxy_session_request_.reset();
    return rv;
  }
  io_state_ = STATE_CREATE_PROXY_STREAM;
  proxy_session_ = proxy_session_request_->ReleaseSessionHandle();
  proxy_session_request_.reset();

  return OK;
}

int QuicSessionPool::ProxyJob::DoCreateProxyStream() {
  // Requiring confirmation here means more confidence that the underlying
  // connection is working before building the proxy tunnel, at the cost of one
  // more round-trip.
  io_state_ = STATE_CREATE_PROXY_STREAM_COMPLETE;
  return proxy_session_->RequestStream(/*requires_confirmation=*/true,
                                       io_callback_, proxy_annotation_tag_);
}

int QuicSessionPool::ProxyJob::DoCreateProxyStreamComplete(int rv) {
  if (rv != 0) {
    return rv;
  }
  proxy_stream_ = proxy_session_->ReleaseStream();

  DCHECK(proxy_stream_);
  if (!proxy_stream_->IsOpen()) {
    return ERR_CONNECTION_CLOSED;
  }

  io_state_ = STATE_ATTEMPT_SESSION;
  return OK;
}

int QuicSessionPool::ProxyJob::DoAttemptSession() {
  IPEndPoint local_address;
  int rv = proxy_session_->GetSelfAddress(&local_address);
  if (rv != 0) {
    return rv;
  }

  IPEndPoint peer_address;
  rv = proxy_session_->GetPeerAddress(&peer_address);
  if (rv != 0) {
    return rv;
  }

  session_attempt_ = std::make_unique<SessionAttempt>(
      this, std::move(local_address), std::move(peer_address), quic_version_,
      cert_verify_flags_, std::move(proxy_stream_), http_user_agent_settings_);

  return session_attempt_->Start(
      base::BindOnce(&ProxyJob::OnSessionAttemptComplete, GetWeakPtr()));
}

}  // namespace net
