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

#include "quiche/quic/tools/connect_tunnel.h"

#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "quiche/quic/core/quic_error_codes.h"
#include "quiche/quic/core/quic_server_id.h"
#include "quiche/quic/core/socket_factory.h"
#include "quiche/quic/platform/api/quic_socket_address.h"
#include "quiche/quic/tools/quic_backend_response.h"
#include "quiche/quic/tools/quic_name_lookup.h"
#include "quiche/quic/tools/quic_simple_server_backend.h"
#include "quiche/common/platform/api/quiche_logging.h"
#include "quiche/common/platform/api/quiche_mem_slice.h"
#include "quiche/spdy/core/http2_header_block.h"

namespace quic {

namespace {

// Arbitrarily chosen. No effort has been made to figure out an optimal size.
constexpr size_t kReadSize = 4 * 1024;

std::optional<QuicServerId> ValidateHeadersAndGetAuthority(
    const spdy::Http2HeaderBlock& request_headers) {
  QUICHE_DCHECK(request_headers.contains(":method"));
  QUICHE_DCHECK(request_headers.find(":method")->second == "CONNECT");
  QUICHE_DCHECK(!request_headers.contains(":protocol"));

  auto scheme_it = request_headers.find(":scheme");
  if (scheme_it != request_headers.end()) {
    QUICHE_DVLOG(1) << "CONNECT request contains unexpected scheme: "
                    << scheme_it->second;
    return std::nullopt;
  }

  auto path_it = request_headers.find(":path");
  if (path_it != request_headers.end()) {
    QUICHE_DVLOG(1) << "CONNECT request contains unexpected path: "
                    << path_it->second;
    return std::nullopt;
  }

  auto authority_it = request_headers.find(":authority");
  if (authority_it == request_headers.end() || authority_it->second.empty()) {
    QUICHE_DVLOG(1) << "CONNECT request missing authority";
    return std::nullopt;
  }

  // A valid CONNECT authority must contain host and port and nothing else, per
  // https://www.rfc-editor.org/rfc/rfc9110.html#name-connect. This matches the
  // host and port parsing rules for QuicServerId.
  std::optional<QuicServerId> server_id =
      QuicServerId::ParseFromHostPortString(authority_it->second);
  if (!server_id.has_value()) {
    QUICHE_DVLOG(1) << "CONNECT request authority is malformed: "
                    << authority_it->second;
    return std::nullopt;
  }

  return server_id;
}

bool ValidateAuthority(
    const QuicServerId& authority,
    const absl::flat_hash_set<QuicServerId>& acceptable_destinations) {
  if (acceptable_destinations.contains(authority)) {
    return true;
  }

  QUICHE_DVLOG(1) << "CONNECT request authority: "
                  << authority.ToHostPortString()
                  << " is not an acceptable allow-listed destiation ";
  return false;
}

}  // namespace

ConnectTunnel::ConnectTunnel(
    QuicSimpleServerBackend::RequestHandler* client_stream_request_handler,
    SocketFactory* socket_factory,
    absl::flat_hash_set<QuicServerId> acceptable_destinations)
    : acceptable_destinations_(std::move(acceptable_destinations)),
      socket_factory_(socket_factory),
      client_stream_request_handler_(client_stream_request_handler) {
  QUICHE_DCHECK(client_stream_request_handler_);
  QUICHE_DCHECK(socket_factory_);
}

ConnectTunnel::~ConnectTunnel() {
  // Expect client and destination sides of tunnel to both be closed before
  // destruction.
  QUICHE_DCHECK_EQ(client_stream_request_handler_, nullptr);
  QUICHE_DCHECK(!IsConnectedToDestination());
  QUICHE_DCHECK(!receive_started_);
}

void ConnectTunnel::OpenTunnel(const spdy::Http2HeaderBlock& request_headers) {
  QUICHE_DCHECK(!IsConnectedToDestination());

  std::optional<QuicServerId> authority =
      ValidateHeadersAndGetAuthority(request_headers);
  if (!authority.has_value()) {
    TerminateClientStream(
        "invalid request headers",
        QuicResetStreamError::FromIetf(QuicHttp3ErrorCode::MESSAGE_ERROR));
    return;
  }

  if (!ValidateAuthority(authority.value(), acceptable_destinations_)) {
    TerminateClientStream(
        "disallowed request authority",
        QuicResetStreamError::FromIetf(QuicHttp3ErrorCode::REQUEST_REJECTED));
    return;
  }

  QuicSocketAddress address =
      tools::LookupAddress(AF_UNSPEC, authority.value());
  if (!address.IsInitialized()) {
    TerminateClientStream("host resolution error");
    return;
  }

  destination_socket_ =
      socket_factory_->CreateTcpClientSocket(address,
                                             /*receive_buffer_size=*/0,
                                             /*send_buffer_size=*/0,
                                             /*async_visitor=*/this);
  QUICHE_DCHECK(destination_socket_);

  absl::Status connect_result = destination_socket_->ConnectBlocking();
  if (!connect_result.ok()) {
    TerminateClientStream(
        "error connecting TCP socket to destination server: " +
        connect_result.ToString());
    return;
  }

  QUICHE_DVLOG(1) << "CONNECT tunnel opened from stream "
                  << client_stream_request_handler_->stream_id() << " to "
                  << authority.value().ToHostPortString();

  SendConnectResponse();
  BeginAsyncReadFromDestination();
}

bool ConnectTunnel::IsConnectedToDestination() const {
  return !!destination_socket_;
}

void ConnectTunnel::SendDataToDestination(absl::string_view data) {
  QUICHE_DCHECK(IsConnectedToDestination());
  QUICHE_DCHECK(!data.empty());

  absl::Status send_result =
      destination_socket_->SendBlocking(std::string(data));
  if (!send_result.ok()) {
    TerminateClientStream("TCP error sending data to destination server: " +
                          send_result.ToString());
  }
}

void ConnectTunnel::OnClientStreamClose() {
  QUICHE_DCHECK(client_stream_request_handler_);

  QUICHE_DVLOG(1) << "CONNECT stream "
                  << client_stream_request_handler_->stream_id() << " closed";

  client_stream_request_handler_ = nullptr;

  if (IsConnectedToDestination()) {
    // TODO(ericorth): Consider just calling shutdown() on the socket rather
    // than fully disconnecting in order to allow a graceful TCP FIN stream
    // shutdown per
    // https://www.rfc-editor.org/rfc/rfc9114.html#name-the-connect-method.
    // Would require shutdown support in the socket library, and would need to
    // deal with the tunnel/socket outliving the client stream.
    destination_socket_->Disconnect();
  }

  // Clear socket pointer.
  destination_socket_.reset();
}

void ConnectTunnel::ConnectComplete(absl::Status /*status*/) {
  // Async connect not expected.
  QUICHE_NOTREACHED();
}

void ConnectTunnel::ReceiveComplete(
    absl::StatusOr<quiche::QuicheMemSlice> data) {
  QUICHE_DCHECK(IsConnectedToDestination());
  QUICHE_DCHECK(receive_started_);

  receive_started_ = false;

  if (!data.ok()) {
    if (client_stream_request_handler_) {
      TerminateClientStream("TCP error receiving data from destination server");
    } else {
      // This typically just means a receive operation was cancelled on calling
      // destination_socket_->Disconnect().
      QUICHE_DVLOG(1) << "TCP error receiving data from destination server "
                         "after stream already closed.";
    }
    return;
  } else if (data.value().empty()) {
    OnDestinationConnectionClosed();
    return;
  }

  QUICHE_DCHECK(client_stream_request_handler_);
  client_stream_request_handler_->SendStreamData(data.value().AsStringView(),
                                                 /*close_stream=*/false);

  BeginAsyncReadFromDestination();
}

void ConnectTunnel::SendComplete(absl::Status /*status*/) {
  // Async send not expected.
  QUICHE_NOTREACHED();
}

void ConnectTunnel::BeginAsyncReadFromDestination() {
  QUICHE_DCHECK(IsConnectedToDestination());
  QUICHE_DCHECK(client_stream_request_handler_);
  QUICHE_DCHECK(!receive_started_);

  receive_started_ = true;
  destination_socket_->ReceiveAsync(kReadSize);
}

void ConnectTunnel::OnDestinationConnectionClosed() {
  QUICHE_DCHECK(IsConnectedToDestination());
  QUICHE_DCHECK(client_stream_request_handler_);

  QUICHE_DVLOG(1) << "CONNECT stream "
                  << client_stream_request_handler_->stream_id()
                  << " destination connection closed";
  destination_socket_->Disconnect();

  // Clear socket pointer.
  destination_socket_.reset();

  // Extra check that nothing in the Disconnect could lead to terminating the
  // stream.
  QUICHE_DCHECK(client_stream_request_handler_);

  client_stream_request_handler_->SendStreamData("", /*close_stream=*/true);
}

void ConnectTunnel::SendConnectResponse() {
  QUICHE_DCHECK(IsConnectedToDestination());
  QUICHE_DCHECK(client_stream_request_handler_);

  spdy::Http2HeaderBlock response_headers;
  response_headers[":status"] = "200";

  QuicBackendResponse response;
  response.set_headers(std::move(response_headers));
  // Need to leave the stream open after sending the CONNECT response.
  response.set_response_type(QuicBackendResponse::INCOMPLETE_RESPONSE);

  client_stream_request_handler_->OnResponseBackendComplete(&response);
}

void ConnectTunnel::TerminateClientStream(absl::string_view error_description,
                                          QuicResetStreamError error_code) {
  QUICHE_DCHECK(client_stream_request_handler_);

  std::string error_description_str =
      error_description.empty() ? ""
                                : absl::StrCat(" due to ", error_description);
  QUICHE_DVLOG(1) << "Terminating CONNECT stream "
                  << client_stream_request_handler_->stream_id()
                  << " with error code " << error_code.ietf_application_code()
                  << error_description_str;

  client_stream_request_handler_->TerminateStreamWithError(error_code);
}

}  // namespace quic
