blob: bd0b66eaad9d1bd397fe4e218a0ac59d2938f088 [file] [log] [blame]
// 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 <limits>
#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/optional.h"
#include "absl/types/span.h"
#include "url/third_party/mozilla/url_parse.h"
#include "quiche/quic/core/io/socket_factory.h"
#include "quiche/quic/core/quic_error_codes.h"
#include "quiche/quic/platform/api/quic_socket_address.h"
#include "quiche/quic/tools/quic_backend_response.h"
#include "quiche/quic/tools/quic_client.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;
absl::optional<ConnectTunnel::HostAndPort> ValidateAndParseAuthorityString(
absl::string_view authority_string) {
url::Component username_component;
url::Component password_component;
url::Component host_component;
url::Component port_component;
url::ParseAuthority(authority_string.data(),
url::Component(0, authority_string.size()),
&username_component, &password_component, &host_component,
&port_component);
// A valid CONNECT authority must contain host and port and nothing else, per
// https://www.rfc-editor.org/rfc/rfc9110.html#name-connect.
if (username_component.is_valid() || password_component.is_valid() ||
!host_component.is_nonempty() || !port_component.is_nonempty()) {
QUICHE_DVLOG(1) << "CONNECT request authority is malformed: "
<< authority_string;
return absl::nullopt;
}
QUICHE_DCHECK_LT(static_cast<size_t>(host_component.end()),
authority_string.length());
if (authority_string.length() > 2 &&
authority_string.data()[host_component.begin] == '[' &&
authority_string.data()[host_component.end() - 1] == ']') {
// Strip "[]" off IPv6 literals.
host_component.begin += 1;
host_component.len -= 2;
}
std::string hostname(authority_string.data() + host_component.begin,
host_component.len);
int parsed_port_number =
url::ParsePort(authority_string.data(), port_component);
// Negative result is either invalid or unspecified, either of which is
// disallowed for a CONNECT authority. Port 0 is technically valid but
// reserved and not really usable in practice, so easiest to just disallow it
// here.
if (parsed_port_number <= 0) {
QUICHE_DVLOG(1) << "CONNECT request authority port is malformed: "
<< authority_string;
return absl::nullopt;
}
QUICHE_DCHECK_LE(parsed_port_number, std::numeric_limits<uint16_t>::max());
return std::make_pair(std::move(hostname),
static_cast<uint16_t>(parsed_port_number));
}
absl::optional<ConnectTunnel::HostAndPort> 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 absl::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 absl::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 absl::nullopt;
}
return ValidateAndParseAuthorityString(authority_it->second);
}
bool ValidateAuthority(
const ConnectTunnel::HostAndPort& authority,
const absl::flat_hash_set<std::pair<std::string, uint16_t>>&
acceptable_destinations) {
if (acceptable_destinations.contains(authority)) {
return true;
}
QUICHE_DVLOG(1) << "CONNECT request authority: "
<< absl::StrCat(authority.first, ":", authority.second)
<< " 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<HostAndPort> 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());
absl::optional<HostAndPort> authority =
ValidateHeadersAndGetAuthority(request_headers);
if (!authority) {
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().first,
absl::StrCat(authority.value().second));
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().first << ":" << authority.value().second;
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